mirror of
https://github.com/yattee/yattee.git
synced 2025-04-26 08:36:29 +00:00
Recently opened for sidebar navigation
This commit is contained in:
parent
8571822f23
commit
ee1cb924c9
@ -3,12 +3,11 @@ import SwiftUI
|
|||||||
|
|
||||||
final class NavigationState: ObservableObject {
|
final class NavigationState: ObservableObject {
|
||||||
enum TabSelection: Hashable {
|
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 tabSelection: TabSelection = .watchNow
|
||||||
|
|
||||||
@Published var showingVideoDetails = false
|
|
||||||
@Published var showingVideo = false
|
@Published var showingVideo = false
|
||||||
@Published var video: Video?
|
@Published var video: Video?
|
||||||
|
|
||||||
@ -20,56 +19,14 @@ final class NavigationState: ObservableObject {
|
|||||||
@Published var presentingUnsubscribeAlert = false
|
@Published var presentingUnsubscribeAlert = false
|
||||||
@Published var channelToUnsubscribe: Channel!
|
@Published var channelToUnsubscribe: Channel!
|
||||||
|
|
||||||
@Published var openChannels = Set<Channel>()
|
|
||||||
@Published var isChannelOpen = false
|
@Published var isChannelOpen = false
|
||||||
@Published var sidebarSectionChanged = 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) {
|
func playVideo(_ video: Video) {
|
||||||
self.video = video
|
self.video = video
|
||||||
showingVideo = true
|
showingVideo = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func showVideoDetailsIfNeeded() {
|
|
||||||
showingVideoDetails = returnToDetails
|
|
||||||
returnToDetails = false
|
|
||||||
}
|
|
||||||
|
|
||||||
var tabSelectionOptionalBinding: Binding<TabSelection?> {
|
var tabSelectionOptionalBinding: Binding<TabSelection?> {
|
||||||
Binding<TabSelection?>(
|
Binding<TabSelection?>(
|
||||||
get: {
|
get: {
|
||||||
|
116
Model/Recents.swift
Normal file
116
Model/Recents.swift
Normal 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -8,14 +8,11 @@ final class SearchState: ObservableObject {
|
|||||||
|
|
||||||
@Published var querySuggestions = Store<[String]>()
|
@Published var querySuggestions = Store<[String]>()
|
||||||
|
|
||||||
@Default(.searchQuery) private var queryText
|
|
||||||
|
|
||||||
private var previousResource: Resource?
|
private var previousResource: Resource?
|
||||||
private var resource: Resource!
|
private var resource: Resource!
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
let newQuery = query
|
let newQuery = query
|
||||||
newQuery.query = queryText
|
|
||||||
query = newQuery
|
query = newQuery
|
||||||
|
|
||||||
resource = InvidiousAPI.shared.search(newQuery)
|
resource = InvidiousAPI.shared.search(newQuery)
|
||||||
@ -53,7 +50,23 @@ final class SearchState: ObservableObject {
|
|||||||
previousResource?.removeObservers(ownedBy: store)
|
previousResource?.removeObservers(ownedBy: store)
|
||||||
previousResource = newResource
|
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 = newResource
|
||||||
resource.addObserver(store)
|
resource.addObserver(store)
|
||||||
|
@ -63,8 +63,8 @@
|
|||||||
3761AC0F26F0F9A600AA496F /* UnsubscribeAlertModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3761AC0E26F0F9A600AA496F /* UnsubscribeAlertModifier.swift */; };
|
3761AC0F26F0F9A600AA496F /* UnsubscribeAlertModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3761AC0E26F0F9A600AA496F /* UnsubscribeAlertModifier.swift */; };
|
||||||
3761AC1026F0F9A600AA496F /* 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 */; };
|
3761AC1126F0F9A600AA496F /* UnsubscribeAlertModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3761AC0E26F0F9A600AA496F /* UnsubscribeAlertModifier.swift */; };
|
||||||
3763495126DFF59D00B9A393 /* AppSidebarRecentlyOpened.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3763495026DFF59D00B9A393 /* AppSidebarRecentlyOpened.swift */; };
|
3763495126DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3763495026DFF59D00B9A393 /* AppSidebarRecents.swift */; };
|
||||||
3763495226DFF59D00B9A393 /* AppSidebarRecentlyOpened.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3763495026DFF59D00B9A393 /* AppSidebarRecentlyOpened.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 */; };
|
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 */; };
|
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 */; };
|
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 */; };
|
37B17DA0268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B17D9F268A1F25006AEE9B /* VideoContextMenuView.swift */; };
|
||||||
37B17DA1268A1F89006AEE9B /* 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 */; };
|
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 */; };
|
37B767DB2677C3CA0098BAA8 /* PlayerState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B767DA2677C3CA0098BAA8 /* PlayerState.swift */; };
|
||||||
37B767DC2677C3CA0098BAA8 /* 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 */; };
|
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 */; };
|
37BE0BDA26A214630092E2DB /* PlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BD926A214630092E2DB /* PlayerViewController.swift */; };
|
||||||
37BE0BDC26A2367F0092E2DB /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BDB26A2367F0092E2DB /* Player.swift */; };
|
37BE0BDC26A2367F0092E2DB /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BDB26A2367F0092E2DB /* Player.swift */; };
|
||||||
37BE0BE526A336910092E2DB /* OptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B76E95268747C900CE5671 /* OptionsView.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 */; };
|
37C7A1D5267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C7A1D4267BFD9D0010EAD6 /* SponsorBlockSegment.swift */; };
|
||||||
37C7A1D6267BFD9D0010EAD6 /* 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 */; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleAssetStream.swift; sourceTree = "<group>"; };
|
||||||
@ -432,7 +434,7 @@
|
|||||||
children = (
|
children = (
|
||||||
37BD07BA2698AB60003EBB87 /* AppSidebarNavigation.swift */,
|
37BD07BA2698AB60003EBB87 /* AppSidebarNavigation.swift */,
|
||||||
37BA794A26DC30EC002A0235 /* AppSidebarPlaylists.swift */,
|
37BA794A26DC30EC002A0235 /* AppSidebarPlaylists.swift */,
|
||||||
3763495026DFF59D00B9A393 /* AppSidebarRecentlyOpened.swift */,
|
3763495026DFF59D00B9A393 /* AppSidebarRecents.swift */,
|
||||||
37BA794626DC2E56002A0235 /* AppSidebarSubscriptions.swift */,
|
37BA794626DC2E56002A0235 /* AppSidebarSubscriptions.swift */,
|
||||||
37D4B0C32671614700C925CA /* AppTabNavigation.swift */,
|
37D4B0C32671614700C925CA /* AppTabNavigation.swift */,
|
||||||
37BD07B42698AA4D003EBB87 /* ContentView.swift */,
|
37BD07B42698AA4D003EBB87 /* ContentView.swift */,
|
||||||
@ -665,7 +667,6 @@
|
|||||||
371AAE2926CF143200901972 /* Options */,
|
371AAE2926CF143200901972 /* Options */,
|
||||||
373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */,
|
373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */,
|
||||||
37BAB54B269B39FD00E75ED1 /* TVNavigationView.swift */,
|
37BAB54B269B39FD00E75ED1 /* TVNavigationView.swift */,
|
||||||
37B17DA3268A285E006AEE9B /* VideoDetailsView.swift */,
|
|
||||||
37D4B15E267164AF00C925CA /* Assets.xcassets */,
|
37D4B15E267164AF00C925CA /* Assets.xcassets */,
|
||||||
37D4B1AE26729DEB00C925CA /* Info.plist */,
|
37D4B1AE26729DEB00C925CA /* Info.plist */,
|
||||||
);
|
);
|
||||||
@ -692,6 +693,7 @@
|
|||||||
376578882685471400D4EA09 /* Playlist.swift */,
|
376578882685471400D4EA09 /* Playlist.swift */,
|
||||||
37BA794226DBA973002A0235 /* Playlists.swift */,
|
37BA794226DBA973002A0235 /* Playlists.swift */,
|
||||||
37C7A1DB267CE9D90010EAD6 /* Profile.swift */,
|
37C7A1DB267CE9D90010EAD6 /* Profile.swift */,
|
||||||
|
37C194C626F6A9C8005D3B96 /* Recents.swift */,
|
||||||
373CFACA26966264003CB2C6 /* SearchQuery.swift */,
|
373CFACA26966264003CB2C6 /* SearchQuery.swift */,
|
||||||
3711403E26B206A6005B3555 /* SearchState.swift */,
|
3711403E26B206A6005B3555 /* SearchState.swift */,
|
||||||
37EAD86E267B9ED100D9E01B /* Segment.swift */,
|
37EAD86E267B9ED100D9E01B /* Segment.swift */,
|
||||||
@ -998,7 +1000,7 @@
|
|||||||
37CEE4BD2677B670005A1EFE /* SingleAssetStream.swift in Sources */,
|
37CEE4BD2677B670005A1EFE /* SingleAssetStream.swift in Sources */,
|
||||||
37BD07C82698B71C003EBB87 /* AppTabNavigation.swift in Sources */,
|
37BD07C82698B71C003EBB87 /* AppTabNavigation.swift in Sources */,
|
||||||
37EAD86B267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
|
37EAD86B267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
|
||||||
3763495126DFF59D00B9A393 /* AppSidebarRecentlyOpened.swift in Sources */,
|
3763495126DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */,
|
||||||
37BA793B26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
|
37BA793B26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
|
||||||
37BD07B52698AA4D003EBB87 /* ContentView.swift in Sources */,
|
37BD07B52698AA4D003EBB87 /* ContentView.swift in Sources */,
|
||||||
37152EEA26EFEB95004FB96D /* LazyView.swift in Sources */,
|
37152EEA26EFEB95004FB96D /* LazyView.swift in Sources */,
|
||||||
@ -1008,6 +1010,7 @@
|
|||||||
37BE0BD626A1D4A90092E2DB /* PlayerViewController.swift in Sources */,
|
37BE0BD626A1D4A90092E2DB /* PlayerViewController.swift in Sources */,
|
||||||
37BA793F26DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
|
37BA793F26DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
|
||||||
37754C9D26B7500000DBD602 /* VideosView.swift in Sources */,
|
37754C9D26B7500000DBD602 /* VideosView.swift in Sources */,
|
||||||
|
37C194C726F6A9C8005D3B96 /* Recents.swift in Sources */,
|
||||||
3711403F26B206A6005B3555 /* SearchState.swift in Sources */,
|
3711403F26B206A6005B3555 /* SearchState.swift in Sources */,
|
||||||
37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */,
|
37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */,
|
||||||
37BE0BD326A1D4780092E2DB /* Player.swift in Sources */,
|
37BE0BD326A1D4780092E2DB /* Player.swift in Sources */,
|
||||||
@ -1070,6 +1073,7 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
37C194C826F6A9C8005D3B96 /* Recents.swift in Sources */,
|
||||||
37BE0BDC26A2367F0092E2DB /* Player.swift in Sources */,
|
37BE0BDC26A2367F0092E2DB /* Player.swift in Sources */,
|
||||||
3788AC2C26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */,
|
3788AC2C26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */,
|
||||||
37CEE4BE2677B670005A1EFE /* SingleAssetStream.swift in Sources */,
|
37CEE4BE2677B670005A1EFE /* SingleAssetStream.swift in Sources */,
|
||||||
@ -1135,7 +1139,7 @@
|
|||||||
3748186B26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */,
|
3748186B26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */,
|
||||||
373CFADC269663F1003CB2C6 /* Thumbnail.swift in Sources */,
|
373CFADC269663F1003CB2C6 /* Thumbnail.swift in Sources */,
|
||||||
373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
|
373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
|
||||||
3763495226DFF59D00B9A393 /* AppSidebarRecentlyOpened.swift in Sources */,
|
3763495226DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */,
|
||||||
37BA794426DBA973002A0235 /* Playlists.swift in Sources */,
|
37BA794426DBA973002A0235 /* Playlists.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@ -1175,7 +1179,6 @@
|
|||||||
373CFAC926966188003CB2C6 /* SearchOptionsView.swift in Sources */,
|
373CFAC926966188003CB2C6 /* SearchOptionsView.swift in Sources */,
|
||||||
37A9965C26D6F8CA006E3224 /* VideosCellsHorizontal.swift in Sources */,
|
37A9965C26D6F8CA006E3224 /* VideosCellsHorizontal.swift in Sources */,
|
||||||
37BD07C92698FBDB003EBB87 /* ContentView.swift in Sources */,
|
37BD07C92698FBDB003EBB87 /* ContentView.swift in Sources */,
|
||||||
37B17DA6268A285E006AEE9B /* VideoDetailsView.swift in Sources */,
|
|
||||||
37141671267A8ACC006CA35D /* TrendingView.swift in Sources */,
|
37141671267A8ACC006CA35D /* TrendingView.swift in Sources */,
|
||||||
3788AC2926F6840700F6BAA9 /* WatchNowSection.swift in Sources */,
|
3788AC2926F6840700F6BAA9 /* WatchNowSection.swift in Sources */,
|
||||||
37BA794126DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
|
37BA794126DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
|
||||||
@ -1206,6 +1209,7 @@
|
|||||||
37B17DA0268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */,
|
37B17DA0268A1F89006AEE9B /* VideoContextMenuView.swift in Sources */,
|
||||||
37BE0BD726A1D4A90092E2DB /* PlayerViewController.swift in Sources */,
|
37BE0BD726A1D4A90092E2DB /* PlayerViewController.swift in Sources */,
|
||||||
37CEE4C32677B697005A1EFE /* Stream.swift in Sources */,
|
37CEE4C32677B697005A1EFE /* Stream.swift in Sources */,
|
||||||
|
37C194C926F6A9C8005D3B96 /* Recents.swift in Sources */,
|
||||||
37BE0BE526A336910092E2DB /* OptionsView.swift in Sources */,
|
37BE0BE526A336910092E2DB /* OptionsView.swift in Sources */,
|
||||||
37BA793D26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
|
37BA793D26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
|
||||||
3711404126B206A6005B3555 /* SearchState.swift in Sources */,
|
3711404126B206A6005B3555 /* SearchState.swift in Sources */,
|
||||||
|
@ -1,5 +1,21 @@
|
|||||||
import Defaults
|
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 {
|
enum ListingLayout: String, CaseIterable, Identifiable, Defaults.Serializable {
|
||||||
case list, cells
|
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")
|
|
||||||
}
|
|
||||||
|
@ -10,11 +10,7 @@ struct UnsubscribeAlertModifier: ViewModifier {
|
|||||||
.alert(unsubscribeAlertTitle, isPresented: $navigationState.presentingUnsubscribeAlert) {
|
.alert(unsubscribeAlertTitle, isPresented: $navigationState.presentingUnsubscribeAlert) {
|
||||||
if let channel = navigationState.channelToUnsubscribe {
|
if let channel = navigationState.channelToUnsubscribe {
|
||||||
Button("Unsubscribe", role: .destructive) {
|
Button("Unsubscribe", role: .destructive) {
|
||||||
subscriptions.unsubscribe(channel.id) {
|
subscriptions.unsubscribe(channel.id)
|
||||||
navigationState.openChannel(channel)
|
|
||||||
navigationState.tabSelection = .channel(channel.id)
|
|
||||||
navigationState.sidebarSectionChanged.toggle()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ struct AppSidebarNavigation: View {
|
|||||||
|
|
||||||
@EnvironmentObject<NavigationState> private var navigationState
|
@EnvironmentObject<NavigationState> private var navigationState
|
||||||
@EnvironmentObject<Playlists> private var playlists
|
@EnvironmentObject<Playlists> private var playlists
|
||||||
|
@EnvironmentObject<Recents> private var recents
|
||||||
@EnvironmentObject<SearchState> private var searchState
|
@EnvironmentObject<SearchState> private var searchState
|
||||||
@EnvironmentObject<Subscriptions> private var subscriptions
|
@EnvironmentObject<Subscriptions> private var subscriptions
|
||||||
|
|
||||||
@ -66,6 +67,8 @@ struct AppSidebarNavigation: View {
|
|||||||
query.query = self.searchQuery
|
query.query = self.searchQuery
|
||||||
}
|
}
|
||||||
|
|
||||||
|
recents.open(RecentItem(type: .query, identifier: self.searchQuery, title: self.searchQuery))
|
||||||
|
|
||||||
navigationState.tabSelection = .search
|
navigationState.tabSelection = .search
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -111,7 +114,7 @@ struct AppSidebarNavigation: View {
|
|||||||
return Group {
|
return Group {
|
||||||
mainNavigationLinks
|
mainNavigationLinks
|
||||||
|
|
||||||
AppSidebarRecentlyOpened(selection: selection)
|
AppSidebarRecents(selection: selection)
|
||||||
.id("recentlyOpened")
|
.id("recentlyOpened")
|
||||||
AppSidebarSubscriptions(selection: selection)
|
AppSidebarSubscriptions(selection: selection)
|
||||||
AppSidebarPlaylists(selection: selection)
|
AppSidebarPlaylists(selection: selection)
|
||||||
@ -130,7 +133,7 @@ struct AppSidebarNavigation: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
NavigationLink(destination: LazyView(SubscriptionsView()), tag: TabSelection.subscriptions, selection: selection) {
|
NavigationLink(destination: LazyView(SubscriptionsView()), tag: TabSelection.subscriptions, selection: selection) {
|
||||||
Label("Subscriptions", systemImage: "star.circle.fill")
|
Label("Subscriptions", systemImage: "star.circle")
|
||||||
.accessibility(label: Text("Subscriptions"))
|
.accessibility(label: Text("Subscriptions"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) }
|
|
||||||
}
|
|
||||||
}
|
|
91
Shared/Navigation/AppSidebarRecents.swift
Normal file
91
Shared/Navigation/AppSidebarRecents.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,8 @@ struct AppTabNavigation: View {
|
|||||||
@EnvironmentObject<NavigationState> private var navigationState
|
@EnvironmentObject<NavigationState> private var navigationState
|
||||||
@EnvironmentObject<SearchState> private var searchState
|
@EnvironmentObject<SearchState> private var searchState
|
||||||
|
|
||||||
|
@EnvironmentObject<Recents> private var recents
|
||||||
|
|
||||||
@State private var searchQuery = ""
|
@State private var searchQuery = ""
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@ -82,18 +84,17 @@ struct AppTabNavigation: View {
|
|||||||
.tag(TabSelection.search)
|
.tag(TabSelection.search)
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $navigationState.isChannelOpen, onDismiss: {
|
.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 {
|
NavigationView {
|
||||||
ChannelVideosView(presentedChannel)
|
ChannelVideosView(recents.presentedChannel!)
|
||||||
.environment(\.inNavigationView, true)
|
.environment(\.inNavigationView, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate var presentedChannel: Channel! {
|
|
||||||
navigationState.openChannels.first
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,10 @@ import SwiftUI
|
|||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
@StateObject private var navigationState = NavigationState()
|
@StateObject private var navigationState = NavigationState()
|
||||||
@StateObject private var playbackState = PlaybackState()
|
@StateObject private var playbackState = PlaybackState()
|
||||||
|
@StateObject private var playlists = Playlists()
|
||||||
|
@StateObject private var recents = Recents()
|
||||||
@StateObject private var searchState = SearchState()
|
@StateObject private var searchState = SearchState()
|
||||||
@StateObject private var subscriptions = Subscriptions()
|
@StateObject private var subscriptions = Subscriptions()
|
||||||
@StateObject private var playlists = Playlists()
|
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||||
@ -44,9 +45,10 @@ struct ContentView: View {
|
|||||||
#endif
|
#endif
|
||||||
.environmentObject(navigationState)
|
.environmentObject(navigationState)
|
||||||
.environmentObject(playbackState)
|
.environmentObject(playbackState)
|
||||||
|
.environmentObject(playlists)
|
||||||
|
.environmentObject(recents)
|
||||||
.environmentObject(searchState)
|
.environmentObject(searchState)
|
||||||
.environmentObject(subscriptions)
|
.environmentObject(subscriptions)
|
||||||
.environmentObject(playlists)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,8 +85,6 @@ struct VideoPlayerView: View {
|
|||||||
.onDisappear {
|
.onDisappear {
|
||||||
resource.removeObservers(ownedBy: store)
|
resource.removeObservers(ownedBy: store)
|
||||||
resource.invalidate()
|
resource.invalidate()
|
||||||
|
|
||||||
navigationState.showingVideoDetails = navigationState.returnToDetails
|
|
||||||
}
|
}
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.frame(maxWidth: 1000, minHeight: 700)
|
.frame(maxWidth: 1000, minHeight: 700)
|
||||||
|
@ -3,13 +3,18 @@ import Siesta
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct SearchView: View {
|
struct SearchView: View {
|
||||||
@Default(.searchQuery) private var queryText
|
|
||||||
@Default(.searchSortOrder) private var searchSortOrder
|
@Default(.searchSortOrder) private var searchSortOrder
|
||||||
@Default(.searchDate) private var searchDate
|
@Default(.searchDate) private var searchDate
|
||||||
@Default(.searchDuration) private var searchDuration
|
@Default(.searchDuration) private var searchDuration
|
||||||
|
|
||||||
@EnvironmentObject<SearchState> private var state
|
@EnvironmentObject<SearchState> private var state
|
||||||
|
|
||||||
|
private var query: SearchQuery?
|
||||||
|
|
||||||
|
init(_ query: SearchQuery? = nil) {
|
||||||
|
self.query = query
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
VideosView(videos: state.store.collection)
|
VideosView(videos: state.store.collection)
|
||||||
@ -27,11 +32,8 @@ struct SearchView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
state.changeQuery { query in
|
if query != nil {
|
||||||
query.query = queryText
|
state.resetQuery(query!)
|
||||||
query.sortBy = searchSortOrder
|
|
||||||
query.date = searchDate
|
|
||||||
query.duration = searchDuration
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: state.query.query) { queryText in
|
.onChange(of: state.query.query) { queryText in
|
||||||
|
@ -3,6 +3,7 @@ import SwiftUI
|
|||||||
|
|
||||||
struct VideoContextMenuView: View {
|
struct VideoContextMenuView: View {
|
||||||
@EnvironmentObject<NavigationState> private var navigationState
|
@EnvironmentObject<NavigationState> private var navigationState
|
||||||
|
@EnvironmentObject<Recents> private var recents
|
||||||
@EnvironmentObject<Subscriptions> private var subscriptions
|
@EnvironmentObject<Subscriptions> private var subscriptions
|
||||||
|
|
||||||
let video: Video
|
let video: Video
|
||||||
@ -14,15 +15,11 @@ struct VideoContextMenuView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Section {
|
Section {
|
||||||
if navigationState.showOpenChannel(video.channel.id) {
|
|
||||||
openChannelButton
|
openChannelButton
|
||||||
}
|
|
||||||
|
|
||||||
subscriptionButton
|
subscriptionButton
|
||||||
.opacity(subscribed ? 1 : 1)
|
.opacity(subscribed ? 1 : 1)
|
||||||
|
|
||||||
openVideoDetailsButton
|
|
||||||
|
|
||||||
if navigationState.tabSelection == .playlists {
|
if navigationState.tabSelection == .playlists {
|
||||||
removeFromPlaylistButton
|
removeFromPlaylistButton
|
||||||
} else {
|
} else {
|
||||||
@ -33,8 +30,10 @@ struct VideoContextMenuView: View {
|
|||||||
|
|
||||||
var openChannelButton: some View {
|
var openChannelButton: some View {
|
||||||
Button("\(video.author) Channel") {
|
Button("\(video.author) Channel") {
|
||||||
navigationState.openChannel(video.channel)
|
let recent = RecentItem(from: video.channel)
|
||||||
navigationState.tabSelection = .channel(video.channel.id)
|
recents.open(recent)
|
||||||
|
navigationState.tabSelection = .recentlyOpened(recent.tag)
|
||||||
|
navigationState.isChannelOpen = true
|
||||||
navigationState.sidebarSectionChanged.toggle()
|
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 {
|
var addToPlaylistButton: some View {
|
||||||
Button("Add to playlist...") {
|
Button("Add to playlist...") {
|
||||||
videoIDToAddToPlaylist = video.id
|
videoIDToAddToPlaylist = video.id
|
||||||
|
@ -4,6 +4,7 @@ import SwiftUI
|
|||||||
struct TVNavigationView: View {
|
struct TVNavigationView: View {
|
||||||
@EnvironmentObject<NavigationState> private var navigationState
|
@EnvironmentObject<NavigationState> private var navigationState
|
||||||
@EnvironmentObject<PlaybackState> private var playbackState
|
@EnvironmentObject<PlaybackState> private var playbackState
|
||||||
|
@EnvironmentObject<Recents> private var recents
|
||||||
@EnvironmentObject<SearchState> private var searchState
|
@EnvironmentObject<SearchState> private var searchState
|
||||||
|
|
||||||
@State private var showingOptions = false
|
@State private var showingOptions = false
|
||||||
@ -47,31 +48,20 @@ struct TVNavigationView: View {
|
|||||||
}
|
}
|
||||||
.fullScreenCover(isPresented: $showingOptions) { OptionsView() }
|
.fullScreenCover(isPresented: $showingOptions) { OptionsView() }
|
||||||
.fullScreenCover(isPresented: $showingAddToPlaylist) { AddToPlaylistView() }
|
.fullScreenCover(isPresented: $showingAddToPlaylist) { AddToPlaylistView() }
|
||||||
.fullScreenCover(isPresented: $navigationState.showingVideoDetails) {
|
|
||||||
if let video = navigationState.video {
|
|
||||||
VideoDetailsView(video)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.fullScreenCover(isPresented: $navigationState.showingVideo) {
|
.fullScreenCover(isPresented: $navigationState.showingVideo) {
|
||||||
if let video = navigationState.video {
|
if let video = navigationState.video {
|
||||||
VideoPlayerView(video)
|
VideoPlayerView(video)
|
||||||
.environmentObject(playbackState)
|
.environmentObject(playbackState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.fullScreenCover(isPresented: $navigationState.isChannelOpen, onDismiss: {
|
.fullScreenCover(isPresented: $navigationState.isChannelOpen) {
|
||||||
navigationState.closeChannel(presentedChannel)
|
if let channel = recents.presentedChannel {
|
||||||
}) {
|
ChannelVideosView(channel)
|
||||||
if presentedChannel != nil {
|
|
||||||
ChannelVideosView(presentedChannel)
|
|
||||||
.background(.thickMaterial)
|
.background(.thickMaterial)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onPlayPauseCommand { showingOptions.toggle() }
|
.onPlayPauseCommand { showingOptions.toggle() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate var presentedChannel: Channel! {
|
|
||||||
navigationState.openChannels.first
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TVNavigationView_Previews: PreviewProvider {
|
struct TVNavigationView_Previews: PreviewProvider {
|
||||||
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user