diff --git a/Model/Accounts/InstancesModel.swift b/Model/Accounts/InstancesModel.swift index 39bc1515..50821afa 100644 --- a/Model/Accounts/InstancesModel.swift +++ b/Model/Accounts/InstancesModel.swift @@ -2,7 +2,7 @@ import Defaults import Foundation final class InstancesModel: ObservableObject { - var all: [Instance] { + static var all: [Instance] { Defaults[.instances] } diff --git a/Model/MenuModel.swift b/Model/MenuModel.swift new file mode 100644 index 00000000..2945d24c --- /dev/null +++ b/Model/MenuModel.swift @@ -0,0 +1,18 @@ +import Combine +import Foundation + +final class MenuModel: ObservableObject { + @Published var accounts: AccountsModel? { didSet { registerChildModel(accounts) } } + @Published var navigation: NavigationModel? { didSet { registerChildModel(navigation) } } + @Published var player: PlayerModel? { didSet { registerChildModel(player) } } + + private var cancellables = [AnyCancellable]() + + func registerChildModel(_ model: T?) { + guard !model.isNil else { + return + } + + cancellables.append(model!.objectWillChange.sink { [weak self] _ in self?.objectWillChange.send() }) + } +} diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index 984ddc3f..cd256325 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -43,7 +43,6 @@ final class PlayerModel: ObservableObject { @Published var restoredSegments = [Segment]() var accounts: AccountsModel - var instances: InstancesModel var composition = AVMutableComposition() @@ -67,9 +66,8 @@ final class PlayerModel: ObservableObject { #endif }} - init(accounts: AccountsModel? = nil, instances: InstancesModel? = nil) { + init(accounts: AccountsModel? = nil, instances _: InstancesModel? = nil) { self.accounts = accounts ?? AccountsModel() - self.instances = instances ?? InstancesModel() addItemDidPlayToEndTimeObserver() addFrequentTimeObserver() @@ -81,6 +79,10 @@ final class PlayerModel: ObservableObject { presentingPlayer = true } + func togglePlayer() { + presentingPlayer.toggle() + } + var isPlaying: Bool { player.timeControlStatus == .playing } diff --git a/Model/Player/PlayerStreams.swift b/Model/Player/PlayerStreams.swift index dc597462..13acf6c9 100644 --- a/Model/Player/PlayerStreams.swift +++ b/Model/Player/PlayerStreams.swift @@ -22,7 +22,7 @@ extension PlayerModel { availableStreams = [] var instancesWithLoadedStreams = [Instance]() - instances.all.forEach { instance in + InstancesModel.all.forEach { instance in fetchStreams(instance.anonymous.video(video.videoID), instance: instance, video: video) { _ in self.completeIfAllInstancesLoaded( instance: instance, @@ -59,7 +59,7 @@ extension PlayerModel { instancesWithLoadedStreams.append(instance) rebuildTVMenu() - if instances.all.count == instancesWithLoadedStreams.count { + if InstancesModel.all.count == instancesWithLoadedStreams.count { completionHandler(streams.sorted { $0.kind < $1.kind }) } } diff --git a/Shared/MenuCommands.swift b/Shared/MenuCommands.swift new file mode 100644 index 00000000..9ff90678 --- /dev/null +++ b/Shared/MenuCommands.swift @@ -0,0 +1,63 @@ +import Foundation +import SwiftUI + +struct MenuCommands: Commands { + @Binding var model: MenuModel + + var body: some Commands { + navigationMenu + playbackMenu + } + + private var navigationMenu: some Commands { + CommandMenu("Navigation") { + Button("Favorites") { + model.navigation?.tabSelection = .favorites + } + .keyboardShortcut("1") + + Button("Subscriptions") { + model.navigation?.tabSelection = .subscriptions + } + .disabled(!(model.accounts?.app.supportsSubscriptions ?? true)) + .keyboardShortcut("2") + + Button("Popular") { + model.navigation?.tabSelection = .popular + } + .disabled(!(model.accounts?.app.supportsPopular ?? true)) + .keyboardShortcut("3") + + Button("Trending") { + model.navigation?.tabSelection = .trending + } + .keyboardShortcut("4") + + Button("Search") { + model.navigation?.tabSelection = .search + } + .keyboardShortcut("f") + } + } + + private var playbackMenu: some Commands { + CommandMenu("Playback") { + Button((model.player?.isPlaying ?? true) ? "Pause" : "Play") { + model.player?.togglePlay() + } + .disabled(model.player?.currentItem.isNil ?? true) + .keyboardShortcut("p") + + Button("Play Next") { + model.player?.advanceToNextItem() + } + .disabled(model.player?.queue.isEmpty ?? true) + .keyboardShortcut("s") + + Button((model.player?.presentingPlayer ?? true) ? "Hide Player" : "Show Player") { + model.player?.togglePlayer() + } + .keyboardShortcut("o") + } + } +} diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index f6964be2..ea3749df 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -17,6 +17,8 @@ struct ContentView: View { @StateObject private var subscriptions = SubscriptionsModel() @StateObject private var thumbnailsModel = ThumbnailsModel() + @EnvironmentObject private var menu + #if os(iOS) @Environment(\.horizontalSizeClass) private var horizontalSizeClass #endif @@ -106,7 +108,7 @@ struct ContentView: View { if let account = accounts.lastUsed ?? instances.lastUsed?.anonymousAccount ?? - instances.all.first?.anonymousAccount + InstancesModel.all.first?.anonymousAccount { accounts.setCurrent(account) } @@ -120,6 +122,10 @@ struct ContentView: View { search.accounts = accounts subscriptions.accounts = accounts + menu.accounts = accounts + menu.navigation = navigation + menu.player = player + if !accounts.current.isNil { player.loadHistoryDetails() } diff --git a/Shared/Player/PlaybackBar.swift b/Shared/Player/PlaybackBar.swift index 3ef92575..6bd43d0a 100644 --- a/Shared/Player/PlaybackBar.swift +++ b/Shared/Player/PlaybackBar.swift @@ -6,7 +6,6 @@ struct PlaybackBar: View { @Environment(\.dismiss) private var dismiss @Environment(\.inNavigationView) private var inNavigationView - @EnvironmentObject private var instances @EnvironmentObject private var player var body: some View { @@ -154,7 +153,7 @@ struct PlaybackBar: View { private var streamControl: some View { #if os(macOS) Picker("", selection: $player.streamSelection) { - ForEach(instances.all) { instance in + ForEach(InstancesModel.all) { instance in let instanceStreams = availableStreamsForInstance(instance) if !instanceStreams.values.isEmpty { let kinds = Array(instanceStreams.keys).sorted { $0 < $1 } @@ -175,7 +174,7 @@ struct PlaybackBar: View { } #else Menu { - ForEach(instances.all) { instance in + ForEach(InstancesModel.all) { instance in let instanceStreams = availableStreamsForInstance(instance) if !instanceStreams.values.isEmpty { let kinds = Array(instanceStreams.keys).sorted { $0 < $1 } diff --git a/Shared/Views/PlayerControlsView.swift b/Shared/Views/PlayerControlsView.swift index 2cc40a79..d25ffbac 100644 --- a/Shared/Views/PlayerControlsView.swift +++ b/Shared/Views/PlayerControlsView.swift @@ -48,9 +48,6 @@ struct PlayerControlsView: View { .contentShape(Rectangle()) } .padding(.vertical, 20) - #if !os(tvOS) - .keyboardShortcut("o") - #endif ZStack(alignment: .bottom) { HStack { @@ -73,10 +70,6 @@ struct PlayerControlsView: View { .font(.system(size: 30)) .frame(minWidth: 30) - #if !os(tvOS) - .keyboardShortcut("p") - #endif - Button(action: { model.advanceToNextItem() }) { Label("Next", systemImage: "forward.fill") } diff --git a/Shared/YatteeApp.swift b/Shared/YatteeApp.swift index 44be5744..493fc4e8 100644 --- a/Shared/YatteeApp.swift +++ b/Shared/YatteeApp.swift @@ -7,15 +7,19 @@ struct YatteeApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate #endif + @StateObject private var menu = MenuModel() + var body: some Scene { WindowGroup { ContentView() + .environmentObject(menu) } #if !os(tvOS) .handlesExternalEvents(matching: Set(["*"])) .commands { SidebarCommands() CommandGroup(replacing: .newItem, addition: {}) + MenuCommands(model: Binding(get: { menu }, set: { _ in })) } #endif diff --git a/Yattee.xcodeproj/project.pbxproj b/Yattee.xcodeproj/project.pbxproj index e832038f..455532fa 100644 --- a/Yattee.xcodeproj/project.pbxproj +++ b/Yattee.xcodeproj/project.pbxproj @@ -77,6 +77,8 @@ 371F2F1A269B43D300E4A7AB /* NavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationModel.swift */; }; 371F2F1B269B43D300E4A7AB /* NavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationModel.swift */; }; 371F2F1C269B43D300E4A7AB /* NavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationModel.swift */; }; + 3729037E2739E47400EA99F6 /* MenuCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3729037D2739E47400EA99F6 /* MenuCommands.swift */; }; + 3729037F2739E47400EA99F6 /* MenuCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3729037D2739E47400EA99F6 /* MenuCommands.swift */; }; 372915E42687E33E00F5A35B /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 372915E32687E33E00F5A35B /* Defaults */; }; 372915E62687E3B900F5A35B /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; }; 372915E72687E3B900F5A35B /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; }; @@ -467,6 +469,9 @@ 37EAD86F267B9ED100D9E01B /* Segment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EAD86E267B9ED100D9E01B /* Segment.swift */; }; 37EAD870267B9ED100D9E01B /* Segment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EAD86E267B9ED100D9E01B /* Segment.swift */; }; 37EAD871267B9ED100D9E01B /* Segment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EAD86E267B9ED100D9E01B /* Segment.swift */; }; + 37EF5C222739D37B00B03725 /* MenuModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF5C212739D37B00B03725 /* MenuModel.swift */; }; + 37EF5C232739D37B00B03725 /* MenuModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF5C212739D37B00B03725 /* MenuModel.swift */; }; + 37EF5C242739D37B00B03725 /* MenuModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF5C212739D37B00B03725 /* MenuModel.swift */; }; 37F49BA326CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */; }; 37F49BA426CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */; }; 37F49BA526CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */; }; @@ -579,6 +584,7 @@ 37169AA12729D98A0011DE61 /* InstancesBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesBridge.swift; sourceTree = ""; }; 37169AA52729E2CC0011DE61 /* AccountsBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsBridge.swift; sourceTree = ""; }; 371F2F19269B43D300E4A7AB /* NavigationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModel.swift; sourceTree = ""; }; + 3729037D2739E47400EA99F6 /* MenuCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuCommands.swift; sourceTree = ""; }; 372915E52687E3B900F5A35B /* Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Defaults.swift; sourceTree = ""; }; 3730D89F2712E2B70020ED53 /* NowPlayingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingView.swift; sourceTree = ""; }; 373197D82732015300EF734F /* RelatedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelatedView.swift; sourceTree = ""; }; @@ -719,6 +725,7 @@ 37E70926271CDDAE00D34DDE /* OpenSettingsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenSettingsButton.swift; sourceTree = ""; }; 37EAD86A267B9C5600D9E01B /* SponsorBlockAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockAPI.swift; sourceTree = ""; }; 37EAD86E267B9ED100D9E01B /* Segment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Segment.swift; sourceTree = ""; }; + 37EF5C212739D37B00B03725 /* MenuModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuModel.swift; sourceTree = ""; }; 37F49BA226CAA59B00304AC0 /* Playlist+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Playlist+Fixtures.swift"; sourceTree = ""; }; 37F4AE7126828F0900BD60EA /* VerticalCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalCells.swift; sourceTree = ""; }; 37F64FE326FE70A60081B69E /* RedrawOnModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedrawOnModifier.swift; sourceTree = ""; }; @@ -1117,6 +1124,7 @@ 375168D52700FAFF008F96A6 /* Debounce.swift */, 372915E52687E3B900F5A35B /* Defaults.swift */, 3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */, + 3729037D2739E47400EA99F6 /* MenuCommands.swift */, 3700155E271B12DD0049C794 /* SiestaConfiguration.swift */, 37FFC43F272734C3009FFD26 /* Throttle.swift */, 37CB12782724C76D00213B45 /* VideoURLParser.swift */, @@ -1196,6 +1204,7 @@ 37141672267A8E10006CA35D /* Country.swift */, 37599F2F272B42810087F250 /* FavoriteItem.swift */, 37599F33272B44000087F250 /* FavoritesModel.swift */, + 37EF5C212739D37B00B03725 /* MenuModel.swift */, 371F2F19269B43D300E4A7AB /* NavigationModel.swift */, 376578882685471400D4EA09 /* Playlist.swift */, 37BA794226DBA973002A0235 /* PlaylistsModel.swift */, @@ -1735,6 +1744,7 @@ 37C194C726F6A9C8005D3B96 /* RecentsModel.swift in Sources */, 37484C1926FC837400287258 /* PlaybackSettings.swift in Sources */, 3711403F26B206A6005B3555 /* SearchModel.swift in Sources */, + 3729037E2739E47400EA99F6 /* MenuCommands.swift in Sources */, 37F64FE426FE70A60081B69E /* RedrawOnModifier.swift in Sources */, 376A33E02720CAD6000C1D6B /* VideosApp.swift in Sources */, 37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */, @@ -1811,6 +1821,7 @@ 3748186E26A769D60084E870 /* DetailBadge.swift in Sources */, 376BE50B27349108009AD608 /* BrowsingSettings.swift in Sources */, 37AAF2A026741C97007FC770 /* SubscriptionsView.swift in Sources */, + 37EF5C222739D37B00B03725 /* MenuModel.swift in Sources */, 37599F30272B42810087F250 /* FavoriteItem.swift in Sources */, 373197D92732015300EF734F /* RelatedView.swift in Sources */, 373CFAEB26975CBF003CB2C6 /* PlaylistFormView.swift in Sources */, @@ -1867,6 +1878,7 @@ 3705B183267B4E4900704544 /* TrendingCategory.swift in Sources */, 37FB285F272225E800A57617 /* ContentItemView.swift in Sources */, 37FD43DC270470B70073EE42 /* InstancesSettings.swift in Sources */, + 3729037F2739E47400EA99F6 /* MenuCommands.swift in Sources */, 37C0698327260B2100F7F6CB /* ThumbnailsModel.swift in Sources */, 376B2E0826F920D600B1D64D /* SignInRequiredView.swift in Sources */, 37CC3F4D270CFE1700608308 /* PlayerQueueView.swift in Sources */, @@ -1943,6 +1955,7 @@ 3700155C271B0D4D0049C794 /* PipedAPI.swift in Sources */, 376BE50C27349108009AD608 /* BrowsingSettings.swift in Sources */, 37D4B19826717E1500C925CA /* Video.swift in Sources */, + 37EF5C232739D37B00B03725 /* MenuModel.swift in Sources */, 37599F31272B42810087F250 /* FavoriteItem.swift in Sources */, 3730F75A2733481E00F385FC /* RelatedView.swift in Sources */, 375168D72700FDB8008F96A6 /* Debounce.swift in Sources */, @@ -2096,6 +2109,7 @@ 376A33E62720CB35000C1D6B /* Account.swift in Sources */, 37599F3A272B4D740087F250 /* FavoriteButton.swift in Sources */, 37484C1F26FC83A400287258 /* InstancesSettings.swift in Sources */, + 37EF5C242739D37B00B03725 /* MenuModel.swift in Sources */, 37C7A1D7267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */, 376578932685490700D4EA09 /* PlaylistsView.swift in Sources */, 37CC3F47270CE30600608308 /* PlayerQueueItem.swift in Sources */,