From b50d915d8ed5ce9c0a5beee052d2526c51c692c9 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Mon, 25 Oct 2021 23:29:06 +0200 Subject: [PATCH] Add to playlist from video player, state fixes --- Model/Player/PlayerModel.swift | 30 +++++++++++------- Model/Player/PlayerQueue.swift | 22 +++++++++++-- Model/PlaylistsModel.swift | 13 ++++++-- Pearvidious.xcodeproj/project.pbxproj | 8 +++++ Shared/Defaults.swift | 1 + Shared/Navigation/ContentView.swift | 2 ++ Shared/Player/VideoDetails.swift | 18 ++++++++++- Shared/Playlists/AddToPlaylistView.swift | 20 ++++++------ Shared/Throttle.swift | 40 ++++++++++++++++++++++++ macOS/PlayerViewController.swift | 2 +- 10 files changed, 129 insertions(+), 27 deletions(-) create mode 100644 Shared/Throttle.swift diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index 527b3118..1e7a1972 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -50,6 +50,8 @@ final class PlayerModel: ObservableObject { private var statusObservation: NSKeyValueObservation? + private var timeObserverThrottle = Throttle(interval: 2) + init(accounts: AccountsModel? = nil, instances: InstancesModel? = nil) { self.accounts = accounts ?? AccountsModel() self.instances = instances ?? InstancesModel() @@ -365,8 +367,6 @@ final class PlayerModel: ObservableObject { return } - self.currentRate = self.player.rate - guard !self.currentItem.isNil else { return } @@ -390,7 +390,9 @@ final class PlayerModel: ObservableObject { return } - self.updateCurrentItemIntervals() + self.timeObserverThrottle.execute { + self.updateCurrentItemIntervals() + } } } @@ -402,15 +404,21 @@ final class PlayerModel: ObservableObject { return } - #if os(macOS) - if player.timeControlStatus == .playing { - ScreenSaverManager.shared.disable(reason: "Yattee is playing video") - } else { - ScreenSaverManager.shared.enable() - } - #endif + if player.timeControlStatus != .waitingToPlayAtSpecifiedRate { + self.objectWillChange.send() + } - self.updateCurrentItemIntervals() + self.timeObserverThrottle.execute { + #if os(macOS) + if player.timeControlStatus == .playing { + ScreenSaverManager.shared.disable(reason: "Yattee is playing video") + } else { + ScreenSaverManager.shared.enable() + } + #endif + + self.updateCurrentItemIntervals() + } } } diff --git a/Model/Player/PlayerQueue.swift b/Model/Player/PlayerQueue.swift index 1dc2c61d..05df0aec 100644 --- a/Model/Player/PlayerQueue.swift +++ b/Model/Player/PlayerQueue.swift @@ -191,7 +191,25 @@ extension PlayerModel { } } - history = [Defaults[.lastPlayed]].compactMap { $0 } + Defaults[.history] + var savedHistory = Defaults[.history] + + if let lastPlayed = Defaults[.lastPlayed] { + if let index = savedHistory.firstIndex(where: { $0.videoID == lastPlayed.videoID }) { + var updatedLastPlayed = savedHistory[index] + + updatedLastPlayed.playbackTime = lastPlayed.playbackTime + updatedLastPlayed.videoDuration = lastPlayed.videoDuration + + savedHistory.remove(at: index) + savedHistory.insert(updatedLastPlayed, at: 0) + } else { + savedHistory.insert(lastPlayed, at: 0) + } + + Defaults[.lastPlayed] = nil + } + + history = savedHistory history.forEach { item in accounts.api.loadDetails(item) { newItem in if let index = self.history.firstIndex(where: { $0.id == item.id }) { @@ -199,7 +217,5 @@ extension PlayerModel { } } } - - Defaults[.lastPlayed] = nil } } diff --git a/Model/PlaylistsModel.swift b/Model/PlaylistsModel.swift index 4d1a34d1..763012cc 100644 --- a/Model/PlaylistsModel.swift +++ b/Model/PlaylistsModel.swift @@ -15,8 +15,12 @@ final class PlaylistsModel: ObservableObject { playlists.sorted { $0.title.lowercased() < $1.title.lowercased() } } - func find(id: Playlist.ID) -> Playlist? { - playlists.first { $0.id == id } + func find(id: Playlist.ID?) -> Playlist? { + if id.isNil { + return nil + } + + return playlists.first { $0.id == id! } } var isEmpty: Bool { @@ -26,6 +30,11 @@ final class PlaylistsModel: ObservableObject { func load(force: Bool = false, onSuccess: @escaping () -> Void = {}) { let request = force ? resource?.load() : resource?.loadIfNeeded() + guard !request.isNil else { + onSuccess() + return + } + request? .onSuccess { resource in if let playlists: [Playlist] = resource.typedContent() { diff --git a/Pearvidious.xcodeproj/project.pbxproj b/Pearvidious.xcodeproj/project.pbxproj index 7bd6fb27..a1d81fa5 100644 --- a/Pearvidious.xcodeproj/project.pbxproj +++ b/Pearvidious.xcodeproj/project.pbxproj @@ -421,6 +421,9 @@ 37FD43E42704847C0073EE42 /* View+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FD43E22704847C0073EE42 /* View+Fixtures.swift */; }; 37FD43E52704847C0073EE42 /* View+Fixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FD43E22704847C0073EE42 /* View+Fixtures.swift */; }; 37FD43F02704A9C00073EE42 /* RecentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C194C626F6A9C8005D3B96 /* RecentsModel.swift */; }; + 37FFC440272734C3009FFD26 /* Throttle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FFC43F272734C3009FFD26 /* Throttle.swift */; }; + 37FFC441272734C3009FFD26 /* Throttle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FFC43F272734C3009FFD26 /* Throttle.swift */; }; + 37FFC442272734C3009FFD26 /* Throttle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FFC43F272734C3009FFD26 /* Throttle.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -635,6 +638,7 @@ 37FB285D272225E800A57617 /* ContentItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentItemView.swift; sourceTree = ""; }; 37FD43DB270470B70073EE42 /* InstancesSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesSettings.swift; sourceTree = ""; }; 37FD43E22704847C0073EE42 /* View+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Fixtures.swift"; sourceTree = ""; }; + 37FFC43F272734C3009FFD26 /* Throttle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Throttle.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1013,6 +1017,7 @@ 3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */, 37D4B0C22671614700C925CA /* PearvidiousApp.swift */, 3700155E271B12DD0049C794 /* SiestaConfiguration.swift */, + 37FFC43F272734C3009FFD26 /* Throttle.swift */, 37CB12782724C76D00213B45 /* VideoURLParser.swift */, 37D4B0C42671614800C925CA /* Assets.xcassets */, 37BD07C42698ADEE003EBB87 /* Pearvidious.entitlements */, @@ -1634,6 +1639,7 @@ 37BA794726DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */, 377FC7DC267A081800A6BBAF /* PopularView.swift in Sources */, 37CC3F4C270CFE1700608308 /* PlayerQueueView.swift in Sources */, + 37FFC440272734C3009FFD26 /* Throttle.swift in Sources */, 3705B182267B4E4900704544 /* TrendingCategory.swift in Sources */, 3700155F271B12DD0049C794 /* SiestaConfiguration.swift in Sources */, 37EAD86F267B9ED100D9E01B /* Segment.swift in Sources */, @@ -1753,6 +1759,7 @@ 37EAD870267B9ED100D9E01B /* Segment.swift in Sources */, 3788AC2826F6840700F6BAA9 /* WatchNowSection.swift in Sources */, 37F64FE526FE70A60081B69E /* RedrawOnModifier.swift in Sources */, + 37FFC441272734C3009FFD26 /* Throttle.swift in Sources */, 37BA793C26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */, 378E510026FE8EEE00F49626 /* AccountsMenuView.swift in Sources */, 37141670267A8ACC006CA35D /* TrendingView.swift in Sources */, @@ -1880,6 +1887,7 @@ 37319F0727103F94004ECCD0 /* PlayerQueue.swift in Sources */, 37E70925271CD43000D34DDE /* WelcomeScreen.swift in Sources */, 37DD87C9271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */, + 37FFC442272734C3009FFD26 /* Throttle.swift in Sources */, 375168D82700FDB9008F96A6 /* Debounce.swift in Sources */, 37BA794126DB8F97002A0235 /* ChannelVideosView.swift in Sources */, 37C0697C2725C09E00F7F6CB /* PlayerQueueItemBridge.swift in Sources */, diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index f3c32b13..eea7ebca 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -21,6 +21,7 @@ extension Defaults.Keys { ]) static let lastAccountID = Key("lastAccountID") static let lastInstanceID = Key("lastInstanceID") + static let lastUsedPlaylistID = Key("lastPlaylistID") static let sponsorBlockInstance = Key("sponsorBlockInstance", default: "https://sponsor.ajay.app") static let sponsorBlockCategories = Key>("sponsorBlockCategories", default: Set(SponsorBlockAPI.categories)) diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index 1d7fd78a..a2841035 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -59,6 +59,7 @@ struct ContentView: View { .environmentObject(instances) .environmentObject(navigation) .environmentObject(player) + .environmentObject(playlists) .environmentObject(subscriptions) .environmentObject(thumbnailsModel) } @@ -70,6 +71,7 @@ struct ContentView: View { .environmentObject(instances) .environmentObject(navigation) .environmentObject(player) + .environmentObject(playlists) .environmentObject(subscriptions) .environmentObject(thumbnailsModel) } diff --git a/Shared/Player/VideoDetails.swift b/Shared/Player/VideoDetails.swift index a4b2dd70..7b6e5c86 100644 --- a/Shared/Player/VideoDetails.swift +++ b/Shared/Player/VideoDetails.swift @@ -11,6 +11,7 @@ struct VideoDetails: View { @State private var subscribed = false @State private var confirmationShown = false + @State private var presentingAddToPlaylist = false @State private var currentPage = Page.details @@ -18,6 +19,7 @@ struct VideoDetails: View { @EnvironmentObject private var accounts @EnvironmentObject private var player + @EnvironmentObject private var playlists @EnvironmentObject private var subscriptions init( @@ -266,11 +268,25 @@ struct VideoDetails: View { } Spacer() + + Button { + presentingAddToPlaylist = true + } label: { + Label("Add to Playlist", systemImage: "text.badge.plus") + .labelStyle(.iconOnly) + .help("Add to Playlist...") + } + .buttonStyle(.plain) } .frame(maxHeight: 35) .foregroundColor(.secondary) } } + .sheet(isPresented: $presentingAddToPlaylist) { + if let video = video { + AddToPlaylistView(video: video) + } + } } var detailsPage: some View { @@ -351,7 +367,7 @@ struct VideoDetails: View { struct VideoDetails_Previews: PreviewProvider { static var previews: some View { - VideoDetails(sidebarQueue: .constant(false)) + VideoDetails(sidebarQueue: .constant(true)) .injectFixtureEnvironmentObjects() } } diff --git a/Shared/Playlists/AddToPlaylistView.swift b/Shared/Playlists/AddToPlaylistView.swift index 260f2789..39b9e7fa 100644 --- a/Shared/Playlists/AddToPlaylistView.swift +++ b/Shared/Playlists/AddToPlaylistView.swift @@ -27,7 +27,7 @@ struct AddToPlaylistView: View { } .onAppear { model.load { - if let playlist = model.all.first { + if let playlist = model.find(id: Defaults[.lastUsedPlaylistID]) ?? model.all.first { selectedPlaylistID = playlist.id } } @@ -117,22 +117,22 @@ struct AddToPlaylistView: View { HStack { Spacer() Button("Add to Playlist", action: addToPlaylist) + .disabled(selectedPlaylist.isNil) + .padding(.top, 30) #if !os(tvOS) .keyboardShortcut(.defaultAction) #endif - .disabled(currentPlaylist.isNil) - .padding(.top, 30) } .padding(.horizontal) } private var selectPlaylistButton: some View { - Button(currentPlaylist?.title ?? "Select playlist") { - guard currentPlaylist != nil else { + Button(selectedPlaylist?.title ?? "Select playlist") { + guard selectedPlaylist != nil else { return } - selectedPlaylistID = model.all.next(after: currentPlaylist!)!.id + selectedPlaylistID = model.all.next(after: selectedPlaylist!)!.id } .contextMenu { ForEach(model.all) { playlist in @@ -146,16 +146,18 @@ struct AddToPlaylistView: View { } private func addToPlaylist() { - guard currentPlaylist != nil else { + guard let id = selectedPlaylist?.id else { return } - model.addVideo(playlistID: currentPlaylist!.id, videoID: video.videoID) { + Defaults[.lastUsedPlaylistID] = id + + model.addVideo(playlistID: id, videoID: video.videoID) { dismiss() } } - private var currentPlaylist: Playlist? { + private var selectedPlaylist: Playlist? { model.find(id: selectedPlaylistID) ?? model.all.first } } diff --git a/Shared/Throttle.swift b/Shared/Throttle.swift new file mode 100644 index 00000000..80244226 --- /dev/null +++ b/Shared/Throttle.swift @@ -0,0 +1,40 @@ +import Foundation + +final class Throttle { + let interval: TimeInterval + private(set) var lastExecutedAt: Date? + + private let syncQueue = DispatchQueue(label: "net.yatee.app.throttle") + + init(interval: TimeInterval) { + self.interval = interval + } + + @discardableResult func execute(_ action: () -> Void) -> Bool { + let executed = syncQueue.sync { () -> Bool in + let now = Date() + + let timeInterval = now.timeIntervalSince(lastExecutedAt ?? .distantPast) + + if timeInterval > interval { + lastExecutedAt = now + + return true + } + + return false + } + + if executed { + action() + } + + return executed + } + + func reset() { + syncQueue.sync { + lastExecutedAt = nil + } + } +} diff --git a/macOS/PlayerViewController.swift b/macOS/PlayerViewController.swift index fc9982fb..998f17de 100644 --- a/macOS/PlayerViewController.swift +++ b/macOS/PlayerViewController.swift @@ -6,7 +6,7 @@ final class PlayerViewController: NSViewController { var playerView = AVPlayerView() override func viewDidDisappear() { - // TODO: pause on disappear settings + playerModel.pause() super.viewDidDisappear() }