mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 21:43:41 +00:00
Show badge for channels subscriptions
This commit is contained in:
parent
d5626b877c
commit
b3ddf4a153
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,23 +113,27 @@ 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
|
||||||
|
self?.backgroundContext.perform { [weak self] in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
self.videos.forEach { Watch.markAsWatched(videoID: $0.videoID, account: account, duration: $0.length, context: self.backgroundContext) }
|
self.videos.forEach { Watch.markAsWatched(videoID: $0.videoID, account: account, duration: $0.length, context: self.backgroundContext) }
|
||||||
|
|
||||||
@ -136,11 +141,23 @@ final class FeedModel: ObservableObject, CacheModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func markAllFeedAsUnwatched() {
|
if videos.isEmpty {
|
||||||
guard accounts.current != nil,
|
loadCachedFeed { mark() }
|
||||||
!videos.isEmpty else { return }
|
} else {
|
||||||
|
mark()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
backgroundContext.perform { [weak self] in
|
var canMarkAllFeedAsWatched: Bool {
|
||||||
|
guard let account = accounts.current else { return false }
|
||||||
|
return (unwatched[account] ?? 0) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func markAllFeedAsUnwatched() {
|
||||||
|
guard accounts.current != nil else { return }
|
||||||
|
|
||||||
|
let mark = { [weak self] in
|
||||||
|
self?.backgroundContext.perform { [weak self] in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|
||||||
let watches = self.watchFetchRequestResult(self.videos, context: self.backgroundContext)
|
let watches = self.watchFetchRequestResult(self.videos, context: self.backgroundContext)
|
||||||
@ -152,6 +169,13 @@ final class FeedModel: ObservableObject, CacheModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if videos.isEmpty {
|
||||||
|
loadCachedFeed { mark() }
|
||||||
|
} else {
|
||||||
|
mark()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func watchFetchRequestResult(_ videos: [Video], context: NSManagedObjectContext) -> [Watch] {
|
func watchFetchRequestResult(_ videos: [Video], context: NSManagedObjectContext) -> [Watch] {
|
||||||
let watchFetchRequest = Watch.fetchRequest()
|
let watchFetchRequest = Watch.fetchRequest()
|
||||||
watchFetchRequest.predicate = NSPredicate(format: "videoID IN %@", videos.map(\.videoID) as [String])
|
watchFetchRequest.predicate = NSPredicate(format: "videoID IN %@", videos.map(\.videoID) as [String])
|
||||||
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,25 +3,30 @@ 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 {
|
} else {
|
||||||
Label(channel.name, systemImage: RecentsModel.symbolSystemImage(channel.name))
|
Label(channel.name, systemImage: RecentsModel.symbolSystemImage(channel.name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.backport
|
||||||
|
.badge(channelBadge(channel))
|
||||||
|
}
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
Button("Unsubscribe") {
|
Button("Unsubscribe") {
|
||||||
navigation.presentUnsubscribeAlert(channel, subscriptions: subscriptions)
|
navigation.presentUnsubscribeAlert(channel, subscriptions: subscriptions)
|
||||||
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
@ -39,6 +39,10 @@ struct SubscriptionsView: View {
|
|||||||
ToolbarItem {
|
ToolbarItem {
|
||||||
ListingStyleButtons(listingStyle: $subscriptionsListingStyle)
|
ListingStyleButtons(listingStyle: $subscriptionsListingStyle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ToolbarItem {
|
||||||
|
toggleWatchedButton
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@ -53,25 +57,11 @@ 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 {
|
playUnwatchedButton
|
||||||
feed.markAllFeedAsWatched()
|
|
||||||
} label: {
|
|
||||||
Label("Mark all as watched", systemImage: "checkmark.circle.fill")
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
toggleWatchedButton
|
||||||
feed.markAllFeedAsUnwatched()
|
|
||||||
} label: {
|
|
||||||
Label("Mark all as unwatched", systemImage: "checkmark.circle")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user