From ee1cb924c9aadaf95973b4afca624dd3463141cb Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Sun, 19 Sep 2021 13:06:54 +0200 Subject: [PATCH] Recently opened for sidebar navigation --- Model/NavigationState.swift | 45 +------ Model/Recents.swift | 116 ++++++++++++++++++ Model/SearchState.swift | 21 +++- Pearvidious.xcodeproj/project.pbxproj | 24 ++-- Shared/Defaults.swift | 31 ++--- .../Modifiers/UnsubscribeAlertModifier.swift | 6 +- Shared/Navigation/AppSidebarNavigation.swift | 7 +- .../Navigation/AppSidebarRecentlyOpened.swift | 54 -------- Shared/Navigation/AppSidebarRecents.swift | 91 ++++++++++++++ Shared/Navigation/AppTabNavigation.swift | 15 +-- Shared/Navigation/ContentView.swift | 6 +- Shared/Player/VideoPlayerView.swift | 2 - Shared/Views/SearchView.swift | 14 ++- Shared/Views/VideoContextMenuView.swift | 19 +-- tvOS/TVNavigationView.swift | 18 +-- tvOS/VideoDetailsView.swift | 113 ----------------- 16 files changed, 291 insertions(+), 291 deletions(-) create mode 100644 Model/Recents.swift delete mode 100644 Shared/Navigation/AppSidebarRecentlyOpened.swift create mode 100644 Shared/Navigation/AppSidebarRecents.swift delete mode 100644 tvOS/VideoDetailsView.swift diff --git a/Model/NavigationState.swift b/Model/NavigationState.swift index 378b89b1..20aaf040 100644 --- a/Model/NavigationState.swift +++ b/Model/NavigationState.swift @@ -3,12 +3,11 @@ import SwiftUI final class NavigationState: ObservableObject { enum TabSelection: Hashable { - case watchNow, subscriptions, popular, trending, playlists, channel(String), playlist(String), search + case watchNow, subscriptions, popular, trending, playlists, channel(String), playlist(String), recentlyOpened(String), search } @Published var tabSelection: TabSelection = .watchNow - @Published var showingVideoDetails = false @Published var showingVideo = false @Published var video: Video? @@ -20,56 +19,14 @@ final class NavigationState: ObservableObject { @Published var presentingUnsubscribeAlert = false @Published var channelToUnsubscribe: Channel! - @Published var openChannels = Set() @Published var isChannelOpen = false @Published var sidebarSectionChanged = false - func openChannel(_ channel: Channel) { - openChannels.insert(channel) - - isChannelOpen = true - } - - func closeChannel(_ channel: Channel) { - guard openChannels.remove(channel) != nil else { - return - } - - isChannelOpen = !openChannels.isEmpty - - if tabSelection == .channel(channel.id) { - tabSelection = .subscriptions - } - } - - func showOpenChannel(_ id: Channel.ID) -> Bool { - if case .channel = tabSelection { - return false - } else { - return !openChannels.contains { $0.id == id } - } - } - - func openVideoDetails(_ video: Video) { - self.video = video - showingVideoDetails = true - } - - func closeVideoDetails() { - showingVideoDetails = false - video = nil - } - func playVideo(_ video: Video) { self.video = video showingVideo = true } - func showVideoDetailsIfNeeded() { - showingVideoDetails = returnToDetails - returnToDetails = false - } - var tabSelectionOptionalBinding: Binding { Binding( get: { diff --git a/Model/Recents.swift b/Model/Recents.swift new file mode 100644 index 00000000..d46edb59 --- /dev/null +++ b/Model/Recents.swift @@ -0,0 +1,116 @@ +import Defaults +import Foundation + +final class Recents: ObservableObject { + @Default(.recentlyOpened) var items + + var isEmpty: Bool { + items.isEmpty + } + + func clear() { + items = [] + } + + func clearQueries() { + items.removeAll { $0.type == .query } + } + + func open(_ item: RecentItem) { + if !items.contains(where: { $0.id == item.id }) { + items.append(item) + } + } + + func close(_ item: RecentItem) { + if let index = items.firstIndex(where: { $0.id == item.id }) { + items.remove(at: index) + } + } + + var presentedChannel: Channel? { + if let recent = items.last(where: { $0.type == .channel }) { + return recent.channel + } + + return nil + } +} + +struct RecentItem: Defaults.Serializable, Identifiable { + static var bridge = RecentItemBridge() + + enum ItemType: String { + case channel, query + } + + var type: ItemType + var id: String + var title: String + + var tag: String { + "recent\(type.rawValue.capitalized)\(id)" + } + + var query: SearchQuery? { + guard type == .query else { + return nil + } + + return SearchQuery(query: title) + } + + var channel: Channel? { + guard type == .channel else { + return nil + } + + return Channel(id: id, name: title) + } + + init(type: ItemType, identifier: String, title: String) { + self.type = type + id = identifier + self.title = title + } + + init(from channel: Channel) { + type = .channel + id = channel.id + title = channel.name + } +} + +struct RecentItemBridge: Defaults.Bridge { + typealias Value = RecentItem + typealias Serializable = [String: String] + + func serialize(_ value: Value?) -> Serializable? { + guard let value = value else { + return nil + } + + return [ + "type": value.type.rawValue, + "identifier": value.id, + "title": value.title + ] + } + + func deserialize(_ object: Serializable?) -> RecentItem? { + guard + let object = object, + let type = object["type"], + let identifier = object["identifier"], + let title = object["title"] + else { + return nil + } + + return RecentItem( + type: .init(rawValue: type)!, + identifier: identifier, + title: title + ) + } +} diff --git a/Model/SearchState.swift b/Model/SearchState.swift index 8d31d345..f1664b79 100644 --- a/Model/SearchState.swift +++ b/Model/SearchState.swift @@ -8,14 +8,11 @@ final class SearchState: ObservableObject { @Published var querySuggestions = Store<[String]>() - @Default(.searchQuery) private var queryText - private var previousResource: Resource? private var resource: Resource! init() { let newQuery = query - newQuery.query = queryText query = newQuery resource = InvidiousAPI.shared.search(newQuery) @@ -53,7 +50,23 @@ final class SearchState: ObservableObject { previousResource?.removeObservers(ownedBy: store) previousResource = newResource - queryText = query.query + resource = newResource + resource.addObserver(store) + loadResourceIfNeededAndReplaceStore() + } + + func resetQuery(_ query: SearchQuery) { + self.query = query + + let newResource = InvidiousAPI.shared.search(query) + guard newResource != previousResource else { + return + } + + store.replace([]) + + previousResource?.removeObservers(ownedBy: store) + previousResource = newResource resource = newResource resource.addObserver(store) diff --git a/Pearvidious.xcodeproj/project.pbxproj b/Pearvidious.xcodeproj/project.pbxproj index aed1e09e..325e076b 100644 --- a/Pearvidious.xcodeproj/project.pbxproj +++ b/Pearvidious.xcodeproj/project.pbxproj @@ -63,8 +63,8 @@ 3761AC0F26F0F9A600AA496F /* UnsubscribeAlertModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3761AC0E26F0F9A600AA496F /* UnsubscribeAlertModifier.swift */; }; 3761AC1026F0F9A600AA496F /* UnsubscribeAlertModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3761AC0E26F0F9A600AA496F /* UnsubscribeAlertModifier.swift */; }; 3761AC1126F0F9A600AA496F /* UnsubscribeAlertModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3761AC0E26F0F9A600AA496F /* UnsubscribeAlertModifier.swift */; }; - 3763495126DFF59D00B9A393 /* AppSidebarRecentlyOpened.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3763495026DFF59D00B9A393 /* AppSidebarRecentlyOpened.swift */; }; - 3763495226DFF59D00B9A393 /* AppSidebarRecentlyOpened.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3763495026DFF59D00B9A393 /* AppSidebarRecentlyOpened.swift */; }; + 3763495126DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3763495026DFF59D00B9A393 /* AppSidebarRecents.swift */; }; + 3763495226DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3763495026DFF59D00B9A393 /* AppSidebarRecents.swift */; }; 376578852685429C00D4EA09 /* CaseIterable+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376578842685429C00D4EA09 /* CaseIterable+Next.swift */; }; 376578862685429C00D4EA09 /* CaseIterable+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376578842685429C00D4EA09 /* CaseIterable+Next.swift */; }; 376578872685429C00D4EA09 /* CaseIterable+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376578842685429C00D4EA09 /* CaseIterable+Next.swift */; }; @@ -127,7 +127,6 @@ 37B17DA0268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */; }; 37B17DA1268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */; }; 37B17DA2268A1F8A006AEE9B /* VideoContextMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */; }; - 37B17DA6268A285E006AEE9B /* VideoDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B17DA3268A285E006AEE9B /* VideoDetailsView.swift */; }; 37B767DB2677C3CA0098BAA8 /* PlayerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B767DA2677C3CA0098BAA8 /* PlayerState.swift */; }; 37B767DC2677C3CA0098BAA8 /* PlayerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B767DA2677C3CA0098BAA8 /* PlayerState.swift */; }; 37B767DD2677C3CA0098BAA8 /* PlayerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B767DA2677C3CA0098BAA8 /* PlayerState.swift */; }; @@ -187,6 +186,9 @@ 37BE0BDA26A214630092E2DB /* PlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BD926A214630092E2DB /* PlayerViewController.swift */; }; 37BE0BDC26A2367F0092E2DB /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BDB26A2367F0092E2DB /* Player.swift */; }; 37BE0BE526A336910092E2DB /* OptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B76E95268747C900CE5671 /* OptionsView.swift */; }; + 37C194C726F6A9C8005D3B96 /* Recents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C194C626F6A9C8005D3B96 /* Recents.swift */; }; + 37C194C826F6A9C8005D3B96 /* Recents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C194C626F6A9C8005D3B96 /* Recents.swift */; }; + 37C194C926F6A9C8005D3B96 /* Recents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C194C626F6A9C8005D3B96 /* Recents.swift */; }; 37C7A1D5267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; }; 37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; }; 37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; }; @@ -285,7 +287,7 @@ 3748186D26A769D60084E870 /* DetailBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailBadge.swift; sourceTree = ""; }; 3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvironmentValues.swift; sourceTree = ""; }; 3761AC0E26F0F9A600AA496F /* UnsubscribeAlertModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnsubscribeAlertModifier.swift; sourceTree = ""; }; - 3763495026DFF59D00B9A393 /* AppSidebarRecentlyOpened.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarRecentlyOpened.swift; sourceTree = ""; }; + 3763495026DFF59D00B9A393 /* AppSidebarRecents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarRecents.swift; sourceTree = ""; }; 376578842685429C00D4EA09 /* CaseIterable+Next.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CaseIterable+Next.swift"; sourceTree = ""; }; 376578882685471400D4EA09 /* Playlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Playlist.swift; sourceTree = ""; }; 376578902685490700D4EA09 /* PlaylistsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistsView.swift; sourceTree = ""; }; @@ -306,7 +308,6 @@ 37AAF29926740A01007FC770 /* VideosListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideosListView.swift; sourceTree = ""; }; 37AAF29F26741C97007FC770 /* SubscriptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsView.swift; sourceTree = ""; }; 37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoContextMenuView.swift; sourceTree = ""; }; - 37B17DA3268A285E006AEE9B /* VideoDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDetailsView.swift; sourceTree = ""; }; 37B767DA2677C3CA0098BAA8 /* PlayerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerState.swift; sourceTree = ""; }; 37B76E95268747C900CE5671 /* OptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsView.swift; sourceTree = ""; }; 37B81AF826D2C9A700675966 /* VideoPlayerSizeModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerSizeModifier.swift; sourceTree = ""; }; @@ -331,6 +332,7 @@ 37BE0BD526A1D4A90092E2DB /* PlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewController.swift; sourceTree = ""; }; 37BE0BD926A214630092E2DB /* PlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewController.swift; sourceTree = ""; }; 37BE0BDB26A2367F0092E2DB /* Player.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = ""; }; + 37C194C626F6A9C8005D3B96 /* Recents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Recents.swift; sourceTree = ""; }; 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockSegment.swift; sourceTree = ""; }; 37C7A1DB267CE9D90010EAD6 /* Profile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profile.swift; sourceTree = ""; }; 37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleAssetStream.swift; sourceTree = ""; }; @@ -432,7 +434,7 @@ children = ( 37BD07BA2698AB60003EBB87 /* AppSidebarNavigation.swift */, 37BA794A26DC30EC002A0235 /* AppSidebarPlaylists.swift */, - 3763495026DFF59D00B9A393 /* AppSidebarRecentlyOpened.swift */, + 3763495026DFF59D00B9A393 /* AppSidebarRecents.swift */, 37BA794626DC2E56002A0235 /* AppSidebarSubscriptions.swift */, 37D4B0C32671614700C925CA /* AppTabNavigation.swift */, 37BD07B42698AA4D003EBB87 /* ContentView.swift */, @@ -665,7 +667,6 @@ 371AAE2926CF143200901972 /* Options */, 373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */, 37BAB54B269B39FD00E75ED1 /* TVNavigationView.swift */, - 37B17DA3268A285E006AEE9B /* VideoDetailsView.swift */, 37D4B15E267164AF00C925CA /* Assets.xcassets */, 37D4B1AE26729DEB00C925CA /* Info.plist */, ); @@ -692,6 +693,7 @@ 376578882685471400D4EA09 /* Playlist.swift */, 37BA794226DBA973002A0235 /* Playlists.swift */, 37C7A1DB267CE9D90010EAD6 /* Profile.swift */, + 37C194C626F6A9C8005D3B96 /* Recents.swift */, 373CFACA26966264003CB2C6 /* SearchQuery.swift */, 3711403E26B206A6005B3555 /* SearchState.swift */, 37EAD86E267B9ED100D9E01B /* Segment.swift */, @@ -998,7 +1000,7 @@ 37CEE4BD2677B670005A1EFE /* SingleAssetStream.swift in Sources */, 37BD07C82698B71C003EBB87 /* AppTabNavigation.swift in Sources */, 37EAD86B267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */, - 3763495126DFF59D00B9A393 /* AppSidebarRecentlyOpened.swift in Sources */, + 3763495126DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */, 37BA793B26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */, 37BD07B52698AA4D003EBB87 /* ContentView.swift in Sources */, 37152EEA26EFEB95004FB96D /* LazyView.swift in Sources */, @@ -1008,6 +1010,7 @@ 37BE0BD626A1D4A90092E2DB /* PlayerViewController.swift in Sources */, 37BA793F26DB8F97002A0235 /* ChannelVideosView.swift in Sources */, 37754C9D26B7500000DBD602 /* VideosView.swift in Sources */, + 37C194C726F6A9C8005D3B96 /* Recents.swift in Sources */, 3711403F26B206A6005B3555 /* SearchState.swift in Sources */, 37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */, 37BE0BD326A1D4780092E2DB /* Player.swift in Sources */, @@ -1070,6 +1073,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 37C194C826F6A9C8005D3B96 /* Recents.swift in Sources */, 37BE0BDC26A2367F0092E2DB /* Player.swift in Sources */, 3788AC2C26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */, 37CEE4BE2677B670005A1EFE /* SingleAssetStream.swift in Sources */, @@ -1135,7 +1139,7 @@ 3748186B26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */, 373CFADC269663F1003CB2C6 /* Thumbnail.swift in Sources */, 373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */, - 3763495226DFF59D00B9A393 /* AppSidebarRecentlyOpened.swift in Sources */, + 3763495226DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */, 37BA794426DBA973002A0235 /* Playlists.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1175,7 +1179,6 @@ 373CFAC926966188003CB2C6 /* SearchOptionsView.swift in Sources */, 37A9965C26D6F8CA006E3224 /* VideosCellsHorizontal.swift in Sources */, 37BD07C92698FBDB003EBB87 /* ContentView.swift in Sources */, - 37B17DA6268A285E006AEE9B /* VideoDetailsView.swift in Sources */, 37141671267A8ACC006CA35D /* TrendingView.swift in Sources */, 3788AC2926F6840700F6BAA9 /* WatchNowSection.swift in Sources */, 37BA794126DB8F97002A0235 /* ChannelVideosView.swift in Sources */, @@ -1206,6 +1209,7 @@ 37B17DA0268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */, 37BE0BD726A1D4A90092E2DB /* PlayerViewController.swift in Sources */, 37CEE4C32677B697005A1EFE /* Stream.swift in Sources */, + 37C194C926F6A9C8005D3B96 /* Recents.swift in Sources */, 37BE0BE526A336910092E2DB /* OptionsView.swift in Sources */, 37BA793D26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */, 3711404126B206A6005B3555 /* SearchState.swift in Sources */, diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index b6a5649b..f4c22eb2 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -1,5 +1,21 @@ import Defaults +extension Defaults.Keys { + #if os(tvOS) + static let layout = Key("listingLayout", default: .cells) + #endif + + static let searchSortOrder = Key("searchSortOrder", default: .relevance) + static let searchDate = Key("searchDate") + static let searchDuration = Key("searchDuration") + + static let selectedPlaylistID = Key("selectedPlaylistID") + static let showingAddToPlaylist = Key("showingAddToPlaylist", default: false) + static let videoIDToAddToPlaylist = Key("videoIDToAddToPlaylist") + + static let recentlyOpened = Key<[RecentItem]>("recentlyOpened", default: []) +} + enum ListingLayout: String, CaseIterable, Identifiable, Defaults.Serializable { case list, cells @@ -16,18 +32,3 @@ enum ListingLayout: String, CaseIterable, Identifiable, Defaults.Serializable { } } } - -extension Defaults.Keys { - #if os(tvOS) - static let layout = Key("listingLayout", default: .cells) - #endif - static let searchQuery = Key("searchQuery", default: "") - - static let searchSortOrder = Key("searchSortOrder", default: .relevance) - static let searchDate = Key("searchDate") - static let searchDuration = Key("searchDuration") - - static let selectedPlaylistID = Key("selectedPlaylistID") - static let showingAddToPlaylist = Key("showingAddToPlaylist", default: false) - static let videoIDToAddToPlaylist = Key("videoIDToAddToPlaylist") -} diff --git a/Shared/Modifiers/UnsubscribeAlertModifier.swift b/Shared/Modifiers/UnsubscribeAlertModifier.swift index 6037b2d3..7a73093e 100644 --- a/Shared/Modifiers/UnsubscribeAlertModifier.swift +++ b/Shared/Modifiers/UnsubscribeAlertModifier.swift @@ -10,11 +10,7 @@ struct UnsubscribeAlertModifier: ViewModifier { .alert(unsubscribeAlertTitle, isPresented: $navigationState.presentingUnsubscribeAlert) { if let channel = navigationState.channelToUnsubscribe { Button("Unsubscribe", role: .destructive) { - subscriptions.unsubscribe(channel.id) { - navigationState.openChannel(channel) - navigationState.tabSelection = .channel(channel.id) - navigationState.sidebarSectionChanged.toggle() - } + subscriptions.unsubscribe(channel.id) } } } diff --git a/Shared/Navigation/AppSidebarNavigation.swift b/Shared/Navigation/AppSidebarNavigation.swift index e1db00fd..468d4998 100644 --- a/Shared/Navigation/AppSidebarNavigation.swift +++ b/Shared/Navigation/AppSidebarNavigation.swift @@ -14,6 +14,7 @@ struct AppSidebarNavigation: View { @EnvironmentObject private var navigationState @EnvironmentObject private var playlists + @EnvironmentObject private var recents @EnvironmentObject private var searchState @EnvironmentObject private var subscriptions @@ -66,6 +67,8 @@ struct AppSidebarNavigation: View { query.query = self.searchQuery } + recents.open(RecentItem(type: .query, identifier: self.searchQuery, title: self.searchQuery)) + navigationState.tabSelection = .search } } @@ -111,7 +114,7 @@ struct AppSidebarNavigation: View { return Group { mainNavigationLinks - AppSidebarRecentlyOpened(selection: selection) + AppSidebarRecents(selection: selection) .id("recentlyOpened") AppSidebarSubscriptions(selection: selection) AppSidebarPlaylists(selection: selection) @@ -130,7 +133,7 @@ struct AppSidebarNavigation: View { } NavigationLink(destination: LazyView(SubscriptionsView()), tag: TabSelection.subscriptions, selection: selection) { - Label("Subscriptions", systemImage: "star.circle.fill") + Label("Subscriptions", systemImage: "star.circle") .accessibility(label: Text("Subscriptions")) } diff --git a/Shared/Navigation/AppSidebarRecentlyOpened.swift b/Shared/Navigation/AppSidebarRecentlyOpened.swift deleted file mode 100644 index 200a23ec..00000000 --- a/Shared/Navigation/AppSidebarRecentlyOpened.swift +++ /dev/null @@ -1,54 +0,0 @@ -import SwiftUI - -struct AppSidebarRecentlyOpened: View { - @Binding var selection: TabSelection? - - @EnvironmentObject private var navigationState - @EnvironmentObject private var subscriptions - - @State private var subscriptionsChanged = false - - var body: some View { - Group { - if !recentlyOpened.isEmpty { - Section(header: Text("Recently Opened")) { - ForEach(recentlyOpened) { channel in - NavigationLink(tag: TabSelection.channel(channel.id), selection: $selection) { - LazyView(ChannelVideosView(channel)) - } label: { - HStack { - Label(channel.name, systemImage: AppSidebarNavigation.symbolSystemImage(channel.name)) - - Spacer() - - Button(action: { navigationState.closeChannel(channel) }) { - Image(systemName: "xmark.circle.fill") - } - .foregroundColor(.secondary) - .buttonStyle(.plain) - } - } - - // force recalculating the view on change of subscriptions - .opacity(subscriptionsChanged ? 1 : 1) - .id(channel.id) - .contextMenu { - Button("Subscribe") { - subscriptions.subscribe(channel.id) { - navigationState.sidebarSectionChanged.toggle() - } - } - } - } - } - .onChange(of: subscriptions.all) { _ in - subscriptionsChanged.toggle() - } - } - } - } - - var recentlyOpened: [Channel] { - navigationState.openChannels.filter { !subscriptions.all.contains($0) } - } -} diff --git a/Shared/Navigation/AppSidebarRecents.swift b/Shared/Navigation/AppSidebarRecents.swift new file mode 100644 index 00000000..dd254283 --- /dev/null +++ b/Shared/Navigation/AppSidebarRecents.swift @@ -0,0 +1,91 @@ +import Defaults +import SwiftUI + +struct AppSidebarRecents: View { + @Binding var selection: TabSelection? + + @EnvironmentObject private var navigationState + @EnvironmentObject private var recents + + @Default(.recentlyOpened) private var recentItems + + var body: some View { + Group { + if !recentItems.isEmpty { + Section(header: Text("Recents")) { + ForEach(recentItems) { recent in + Group { + switch recent.type { + case .channel: + RecentNavigationLink(recent: recent, selection: $selection) { + LazyView(ChannelVideosView(Channel(id: recent.id, name: recent.title))) + } + case .query: + RecentNavigationLink(recent: recent, selection: $selection, systemImage: "magnifyingglass") { + LazyView(SearchView(recent.query!)) + } + } + } + .contextMenu { + Button("Clear All Recents") { + recents.clear() + } + + Button("Clear Search History") { + recents.clearQueries() + } + .disabled(!recentItems.contains { $0.type == .query }) + } + } + } + } + } + } +} + +struct RecentNavigationLink: View { + @EnvironmentObject private var recents + + var recent: RecentItem + @Binding var selection: TabSelection? + + var systemImage: String? + let destination: DestinationContent + + init( + recent: RecentItem, + selection: Binding, + systemImage: String? = nil, + @ViewBuilder destination: () -> DestinationContent + ) { + self.recent = recent + _selection = selection + self.systemImage = systemImage + self.destination = destination() + } + + var body: some View { + NavigationLink(tag: TabSelection.recentlyOpened(recent.tag), selection: $selection) { + destination + } label: { + HStack { + Label(recent.title, systemImage: labelSystemImage) + + Spacer() + + Button(action: { + recents.close(recent) + }) { + Image(systemName: "xmark.circle.fill") + } + .foregroundColor(.secondary) + .buttonStyle(.plain) + } + } + .id(recent.tag) + } + + var labelSystemImage: String { + systemImage != nil ? systemImage! : AppSidebarNavigation.symbolSystemImage(recent.title) + } +} diff --git a/Shared/Navigation/AppTabNavigation.swift b/Shared/Navigation/AppTabNavigation.swift index 24cce9df..1fa03a53 100644 --- a/Shared/Navigation/AppTabNavigation.swift +++ b/Shared/Navigation/AppTabNavigation.swift @@ -5,6 +5,8 @@ struct AppTabNavigation: View { @EnvironmentObject private var navigationState @EnvironmentObject private var searchState + @EnvironmentObject private var recents + @State private var searchQuery = "" var body: some View { @@ -82,18 +84,17 @@ struct AppTabNavigation: View { .tag(TabSelection.search) } .sheet(isPresented: $navigationState.isChannelOpen, onDismiss: { - navigationState.closeChannel(presentedChannel) + if let channel = recents.presentedChannel { + let recent = RecentItem(from: channel) + recents.close(recent) + } }) { - if presentedChannel != nil { + if recents.presentedChannel != nil { NavigationView { - ChannelVideosView(presentedChannel) + ChannelVideosView(recents.presentedChannel!) .environment(\.inNavigationView, true) } } } } - - fileprivate var presentedChannel: Channel! { - navigationState.openChannels.first - } } diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index 85986846..3816d4d8 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -3,9 +3,10 @@ import SwiftUI struct ContentView: View { @StateObject private var navigationState = NavigationState() @StateObject private var playbackState = PlaybackState() + @StateObject private var playlists = Playlists() + @StateObject private var recents = Recents() @StateObject private var searchState = SearchState() @StateObject private var subscriptions = Subscriptions() - @StateObject private var playlists = Playlists() #if os(iOS) @Environment(\.horizontalSizeClass) private var horizontalSizeClass @@ -44,9 +45,10 @@ struct ContentView: View { #endif .environmentObject(navigationState) .environmentObject(playbackState) + .environmentObject(playlists) + .environmentObject(recents) .environmentObject(searchState) .environmentObject(subscriptions) - .environmentObject(playlists) } } diff --git a/Shared/Player/VideoPlayerView.swift b/Shared/Player/VideoPlayerView.swift index 40474983..7a1a0d30 100644 --- a/Shared/Player/VideoPlayerView.swift +++ b/Shared/Player/VideoPlayerView.swift @@ -85,8 +85,6 @@ struct VideoPlayerView: View { .onDisappear { resource.removeObservers(ownedBy: store) resource.invalidate() - - navigationState.showingVideoDetails = navigationState.returnToDetails } #if os(macOS) .frame(maxWidth: 1000, minHeight: 700) diff --git a/Shared/Views/SearchView.swift b/Shared/Views/SearchView.swift index 4f1ef033..f56334f1 100644 --- a/Shared/Views/SearchView.swift +++ b/Shared/Views/SearchView.swift @@ -3,13 +3,18 @@ import Siesta import SwiftUI struct SearchView: View { - @Default(.searchQuery) private var queryText @Default(.searchSortOrder) private var searchSortOrder @Default(.searchDate) private var searchDate @Default(.searchDuration) private var searchDuration @EnvironmentObject private var state + private var query: SearchQuery? + + init(_ query: SearchQuery? = nil) { + self.query = query + } + var body: some View { VStack { VideosView(videos: state.store.collection) @@ -27,11 +32,8 @@ struct SearchView: View { } } .onAppear { - state.changeQuery { query in - query.query = queryText - query.sortBy = searchSortOrder - query.date = searchDate - query.duration = searchDuration + if query != nil { + state.resetQuery(query!) } } .onChange(of: state.query.query) { queryText in diff --git a/Shared/Views/VideoContextMenuView.swift b/Shared/Views/VideoContextMenuView.swift index 6b0f51ba..99447ff6 100644 --- a/Shared/Views/VideoContextMenuView.swift +++ b/Shared/Views/VideoContextMenuView.swift @@ -3,6 +3,7 @@ import SwiftUI struct VideoContextMenuView: View { @EnvironmentObject private var navigationState + @EnvironmentObject private var recents @EnvironmentObject private var subscriptions let video: Video @@ -14,15 +15,11 @@ struct VideoContextMenuView: View { var body: some View { Section { - if navigationState.showOpenChannel(video.channel.id) { - openChannelButton - } + openChannelButton subscriptionButton .opacity(subscribed ? 1 : 1) - openVideoDetailsButton - if navigationState.tabSelection == .playlists { removeFromPlaylistButton } else { @@ -33,8 +30,10 @@ struct VideoContextMenuView: View { var openChannelButton: some View { Button("\(video.author) Channel") { - navigationState.openChannel(video.channel) - navigationState.tabSelection = .channel(video.channel.id) + let recent = RecentItem(from: video.channel) + recents.open(recent) + navigationState.tabSelection = .recentlyOpened(recent.tag) + navigationState.isChannelOpen = true navigationState.sidebarSectionChanged.toggle() } } @@ -59,12 +58,6 @@ struct VideoContextMenuView: View { } } - var openVideoDetailsButton: some View { - Button("Open video details") { - navigationState.openVideoDetails(video) - } - } - var addToPlaylistButton: some View { Button("Add to playlist...") { videoIDToAddToPlaylist = video.id diff --git a/tvOS/TVNavigationView.swift b/tvOS/TVNavigationView.swift index 217e0d09..508ee532 100644 --- a/tvOS/TVNavigationView.swift +++ b/tvOS/TVNavigationView.swift @@ -4,6 +4,7 @@ import SwiftUI struct TVNavigationView: View { @EnvironmentObject private var navigationState @EnvironmentObject private var playbackState + @EnvironmentObject private var recents @EnvironmentObject private var searchState @State private var showingOptions = false @@ -47,31 +48,20 @@ struct TVNavigationView: View { } .fullScreenCover(isPresented: $showingOptions) { OptionsView() } .fullScreenCover(isPresented: $showingAddToPlaylist) { AddToPlaylistView() } - .fullScreenCover(isPresented: $navigationState.showingVideoDetails) { - if let video = navigationState.video { - VideoDetailsView(video) - } - } .fullScreenCover(isPresented: $navigationState.showingVideo) { if let video = navigationState.video { VideoPlayerView(video) .environmentObject(playbackState) } } - .fullScreenCover(isPresented: $navigationState.isChannelOpen, onDismiss: { - navigationState.closeChannel(presentedChannel) - }) { - if presentedChannel != nil { - ChannelVideosView(presentedChannel) + .fullScreenCover(isPresented: $navigationState.isChannelOpen) { + if let channel = recents.presentedChannel { + ChannelVideosView(channel) .background(.thickMaterial) } } .onPlayPauseCommand { showingOptions.toggle() } } - - fileprivate var presentedChannel: Channel! { - navigationState.openChannels.first - } } struct TVNavigationView_Previews: PreviewProvider { diff --git a/tvOS/VideoDetailsView.swift b/tvOS/VideoDetailsView.swift deleted file mode 100644 index 02300cc6..00000000 --- a/tvOS/VideoDetailsView.swift +++ /dev/null @@ -1,113 +0,0 @@ -import Defaults -import Siesta -import SwiftUI - -struct VideoDetailsView: View { - @Environment(\.dismiss) private var dismiss - - @EnvironmentObject private var navigationState - - @ObservedObject private var store = Store