Show badge for channels subscriptions

This commit is contained in:
Arkadiusz Fal 2022-12-13 13:14:20 +01:00
parent d5626b877c
commit b3ddf4a153
5 changed files with 119 additions and 40 deletions

View File

@ -70,6 +70,7 @@ final class SubscribedChannelsModel: ObservableObject, CacheModel {
if let channels: [Channel] = resource.typedContent() { if let channels: [Channel] = resource.typedContent() {
self.channels = channels self.channels = channels
self.storeChannels(account: account, channels: channels) self.storeChannels(account: account, channels: channels)
FeedModel.shared.calculateUnwatchedFeed()
onSuccess() onSuccess()
} }
} }

View File

@ -11,6 +11,7 @@ final class FeedModel: ObservableObject, CacheModel {
@Published var videos = [Video]() @Published var videos = [Video]()
@Published private var page = 1 @Published private var page = 1
@Published var unwatched = [Account: Int]() @Published var unwatched = [Account: Int]()
@Published var unwatchedByChannel = [Account: [Channel.ID: Int]]()
private var cacheModel = FeedCacheModel.shared private var cacheModel = FeedCacheModel.shared
private var accounts = AccountsModel.shared private var accounts = AccountsModel.shared
@ -112,43 +113,66 @@ final class FeedModel: ObservableObject, CacheModel {
backgroundContext.perform { [weak self] in backgroundContext.perform { [weak self] in
guard let self else { return } guard let self else { return }
let watched = self.watchFetchRequestResult(feed, context: self.backgroundContext).filter { $0.finished }.count let watched = self.watchFetchRequestResult(feed, context: self.backgroundContext).filter { $0.finished }
let unwatched = feed.count - watched let unwatched = feed.filter { video in !watched.contains { $0.videoID == video.videoID } }
let unwatchedCount = feed.count - watched.count
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
guard let self else { return } guard let self else { return }
if unwatched != self.unwatched[account] { if unwatchedCount != self.unwatched[account] {
self.unwatched[account] = unwatched self.unwatched[account] = unwatchedCount
} }
let byChannel = Dictionary(grouping: unwatched) { $0.channel.id }.mapValues(\.count)
self.unwatchedByChannel[account] = byChannel
} }
} }
} }
func markAllFeedAsWatched() { func markAllFeedAsWatched() {
guard let account = accounts.current else { return } guard let account = accounts.current else { return }
guard !videos.isEmpty else { return }
backgroundContext.perform { [weak self] in let mark = { [weak self] in
guard let self else { return } self?.backgroundContext.perform { [weak self] in
self.videos.forEach { Watch.markAsWatched(videoID: $0.videoID, account: account, duration: $0.length, context: self.backgroundContext) } guard let self else { return }
self.videos.forEach { Watch.markAsWatched(videoID: $0.videoID, account: account, duration: $0.length, context: self.backgroundContext) }
self.calculateUnwatchedFeed() self.calculateUnwatchedFeed()
}
}
if videos.isEmpty {
loadCachedFeed { mark() }
} else {
mark()
} }
} }
var canMarkAllFeedAsWatched: Bool {
guard let account = accounts.current else { return false }
return (unwatched[account] ?? 0) > 0
}
func markAllFeedAsUnwatched() { func markAllFeedAsUnwatched() {
guard accounts.current != nil, guard accounts.current != nil else { return }
!videos.isEmpty else { return }
backgroundContext.perform { [weak self] in let mark = { [weak self] in
guard let self else { return } self?.backgroundContext.perform { [weak self] in
guard let self else { return }
let watches = self.watchFetchRequestResult(self.videos, context: self.backgroundContext) let watches = self.watchFetchRequestResult(self.videos, context: self.backgroundContext)
watches.forEach { self.backgroundContext.delete($0) } watches.forEach { self.backgroundContext.delete($0) }
try? self.backgroundContext.save() try? self.backgroundContext.save()
self.calculateUnwatchedFeed() self.calculateUnwatchedFeed()
}
}
if videos.isEmpty {
loadCachedFeed { mark() }
} else {
mark()
} }
} }
@ -183,6 +207,11 @@ final class FeedModel: ObservableObject, CacheModel {
PlayerModel.shared.play(unwatched) PlayerModel.shared.play(unwatched)
} }
var canPlayUnwatchedFeed: Bool {
guard let account = accounts.current else { return false }
return (unwatched[account] ?? 0) > 0
}
var feedTime: Date? { var feedTime: Date? {
if let account = accounts.current { if let account = accounts.current {
return cacheModel.getFeedTime(account: account) return cacheModel.getFeedTime(account: account)
@ -195,12 +224,13 @@ final class FeedModel: ObservableObject, CacheModel {
getFormattedDate(feedTime) getFormattedDate(feedTime)
} }
private func loadCachedFeed() { private func loadCachedFeed(_ onCompletion: @escaping () -> Void = {}) {
guard let account = accounts.current else { return } guard let account = accounts.current else { return }
let cache = cacheModel.retrieveFeed(account: account) let cache = cacheModel.retrieveFeed(account: account)
if !cache.isEmpty { if !cache.isEmpty {
DispatchQueue.main.async(qos: .userInteractive) { [weak self] in DispatchQueue.main.async(qos: .userInteractive) { [weak self] in
self?.videos = cache self?.videos = cache
onCompletion()
} }
} }
} }

View File

@ -3,24 +3,29 @@ import SwiftUI
struct AppSidebarSubscriptions: View { struct AppSidebarSubscriptions: View {
@ObservedObject private var navigation = NavigationModel.shared @ObservedObject private var navigation = NavigationModel.shared
@ObservedObject private var feed = FeedModel.shared
@ObservedObject private var subscriptions = SubscribedChannelsModel.shared @ObservedObject private var subscriptions = SubscribedChannelsModel.shared
@ObservedObject private var accounts = AccountsModel.shared
var body: some View { var body: some View {
Section(header: Text("Subscriptions")) { Section(header: Text("Subscriptions")) {
ForEach(subscriptions.all) { channel in ForEach(subscriptions.all) { channel in
NavigationLink(tag: TabSelection.channel(channel.id), selection: $navigation.tabSelection) { NavigationLink(tag: TabSelection.channel(channel.id), selection: $navigation.tabSelection) {
LazyView(ChannelVideosView(channel: channel).modifier(PlayerOverlayModifier())) LazyView(ChannelVideosView(channel: channel).modifier(PlayerOverlayModifier()))
} label: { } label: {
if channel.thumbnailURL != nil { HStack {
HStack { if channel.thumbnailURL != nil {
ChannelAvatarView(channel: channel, subscribedBadge: false) ChannelAvatarView(channel: channel, subscribedBadge: false)
.frame(width: 20, height: 20) .frame(width: 20, height: 20)
Text(channel.name) Text(channel.name)
} else {
Label(channel.name, systemImage: RecentsModel.symbolSystemImage(channel.name))
} }
} else {
Label(channel.name, systemImage: RecentsModel.symbolSystemImage(channel.name))
} }
.backport
.badge(channelBadge(channel))
} }
.contextMenu { .contextMenu {
Button("Unsubscribe") { Button("Unsubscribe") {
@ -31,6 +36,14 @@ struct AppSidebarSubscriptions: View {
} }
} }
} }
func channelBadge(_ channel: Channel) -> Text? {
if let count = feed.unwatchedByChannel[accounts.current]?[channel.id] {
return Text(String(count))
}
return nil
}
} }
struct AppSidebarSubscriptions_Previews: PreviewProvider { struct AppSidebarSubscriptions_Previews: PreviewProvider {

View File

@ -3,6 +3,7 @@ import SDWebImageSwiftUI
import SwiftUI import SwiftUI
struct ChannelsView: View { struct ChannelsView: View {
@ObservedObject private var feed = FeedModel.shared
@ObservedObject private var subscriptions = SubscribedChannelsModel.shared @ObservedObject private var subscriptions = SubscribedChannelsModel.shared
@ObservedObject private var accounts = AccountsModel.shared @ObservedObject private var accounts = AccountsModel.shared
@ -23,6 +24,8 @@ struct ChannelsView: View {
Label(channel.name, systemImage: RecentsModel.symbolSystemImage(channel.name)) Label(channel.name, systemImage: RecentsModel.symbolSystemImage(channel.name))
} }
} }
.backport
.badge(channelBadge(channel))
} }
.contextMenu { .contextMenu {
Button { Button {
@ -78,6 +81,14 @@ struct ChannelsView: View {
#endif #endif
} }
func channelBadge(_ channel: Channel) -> Text? {
if let count = feed.unwatchedByChannel[accounts.current]?[channel.id] {
return Text(String(count))
}
return nil
}
var header: some View { var header: some View {
HStack { HStack {
#if os(tvOS) #if os(tvOS)

View File

@ -39,6 +39,10 @@ struct SubscriptionsView: View {
ToolbarItem { ToolbarItem {
ListingStyleButtons(listingStyle: $subscriptionsListingStyle) ListingStyleButtons(listingStyle: $subscriptionsListingStyle)
} }
ToolbarItem {
toggleWatchedButton
}
} }
#endif #endif
} }
@ -53,26 +57,12 @@ struct SubscriptionsView: View {
if subscriptionsViewPage == .feed { if subscriptionsViewPage == .feed {
ListingStyleButtons(listingStyle: $subscriptionsListingStyle) ListingStyleButtons(listingStyle: $subscriptionsListingStyle)
Button {
feed.playUnwatchedFeed()
} label: {
Label("Play unwatched", systemImage: "play")
}
Button {
feed.markAllFeedAsWatched()
} label: {
Label("Mark all as watched", systemImage: "checkmark.circle.fill")
}
Button {
feed.markAllFeedAsUnwatched()
} label: {
Label("Mark all as unwatched", systemImage: "checkmark.circle")
}
} }
playUnwatchedButton
toggleWatchedButton
Section { Section {
SettingsButtons() SettingsButtons()
} }
@ -98,6 +88,40 @@ struct SubscriptionsView: View {
} }
} }
#endif #endif
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")
}
}
} }
struct SubscriptionsView_Previews: PreviewProvider { struct SubscriptionsView_Previews: PreviewProvider {