mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 21:43:41 +00:00
Favorites improvements
This commit is contained in:
parent
784fc8cfc6
commit
f6a261662c
@ -1,6 +1,12 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
enum VideosApp: String, CaseIterable {
|
enum VideosApp: String, CaseIterable {
|
||||||
|
enum AppType: String {
|
||||||
|
case local
|
||||||
|
case youTube
|
||||||
|
case peerTube
|
||||||
|
}
|
||||||
|
|
||||||
case local
|
case local
|
||||||
case invidious
|
case invidious
|
||||||
case piped
|
case piped
|
||||||
@ -10,6 +16,19 @@ enum VideosApp: String, CaseIterable {
|
|||||||
rawValue.capitalized
|
rawValue.capitalized
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var appType: AppType {
|
||||||
|
switch self {
|
||||||
|
case .local:
|
||||||
|
return .local
|
||||||
|
case .invidious:
|
||||||
|
return .youTube
|
||||||
|
case .piped:
|
||||||
|
return .youTube
|
||||||
|
case .peerTube:
|
||||||
|
return .peerTube
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var supportsAccounts: Bool {
|
var supportsAccounts: Bool {
|
||||||
self != .local
|
self != .local
|
||||||
}
|
}
|
||||||
|
@ -6,9 +6,9 @@ struct FavoriteItem: Codable, Equatable, Identifiable, Defaults.Serializable {
|
|||||||
case subscriptions
|
case subscriptions
|
||||||
case popular
|
case popular
|
||||||
case trending(String, String?)
|
case trending(String, String?)
|
||||||
case channel(String, String)
|
case channel(String, String, String)
|
||||||
case playlist(String)
|
case playlist(String)
|
||||||
case channelPlaylist(String, String)
|
case channelPlaylist(String, String, String)
|
||||||
case searchQuery(String, String, String, String)
|
case searchQuery(String, String, String, String)
|
||||||
|
|
||||||
var label: String {
|
var label: String {
|
||||||
@ -21,9 +21,9 @@ struct FavoriteItem: Codable, Equatable, Identifiable, Defaults.Serializable {
|
|||||||
let trendingCountry = Country(rawValue: country)!
|
let trendingCountry = Country(rawValue: country)!
|
||||||
let trendingCategory = category.isNil ? nil : TrendingCategory(rawValue: category!)
|
let trendingCategory = category.isNil ? nil : TrendingCategory(rawValue: category!)
|
||||||
return "\(trendingCountry.flag) \(trendingCountry.id) \(trendingCategory?.name ?? "Trending")"
|
return "\(trendingCountry.flag) \(trendingCountry.id) \(trendingCategory?.name ?? "Trending")"
|
||||||
case let .channel(_, name):
|
case let .channel(_, _, name):
|
||||||
return name
|
return name
|
||||||
case let .channelPlaylist(_, name):
|
case let .channelPlaylist(_, _, name):
|
||||||
return name
|
return name
|
||||||
case let .searchQuery(text, date, duration, order):
|
case let .searchQuery(text, date, duration, order):
|
||||||
var label = "Search: \"\(text)\""
|
var label = "Search: \"\(text)\""
|
||||||
|
@ -237,6 +237,13 @@ final class NavigationModel: ObservableObject {
|
|||||||
presentingAlert = true
|
presentingAlert = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hideViewsAboveBrowser() {
|
||||||
|
player.hide()
|
||||||
|
presentingChannel = false
|
||||||
|
presentingPlaylist = false
|
||||||
|
presentingOpenVideos = false
|
||||||
|
}
|
||||||
|
|
||||||
func hideKeyboard() {
|
func hideKeyboard() {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||||
|
@ -4,32 +4,27 @@ import SwiftUI
|
|||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
struct FavoriteItemView: View {
|
struct FavoriteItemView: View {
|
||||||
let item: FavoriteItem
|
var item: FavoriteItem
|
||||||
|
|
||||||
|
@Environment(\.navigationStyle) private var navigationStyle
|
||||||
@StateObject private var store = FavoriteResourceObserver()
|
@StateObject private var store = FavoriteResourceObserver()
|
||||||
|
|
||||||
@Default(.favorites) private var favorites
|
@Default(.favorites) private var favorites
|
||||||
@Binding private var dragging: FavoriteItem?
|
|
||||||
|
|
||||||
@ObservedObject private var accounts = AccountsModel.shared
|
@ObservedObject private var accounts = AccountsModel.shared
|
||||||
private var playlists = PlaylistsModel.shared
|
private var playlists = PlaylistsModel.shared
|
||||||
private var favoritesModel = FavoritesModel.shared
|
private var favoritesModel = FavoritesModel.shared
|
||||||
|
private var navigation = NavigationModel.shared
|
||||||
|
|
||||||
init(
|
init(item: FavoriteItem) {
|
||||||
item: FavoriteItem,
|
|
||||||
dragging: Binding<FavoriteItem?>
|
|
||||||
) {
|
|
||||||
self.item = item
|
self.item = item
|
||||||
_dragging = dragging
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
if isVisible {
|
if isVisible {
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
Text(label)
|
itemControl
|
||||||
.font(.title3.bold())
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
Button {
|
Button {
|
||||||
favoritesModel.remove(item)
|
favoritesModel.remove(item)
|
||||||
@ -46,19 +41,15 @@ struct FavoriteItemView: View {
|
|||||||
|
|
||||||
HorizontalCells(items: store.contentItems)
|
HorizontalCells(items: store.contentItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
#if os(macOS)
|
.onAppear {
|
||||||
.opacity(dragging?.id == item.id ? 0.5 : 1)
|
resource?.addObserver(store)
|
||||||
#endif
|
if item.section == .subscriptions {
|
||||||
.onAppear {
|
cacheFeed(resource?.loadIfNeeded())
|
||||||
resource?.addObserver(store)
|
} else {
|
||||||
if item.section == .subscriptions {
|
resource?.loadIfNeeded()
|
||||||
cacheFeed(resource?.loadIfNeeded())
|
|
||||||
} else {
|
|
||||||
resource?.loadIfNeeded()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: accounts.current) { _ in
|
.onChange(of: accounts.current) { _ in
|
||||||
@ -71,6 +62,103 @@ struct FavoriteItemView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var itemControl: some View {
|
||||||
|
VStack {
|
||||||
|
#if os(tvOS)
|
||||||
|
itemButton
|
||||||
|
#else
|
||||||
|
if itemIsNavigationLink {
|
||||||
|
itemNavigationLink
|
||||||
|
} else {
|
||||||
|
itemButton
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemButton: some View {
|
||||||
|
Button(action: itemButtonAction) {
|
||||||
|
itemLabel
|
||||||
|
.foregroundColor(.accentColor)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemNavigationLink: some View {
|
||||||
|
NavigationLink(destination: itemNavigationLinkDestination) {
|
||||||
|
itemLabel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemIsNavigationLink: Bool {
|
||||||
|
switch item.section {
|
||||||
|
case .channel:
|
||||||
|
return navigationStyle == .tab
|
||||||
|
case .channelPlaylist:
|
||||||
|
return navigationStyle == .tab
|
||||||
|
case .subscriptions:
|
||||||
|
return navigationStyle == .tab
|
||||||
|
case .popular:
|
||||||
|
return navigationStyle == .tab
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder var itemNavigationLinkDestination: some View {
|
||||||
|
Group {
|
||||||
|
switch item.section {
|
||||||
|
case let .channel(_, id, name):
|
||||||
|
ChannelVideosView(channel: .init(id: id, name: name))
|
||||||
|
case let .channelPlaylist(_, id, title):
|
||||||
|
ChannelPlaylistView(playlist: .init(id: id, title: title))
|
||||||
|
case .subscriptions:
|
||||||
|
SubscriptionsView()
|
||||||
|
case .popular:
|
||||||
|
PopularView()
|
||||||
|
default:
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.modifier(PlayerOverlayModifier())
|
||||||
|
}
|
||||||
|
|
||||||
|
func itemButtonAction() {
|
||||||
|
switch item.section {
|
||||||
|
case let .channel(_, id, name):
|
||||||
|
NavigationModel.shared.openChannel(.init(id: id, name: name), navigationStyle: navigationStyle)
|
||||||
|
case let .channelPlaylist(_, id, title):
|
||||||
|
NavigationModel.shared.openChannelPlaylist(.init(id: id, title: title), navigationStyle: navigationStyle)
|
||||||
|
case .subscriptions:
|
||||||
|
navigation.hideViewsAboveBrowser()
|
||||||
|
navigation.tabSelection = .subscriptions
|
||||||
|
case .popular:
|
||||||
|
navigation.hideViewsAboveBrowser()
|
||||||
|
navigation.tabSelection = .popular
|
||||||
|
case let .trending(country, category):
|
||||||
|
navigation.hideViewsAboveBrowser()
|
||||||
|
Defaults[.trendingCountry] = .init(rawValue: country) ?? .us
|
||||||
|
Defaults[.trendingCategory] = category.isNil ? .default : (.init(rawValue: category!) ?? .default)
|
||||||
|
navigation.tabSelection = .trending
|
||||||
|
case let .searchQuery(text, _, _, _):
|
||||||
|
navigation.hideViewsAboveBrowser()
|
||||||
|
navigation.openSearchQuery(text)
|
||||||
|
case let .playlist(id):
|
||||||
|
navigation.tabSelection = .playlist(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemLabel: some View {
|
||||||
|
HStack {
|
||||||
|
Text(label)
|
||||||
|
.font(.title3.bold())
|
||||||
|
Image(systemName: "chevron.right")
|
||||||
|
.imageScale(.small)
|
||||||
|
}
|
||||||
|
.lineLimit(1)
|
||||||
|
.padding(.trailing, 10)
|
||||||
|
}
|
||||||
|
|
||||||
private func cacheFeed(_ request: Request?) {
|
private func cacheFeed(_ request: Request?) {
|
||||||
request?.onSuccess { response in
|
request?.onSuccess { response in
|
||||||
if let videos: [Video] = response.typedContent() {
|
if let videos: [Video] = response.typedContent() {
|
||||||
@ -85,6 +173,12 @@ struct FavoriteItemView: View {
|
|||||||
return accounts.app.supportsSubscriptions && accounts.signedIn
|
return accounts.app.supportsSubscriptions && accounts.signedIn
|
||||||
case .popular:
|
case .popular:
|
||||||
return accounts.app.supportsPopular
|
return accounts.app.supportsPopular
|
||||||
|
case let .channel(appType, _, _):
|
||||||
|
guard let appType = VideosApp.AppType(rawValue: appType) else { return false }
|
||||||
|
return accounts.app.appType == appType
|
||||||
|
case let .channelPlaylist(appType, _, _):
|
||||||
|
guard let appType = VideosApp.AppType(rawValue: appType) else { return false }
|
||||||
|
return accounts.app.appType == appType
|
||||||
default:
|
default:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -108,10 +202,10 @@ struct FavoriteItemView: View {
|
|||||||
|
|
||||||
return accounts.api.trending(country: trendingCountry, category: trendingCategory)
|
return accounts.api.trending(country: trendingCountry, category: trendingCategory)
|
||||||
|
|
||||||
case let .channel(id, _):
|
case let .channel(_, id, _):
|
||||||
return accounts.api.channelVideos(id)
|
return accounts.api.channelVideos(id)
|
||||||
|
|
||||||
case let .channelPlaylist(id, _):
|
case let .channelPlaylist(_, id, _):
|
||||||
return accounts.api.channelPlaylist(id)
|
return accounts.api.channelPlaylist(id)
|
||||||
|
|
||||||
case let .playlist(id):
|
case let .playlist(id):
|
||||||
@ -140,3 +234,16 @@ struct FavoriteItemView: View {
|
|||||||
return item.section.label.localized()
|
return item.section.label.localized()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct FavoriteItemView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
NavigationView {
|
||||||
|
VStack {
|
||||||
|
FavoriteItemView(item: .init(section: .channel("peerTube", "a", "Search: resistance body upper band workout")))
|
||||||
|
.environment(\.navigationStyle, .tab)
|
||||||
|
FavoriteItemView(item: .init(section: .channel("peerTube", "a", "Marques")))
|
||||||
|
.environment(\.navigationStyle, .sidebar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -6,9 +6,7 @@ import UniformTypeIdentifiers
|
|||||||
struct HomeView: View {
|
struct HomeView: View {
|
||||||
@ObservedObject private var accounts = AccountsModel.shared
|
@ObservedObject private var accounts = AccountsModel.shared
|
||||||
|
|
||||||
@State private var dragging: FavoriteItem?
|
|
||||||
@State private var presentingEditFavorites = false
|
@State private var presentingEditFavorites = false
|
||||||
|
|
||||||
@State private var favoritesChanged = false
|
@State private var favoritesChanged = false
|
||||||
|
|
||||||
@FetchRequest(sortDescriptors: [.init(key: "watchedAt", ascending: false)])
|
@FetchRequest(sortDescriptors: [.init(key: "watchedAt", ascending: false)])
|
||||||
@ -75,11 +73,11 @@ struct HomeView: View {
|
|||||||
if !accounts.current.isNil, showFavoritesInHome {
|
if !accounts.current.isNil, showFavoritesInHome {
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
ForEach(Defaults[.favorites]) { item in
|
ForEach(Defaults[.favorites]) { item in
|
||||||
FavoriteItemView(item: item, dragging: $dragging)
|
FavoriteItemView(item: item)
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
ForEach(favorites) { item in
|
ForEach(favorites) { item in
|
||||||
FavoriteItemView(item: item, dragging: $dragging)
|
FavoriteItemView(item: item)
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.workaroundForVerticalScrollingBug()
|
.workaroundForVerticalScrollingBug()
|
||||||
#endif
|
#endif
|
||||||
|
@ -15,13 +15,6 @@ struct OpenURLHandler {
|
|||||||
var navigationStyle = NavigationStyle.sidebar
|
var navigationStyle = NavigationStyle.sidebar
|
||||||
|
|
||||||
func handle(_ url: URL) {
|
func handle(_ url: URL) {
|
||||||
if Self.firstHandle {
|
|
||||||
Self.firstHandle = false
|
|
||||||
|
|
||||||
Delay.by(1) { Self.shared.handle(url) }
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if accounts.current.isNil {
|
if accounts.current.isNil {
|
||||||
accounts.setCurrent(accounts.any)
|
accounts.setCurrent(accounts.any)
|
||||||
}
|
}
|
||||||
@ -52,27 +45,27 @@ struct OpenURLHandler {
|
|||||||
case .search:
|
case .search:
|
||||||
handleSearchUrlOpen(parser)
|
handleSearchUrlOpen(parser)
|
||||||
case .favorites:
|
case .favorites:
|
||||||
hideViewsAboveBrowser()
|
navigation.hideViewsAboveBrowser()
|
||||||
navigation.tabSelection = .home
|
navigation.tabSelection = .home
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
focusMainWindow()
|
focusMainWindow()
|
||||||
#endif
|
#endif
|
||||||
case .subscriptions:
|
case .subscriptions:
|
||||||
guard accounts.app.supportsSubscriptions, accounts.signedIn else { return }
|
guard accounts.app.supportsSubscriptions, accounts.signedIn else { return }
|
||||||
hideViewsAboveBrowser()
|
navigation.hideViewsAboveBrowser()
|
||||||
navigation.tabSelection = .subscriptions
|
navigation.tabSelection = .subscriptions
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
focusMainWindow()
|
focusMainWindow()
|
||||||
#endif
|
#endif
|
||||||
case .popular:
|
case .popular:
|
||||||
guard accounts.app.supportsPopular else { return }
|
guard accounts.app.supportsPopular else { return }
|
||||||
hideViewsAboveBrowser()
|
navigation.hideViewsAboveBrowser()
|
||||||
navigation.tabSelection = .popular
|
navigation.tabSelection = .popular
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
focusMainWindow()
|
focusMainWindow()
|
||||||
#endif
|
#endif
|
||||||
case .trending:
|
case .trending:
|
||||||
hideViewsAboveBrowser()
|
navigation.hideViewsAboveBrowser()
|
||||||
navigation.tabSelection = .trending
|
navigation.tabSelection = .trending
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
focusMainWindow()
|
focusMainWindow()
|
||||||
@ -86,13 +79,6 @@ struct OpenURLHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func hideViewsAboveBrowser() {
|
|
||||||
player.hide()
|
|
||||||
navigation.presentingChannel = false
|
|
||||||
navigation.presentingPlaylist = false
|
|
||||||
navigation.presentingOpenVideos = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleFileURLOpen(_ parser: URLParser) {
|
private func handleFileURLOpen(_ parser: URLParser) {
|
||||||
guard let url = parser.fileURL else { return }
|
guard let url = parser.fileURL else { return }
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@ struct ChannelPlaylistView: View {
|
|||||||
ShareButton(contentItem: contentItem)
|
ShareButton(contentItem: contentItem)
|
||||||
|
|
||||||
if let playlist = presentedPlaylist {
|
if let playlist = presentedPlaylist {
|
||||||
FavoriteButton(item: FavoriteItem(section: .channelPlaylist(playlist.id, playlist.title)))
|
FavoriteButton(item: FavoriteItem(section: .channelPlaylist(accounts.app.appType.rawValue, playlist.id, playlist.title)))
|
||||||
}
|
}
|
||||||
|
|
||||||
playButton
|
playButton
|
||||||
|
@ -231,7 +231,7 @@ struct ChannelVideosView: View {
|
|||||||
contentTypePicker
|
contentTypePicker
|
||||||
Section {
|
Section {
|
||||||
subscriptionToggleButton
|
subscriptionToggleButton
|
||||||
FavoriteButton(item: FavoriteItem(section: .channel(channel.id, channel.name)))
|
FavoriteButton(item: FavoriteItem(section: .channel(accounts.app.appType.rawValue, channel.id, channel.name)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
|
@ -66,7 +66,7 @@ struct PlaylistVideosView: View {
|
|||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: playlistButtonsPlacement) {
|
ToolbarItem(placement: playlistButtonsPlacement) {
|
||||||
HStack {
|
HStack {
|
||||||
FavoriteButton(item: FavoriteItem(section: .channelPlaylist(playlist.id, playlist.title)))
|
FavoriteButton(item: FavoriteItem(section: .channelPlaylist(accounts.app.appType.rawValue, playlist.id, playlist.title)))
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
player.play(videos)
|
player.play(videos)
|
||||||
|
Loading…
Reference in New Issue
Block a user