Recently opened for sidebar navigation

This commit is contained in:
Arkadiusz Fal 2021-09-19 13:06:54 +02:00
parent 8571822f23
commit ee1cb924c9
16 changed files with 291 additions and 291 deletions

View File

@ -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<Channel>()
@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<TabSelection?> {
Binding<TabSelection?>(
get: {

116
Model/Recents.swift Normal file
View File

@ -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
)
}
}

View File

@ -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)

View File

@ -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 = "<group>"; };
3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnvironmentValues.swift; sourceTree = "<group>"; };
3761AC0E26F0F9A600AA496F /* UnsubscribeAlertModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnsubscribeAlertModifier.swift; sourceTree = "<group>"; };
3763495026DFF59D00B9A393 /* AppSidebarRecentlyOpened.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarRecentlyOpened.swift; sourceTree = "<group>"; };
3763495026DFF59D00B9A393 /* AppSidebarRecents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSidebarRecents.swift; sourceTree = "<group>"; };
376578842685429C00D4EA09 /* CaseIterable+Next.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CaseIterable+Next.swift"; sourceTree = "<group>"; };
376578882685471400D4EA09 /* Playlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Playlist.swift; sourceTree = "<group>"; };
376578902685490700D4EA09 /* PlaylistsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistsView.swift; sourceTree = "<group>"; };
@ -306,7 +308,6 @@
37AAF29926740A01007FC770 /* VideosListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideosListView.swift; sourceTree = "<group>"; };
37AAF29F26741C97007FC770 /* SubscriptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsView.swift; sourceTree = "<group>"; };
37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoContextMenuView.swift; sourceTree = "<group>"; };
37B17DA3268A285E006AEE9B /* VideoDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDetailsView.swift; sourceTree = "<group>"; };
37B767DA2677C3CA0098BAA8 /* PlayerState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerState.swift; sourceTree = "<group>"; };
37B76E95268747C900CE5671 /* OptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionsView.swift; sourceTree = "<group>"; };
37B81AF826D2C9A700675966 /* VideoPlayerSizeModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerSizeModifier.swift; sourceTree = "<group>"; };
@ -331,6 +332,7 @@
37BE0BD526A1D4A90092E2DB /* PlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewController.swift; sourceTree = "<group>"; };
37BE0BD926A214630092E2DB /* PlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewController.swift; sourceTree = "<group>"; };
37BE0BDB26A2367F0092E2DB /* Player.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = "<group>"; };
37C194C626F6A9C8005D3B96 /* Recents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Recents.swift; sourceTree = "<group>"; };
37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockSegment.swift; sourceTree = "<group>"; };
37C7A1DB267CE9D90010EAD6 /* Profile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profile.swift; sourceTree = "<group>"; };
37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleAssetStream.swift; sourceTree = "<group>"; };
@ -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 */,

View File

@ -1,5 +1,21 @@
import Defaults
extension Defaults.Keys {
#if os(tvOS)
static let layout = Key<ListingLayout>("listingLayout", default: .cells)
#endif
static let searchSortOrder = Key<SearchQuery.SortOrder>("searchSortOrder", default: .relevance)
static let searchDate = Key<SearchQuery.Date?>("searchDate")
static let searchDuration = Key<SearchQuery.Duration?>("searchDuration")
static let selectedPlaylistID = Key<String?>("selectedPlaylistID")
static let showingAddToPlaylist = Key<Bool>("showingAddToPlaylist", default: false)
static let videoIDToAddToPlaylist = Key<String?>("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>("listingLayout", default: .cells)
#endif
static let searchQuery = Key<String>("searchQuery", default: "")
static let searchSortOrder = Key<SearchQuery.SortOrder>("searchSortOrder", default: .relevance)
static let searchDate = Key<SearchQuery.Date?>("searchDate")
static let searchDuration = Key<SearchQuery.Duration?>("searchDuration")
static let selectedPlaylistID = Key<String?>("selectedPlaylistID")
static let showingAddToPlaylist = Key<Bool>("showingAddToPlaylist", default: false)
static let videoIDToAddToPlaylist = Key<String?>("videoIDToAddToPlaylist")
}

View File

@ -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)
}
}
}

View File

@ -14,6 +14,7 @@ struct AppSidebarNavigation: View {
@EnvironmentObject<NavigationState> private var navigationState
@EnvironmentObject<Playlists> private var playlists
@EnvironmentObject<Recents> private var recents
@EnvironmentObject<SearchState> private var searchState
@EnvironmentObject<Subscriptions> 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"))
}

View File

@ -1,54 +0,0 @@
import SwiftUI
struct AppSidebarRecentlyOpened: View {
@Binding var selection: TabSelection?
@EnvironmentObject<NavigationState> private var navigationState
@EnvironmentObject<Subscriptions> 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) }
}
}

View File

@ -0,0 +1,91 @@
import Defaults
import SwiftUI
struct AppSidebarRecents: View {
@Binding var selection: TabSelection?
@EnvironmentObject<NavigationState> private var navigationState
@EnvironmentObject<Recents> 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<DestinationContent: View>: View {
@EnvironmentObject<Recents> private var recents
var recent: RecentItem
@Binding var selection: TabSelection?
var systemImage: String?
let destination: DestinationContent
init(
recent: RecentItem,
selection: Binding<TabSelection?>,
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)
}
}

View File

@ -5,6 +5,8 @@ struct AppTabNavigation: View {
@EnvironmentObject<NavigationState> private var navigationState
@EnvironmentObject<SearchState> private var searchState
@EnvironmentObject<Recents> 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
}
}

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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<SearchState> 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

View File

@ -3,6 +3,7 @@ import SwiftUI
struct VideoContextMenuView: View {
@EnvironmentObject<NavigationState> private var navigationState
@EnvironmentObject<Recents> private var recents
@EnvironmentObject<Subscriptions> 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

View File

@ -4,6 +4,7 @@ import SwiftUI
struct TVNavigationView: View {
@EnvironmentObject<NavigationState> private var navigationState
@EnvironmentObject<PlaybackState> private var playbackState
@EnvironmentObject<Recents> private var recents
@EnvironmentObject<SearchState> 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 {

View File

@ -1,113 +0,0 @@
import Defaults
import Siesta
import SwiftUI
struct VideoDetailsView: View {
@Environment(\.dismiss) private var dismiss
@EnvironmentObject<NavigationState> private var navigationState
@ObservedObject private var store = Store<Video>()
@State private var playVideoLinkActive = false
var resource: Resource {
InvidiousAPI.shared.video(video.id)
}
var video: Video
init(_ video: Video) {
self.video = video
resource.addObserver(store)
}
var body: some View {
NavigationView {
HStack {
Spacer()
VStack {
Spacer()
ScrollView(.vertical, showsIndicators: false) {
if let video = store.item {
VStack(alignment: .center) {
ZStack(alignment: .bottom) {
Group {
if let url = video.thumbnailURL(quality: .maxres) {
AsyncImage(url: url) { image in
image
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 1600, height: 800)
} placeholder: {
ProgressView()
}
}
}
.frame(width: 1600, height: 800)
VStack(alignment: .leading) {
Text(video.title)
.font(.system(size: 40))
HStack {
playVideoButton
openChannelButton
}
}
.padding(40)
.frame(width: 1600, alignment: .leading)
.background(.thinMaterial)
}
.mask(RoundedRectangle(cornerRadius: 20))
VStack {
Text(video.description)
.lineLimit(nil)
.focusable()
}.frame(width: 1600, alignment: .leading)
}
}
}
Spacer()
}
Spacer()
}
}
.background(.thinMaterial)
.onAppear {
resource.loadIfNeeded()
}
.edgesIgnoringSafeArea(.all)
}
var playVideoButton: some View {
Button(action: {
navigationState.returnToDetails = true
playVideoLinkActive = true
}) {
HStack(spacing: 8) {
Image(systemName: "play.rectangle.fill")
Text("Play")
}
}
.background(NavigationLink(destination: VideoPlayerView(video), isActive: $playVideoLinkActive) { EmptyView() }.hidden())
}
var openChannelButton: some View {
let channel = video.channel
return Button("Open \(channel.name) channel") {
navigationState.openChannel(channel)
navigationState.tabSelection = .channel(channel.id)
navigationState.returnToDetails = true
dismiss()
}
}
}