From a156ef6a3f9e261abf0cfc8789a00d2ba8c726f0 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Wed, 14 Dec 2022 18:10:01 +0100 Subject: [PATCH] Add action to mark channel feed as watched/unwatched --- Model/FeedModel.swift | 79 +++++++++++++++---- Shared/Channels/ChannelVideosView.swift | 38 +++++++++ .../Navigation/AppSidebarSubscriptions.swift | 29 +++++++ Shared/Navigation/Sidebar.swift | 38 +++++++++ Shared/Subscriptions/ChannelsView.swift | 28 +++++++ 5 files changed, 196 insertions(+), 16 deletions(-) diff --git a/Model/FeedModel.swift b/Model/FeedModel.swift index 41407ce4..02e1aa43 100644 --- a/Model/FeedModel.swift +++ b/Model/FeedModel.swift @@ -158,20 +158,18 @@ final class FeedModel: ObservableObject, CacheModel { return (unwatched[account] ?? 0) > 0 } - func markAllFeedAsUnwatched() { - guard accounts.current != nil else { return } + func canMarkChannelAsWatched(_ channelID: Channel.ID) -> Bool { + guard let account = accounts.current, accounts.signedIn else { return false } + + return unwatchedByChannel[account]?.keys.contains(channelID) ?? false + } + + func markChannelAsWatched(_ channelID: Channel.ID) { + guard accounts.signedIn else { return } let mark = { [weak self] in - self?.backgroundContext.perform { [weak self] in - guard let self else { return } - - let watches = self.watchFetchRequestResult(self.videos, context: self.backgroundContext) - watches.forEach { self.backgroundContext.delete($0) } - - try? self.backgroundContext.save() - - self.calculateUnwatchedFeed() - } + guard let self else { return } + self.markVideos(self.videos.filter { $0.channel.id == channelID }, watched: true) } if videos.isEmpty { @@ -181,10 +179,53 @@ final class FeedModel: ObservableObject, CacheModel { } } - func watchFetchRequestResult(_ videos: [Video], context: NSManagedObjectContext) -> [Watch] { - let watchFetchRequest = Watch.fetchRequest() - watchFetchRequest.predicate = NSPredicate(format: "videoID IN %@", videos.map(\.videoID) as [String]) - return (try? context.fetch(watchFetchRequest)) ?? [] + func markChannelAsUnwatched(_ channelID: Channel.ID) { + guard accounts.signedIn else { return } + + let mark = { [weak self] in + guard let self else { return } + self.markVideos(self.videos.filter { $0.channel.id == channelID }, watched: false) + } + + if videos.isEmpty { + loadCachedFeed { mark() } + } else { + mark() + } + } + + func markAllFeedAsUnwatched() { + guard accounts.current != nil else { return } + + let mark = { [weak self] in + guard let self else { return } + self.markVideos(self.videos, watched: false) + } + + if videos.isEmpty { + loadCachedFeed { mark() } + } else { + mark() + } + } + + func markVideos(_ videos: [Video], watched: Bool) { + guard accounts.signedIn, let account = accounts.current else { return } + + backgroundContext.perform { [weak self] in + guard let self else { return } + + if watched { + videos.forEach { Watch.markAsWatched(videoID: $0.videoID, account: account, duration: $0.length, context: self.backgroundContext) } + } else { + let watches = self.watchFetchRequestResult(videos, context: self.backgroundContext) + watches.forEach { self.backgroundContext.delete($0) } + } + + try? self.backgroundContext.save() + + self.calculateUnwatchedFeed() + } } func playUnwatchedFeed() { @@ -247,4 +288,10 @@ final class FeedModel: ObservableObject, CacheModel { return resource.loadIfNeeded() } + + private func watchFetchRequestResult(_ videos: [Video], context: NSManagedObjectContext) -> [Watch] { + let watchFetchRequest = Watch.fetchRequest() + watchFetchRequest.predicate = NSPredicate(format: "videoID IN %@", videos.map(\.videoID) as [String]) + return (try? context.fetch(watchFetchRequest)) ?? [] + } } diff --git a/Shared/Channels/ChannelVideosView.swift b/Shared/Channels/ChannelVideosView.swift index 5f2daf72..37b7cc8c 100644 --- a/Shared/Channels/ChannelVideosView.swift +++ b/Shared/Channels/ChannelVideosView.swift @@ -23,6 +23,7 @@ struct ChannelVideosView: View { #endif @ObservedObject private var accounts = AccountsModel.shared + @ObservedObject private var feed = FeedModel.shared @ObservedObject private var navigation = NavigationModel.shared @ObservedObject private var recents = RecentsModel.shared @ObservedObject private var subscriptions = SubscribedChannelsModel.shared @@ -129,6 +130,10 @@ struct ChannelVideosView: View { FavoriteButton(item: FavoriteItem(section: .channel(accounts.app.appType.rawValue, presentedChannel.id, presentedChannel.name))) } } + + ToolbarItem { + toggleWatchedButton + } #endif } #endif @@ -231,6 +236,10 @@ struct ChannelVideosView: View { FavoriteButton(item: FavoriteItem(section: .channel(accounts.app.appType.rawValue, channel.id, channel.name))) } + if subscriptions.isSubscribing(channel.id) { + toggleWatchedButton + } + ListingStyleButtons(listingStyle: $channelPlaylistListingStyle) } } label: { @@ -341,6 +350,35 @@ struct ChannelVideosView: View { private var navigationTitle: String { presentedChannel?.name ?? "No channel" } + + @ViewBuilder var toggleWatchedButton: some View { + if let channel = presentedChannel { + if feed.canMarkChannelAsWatched(channel.id) { + markChannelAsWatchedButton + } else { + markChannelAsUnwatchedButton + } + } + } + + var markChannelAsWatchedButton: some View { + Button { + guard let channel = presentedChannel else { return } + feed.markChannelAsWatched(channel.id) + } label: { + Label("Mark channel feed as watched", systemImage: "checkmark.circle.fill") + } + .disabled(!feed.canMarkAllFeedAsWatched) + } + + var markChannelAsUnwatchedButton: some View { + Button { + guard let channel = presentedChannel else { return } + feed.markChannelAsUnwatched(channel.id) + } label: { + Label("Mark channel feed as unwatched", systemImage: "checkmark.circle") + } + } } struct ChannelVideosView_Previews: PreviewProvider { diff --git a/Shared/Navigation/AppSidebarSubscriptions.swift b/Shared/Navigation/AppSidebarSubscriptions.swift index 9de15f35..b6d42f91 100644 --- a/Shared/Navigation/AppSidebarSubscriptions.swift +++ b/Shared/Navigation/AppSidebarSubscriptions.swift @@ -28,6 +28,10 @@ struct AppSidebarSubscriptions: View { .badge(channelBadge(channel)) } .contextMenu { + if subscriptions.isSubscribing(channel.id) { + toggleWatchedButton(channel) + } + Button("Unsubscribe") { navigation.presentUnsubscribeAlert(channel, subscriptions: subscriptions) } @@ -44,6 +48,31 @@ struct AppSidebarSubscriptions: View { return nil } + + @ViewBuilder func toggleWatchedButton(_ channel: Channel) -> some View { + if feed.canMarkChannelAsWatched(channel.id) { + markChannelAsWatchedButton(channel) + } else { + markChannelAsUnwatchedButton(channel) + } + } + + func markChannelAsWatchedButton(_ channel: Channel) -> some View { + Button { + feed.markChannelAsWatched(channel.id) + } label: { + Label("Mark channel feed as watched", systemImage: "checkmark.circle.fill") + } + .disabled(!feed.canMarkAllFeedAsWatched) + } + + func markChannelAsUnwatchedButton(_ channel: Channel) -> some View { + Button { + feed.markChannelAsUnwatched(channel.id) + } label: { + Label("Mark channel feed as unwatched", systemImage: "checkmark.circle") + } + } } struct AppSidebarSubscriptions_Previews: PreviewProvider { diff --git a/Shared/Navigation/Sidebar.swift b/Shared/Navigation/Sidebar.swift index c040c213..1d1ecd30 100644 --- a/Shared/Navigation/Sidebar.swift +++ b/Shared/Navigation/Sidebar.swift @@ -79,6 +79,10 @@ struct Sidebar: View { } .backport .badge(subscriptionsBadge) + .contextMenu { + playUnwatchedButton + toggleWatchedButton + } .id("subscriptions") } @@ -108,6 +112,40 @@ struct Sidebar: View { } } + var playUnwatchedButton: some View { + Button { + feed.playUnwatchedFeed() + } label: { + Label("Play all unwatched", systemImage: "play") + } + .disabled(!feed.canPlayUnwatchedFeed) + } + + @ViewBuilder var toggleWatchedButton: some View { + if feed.canMarkAllFeedAsWatched { + markAllFeedAsWatchedButton + } else { + markAllFeedAsUnwatchedButton + } + } + + var markAllFeedAsWatchedButton: some View { + Button { + feed.markAllFeedAsWatched() + } label: { + Label("Mark all as watched", systemImage: "checkmark.circle.fill") + } + .disabled(!feed.canMarkAllFeedAsWatched) + } + + var markAllFeedAsUnwatchedButton: some View { + Button { + feed.markAllFeedAsUnwatched() + } label: { + Label("Mark all as unwatched", systemImage: "checkmark.circle") + } + } + private var subscriptionsBadge: Text? { guard let account = accounts.current, let unwatched = feed.unwatched[account], diff --git a/Shared/Subscriptions/ChannelsView.swift b/Shared/Subscriptions/ChannelsView.swift index 0a3cd8da..3cfb1f94 100644 --- a/Shared/Subscriptions/ChannelsView.swift +++ b/Shared/Subscriptions/ChannelsView.swift @@ -28,6 +28,9 @@ struct ChannelsView: View { .badge(channelBadge(channel)) } .contextMenu { + if subscriptions.isSubscribing(channel.id) { + toggleWatchedButton(channel) + } Button { subscriptions.unsubscribe(channel.id) } label: { @@ -124,6 +127,31 @@ struct ChannelsView: View { .padding(.top, 15) #endif } + + @ViewBuilder func toggleWatchedButton(_ channel: Channel) -> some View { + if feed.canMarkChannelAsWatched(channel.id) { + markChannelAsWatchedButton(channel) + } else { + markChannelAsUnwatchedButton(channel) + } + } + + func markChannelAsWatchedButton(_ channel: Channel) -> some View { + Button { + feed.markChannelAsWatched(channel.id) + } label: { + Label("Mark channel feed as watched", systemImage: "checkmark.circle.fill") + } + .disabled(!feed.canMarkAllFeedAsWatched) + } + + func markChannelAsUnwatchedButton(_ channel: Channel) -> some View { + Button { + feed.markChannelAsUnwatched(channel.id) + } label: { + Label("Mark channel feed as unwatched", systemImage: "checkmark.circle") + } + } } struct ChannelsView_Previews: PreviewProvider {