Unwatched videos in subscriptions

This commit is contained in:
Arkadiusz Fal 2022-12-13 00:39:50 +01:00
parent 02b30394ed
commit 8c1d900a63
8 changed files with 154 additions and 17 deletions

View File

@ -1,16 +1,11 @@
import SwiftUI import SwiftUI
extension Backport where Content: View { extension Backport where Content: View {
@ViewBuilder func badge(_ count: Text) -> some View { @ViewBuilder func badge(_ count: Text?) -> some View {
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) { if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
content.badge(count) content.badge(count)
} else { } else {
HStack { content
content
Spacer()
Text("\(count)")
.foregroundColor(.secondary)
}
} }
} }
} }

View File

@ -1,4 +1,5 @@
import Cache import Cache
import CoreData
import Foundation import Foundation
import Siesta import Siesta
import SwiftyJSON import SwiftyJSON
@ -9,11 +10,15 @@ final class FeedModel: ObservableObject, CacheModel {
@Published var isLoading = false @Published var isLoading = false
@Published var videos = [Video]() @Published var videos = [Video]()
@Published private var page = 1 @Published private var page = 1
@Published var unwatched = [Account: Int]()
private var cacheModel = FeedCacheModel.shared
private var accounts = AccountsModel.shared private var accounts = AccountsModel.shared
var storage: Storage<String, JSON>? var storage: Storage<String, JSON>?
private var backgroundContext = PersistenceController.shared.container.newBackgroundContext()
var feed: Resource? { var feed: Resource? {
accounts.api.feed(page) accounts.api.feed(page)
} }
@ -78,7 +83,8 @@ final class FeedModel: ObservableObject, CacheModel {
self.videos.append(contentsOf: videos) self.videos.append(contentsOf: videos)
} else { } else {
self.videos = videos self.videos = videos
FeedCacheModel.shared.storeFeed(account: account, videos: self.videos) self.cacheModel.storeFeed(account: account, videos: self.videos)
self.calculateUnwatchedFeed()
} }
} }
} }
@ -99,9 +105,87 @@ final class FeedModel: ObservableObject, CacheModel {
loadFeed(force: true, paginating: true) loadFeed(force: true, paginating: true)
} }
func calculateUnwatchedFeed() {
guard let account = accounts.current else { return }
let feed = cacheModel.retrieveFeed(account: account)
guard !feed.isEmpty else { return }
backgroundContext.perform { [weak self] in
guard let self else { return }
let watched = self.watchFetchRequestResult(feed, context: self.backgroundContext).filter { $0.finished }.count
let unwatched = feed.count - watched
DispatchQueue.main.async { [weak self] in
guard let self else { return }
if unwatched != self.unwatched[account] {
self.unwatched[account] = unwatched
}
}
}
}
func markAllFeedAsWatched() {
guard let account = accounts.current else { return }
guard !videos.isEmpty else { return }
backgroundContext.perform { [weak self] in
guard let self else { return }
self.videos.forEach { Watch.markAsWatched(videoID: $0.videoID, account: account, duration: $0.length, context: self.backgroundContext) }
self.calculateUnwatchedFeed()
}
}
func markAllFeedAsUnwatched() {
guard accounts.current != nil,
!videos.isEmpty else { return }
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()
}
}
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 playUnwatchedFeed() {
guard let account = accounts.current else { return }
let videos = cacheModel.retrieveFeed(account: account)
guard !videos.isEmpty else { return }
let watches = watchFetchRequestResult(videos, context: backgroundContext)
let watchesIDs = watches.map(\.videoID)
let unwatched = videos.filter { video in
if !watchesIDs.contains(video.videoID) {
return true
}
if let watch = watches.first(where: { $0.videoID == video.videoID }),
watch.finished
{
return false
}
return true
}
guard !unwatched.isEmpty else { return }
PlayerModel.shared.play(unwatched)
}
var feedTime: Date? { var feedTime: Date? {
if let account = accounts.current { if let account = accounts.current {
return FeedCacheModel.shared.getFeedTime(account: account) return cacheModel.getFeedTime(account: account)
} }
return nil return nil
@ -113,7 +197,7 @@ final class FeedModel: ObservableObject, CacheModel {
private func loadCachedFeed() { private func loadCachedFeed() {
guard let account = accounts.current else { return } guard let account = accounts.current else { return }
let cache = FeedCacheModel.shared.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

View File

@ -12,7 +12,7 @@ struct AccountsView: View {
list list
} }
.frame(minWidth: 500, maxWidth: 800, minHeight: 350, maxHeight: 700) .frame(minWidth: 500, maxWidth: 800, minHeight: 700, maxHeight: 1200)
#else #else
NavigationView { NavigationView {

View File

@ -5,6 +5,7 @@ struct AppTabNavigation: View {
@ObservedObject private var accounts = AccountsModel.shared @ObservedObject private var accounts = AccountsModel.shared
@ObservedObject private var navigation = NavigationModel.shared @ObservedObject private var navigation = NavigationModel.shared
private var player = PlayerModel.shared private var player = PlayerModel.shared
@ObservedObject private var feed = FeedModel.shared
@ObservedObject private var subscriptions = SubscribedChannelsModel.shared @ObservedObject private var subscriptions = SubscribedChannelsModel.shared
@Default(.showHome) private var showHome @Default(.showHome) private var showHome
@ -47,7 +48,12 @@ struct AppTabNavigation: View {
} }
.overlay(ControlsBar(fullScreen: .constant(false)), alignment: .bottom) .overlay(ControlsBar(fullScreen: .constant(false)), alignment: .bottom)
} }
.onAppear {
feed.calculateUnwatchedFeed()
}
.onChange(of: accounts.current) { _ in
feed.calculateUnwatchedFeed()
}
.id(accounts.current?.id ?? "") .id(accounts.current?.id ?? "")
.overlay(playlistView) .overlay(playlistView)
.overlay(channelView) .overlay(channelView)
@ -87,6 +93,19 @@ struct AppTabNavigation: View {
.accessibility(label: Text("Subscriptions")) .accessibility(label: Text("Subscriptions"))
} }
.tag(TabSelection.subscriptions) .tag(TabSelection.subscriptions)
.backport
.badge(subscriptionsBadge)
}
var subscriptionsBadge: Text? {
guard let account = accounts.current,
let unwatched = feed.unwatched[account],
unwatched > 0
else {
return nil
}
return Text("\(String(unwatched))")
} }
private var subscriptionsVisible: Bool { private var subscriptionsVisible: Bool {

View File

@ -4,6 +4,7 @@ import SwiftUI
struct Sidebar: View { struct Sidebar: View {
@ObservedObject private var accounts = AccountsModel.shared @ObservedObject private var accounts = AccountsModel.shared
@ObservedObject private var navigation = NavigationModel.shared @ObservedObject private var navigation = NavigationModel.shared
@ObservedObject private var feed = FeedModel.shared
@Default(.showHome) private var showHome @Default(.showHome) private var showHome
@Default(.visibleSections) private var visibleSections @Default(.visibleSections) private var visibleSections
@ -36,6 +37,12 @@ struct Sidebar: View {
} }
.listStyle(.sidebar) .listStyle(.sidebar)
} }
.onAppear {
feed.calculateUnwatchedFeed()
}
.onChange(of: accounts.current) { _ in
feed.calculateUnwatchedFeed()
}
.navigationTitle("Yattee") .navigationTitle("Yattee")
#if os(iOS) #if os(iOS)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
@ -70,6 +77,8 @@ struct Sidebar: View {
Label("Subscriptions", systemImage: "star.circle") Label("Subscriptions", systemImage: "star.circle")
.accessibility(label: Text("Subscriptions")) .accessibility(label: Text("Subscriptions"))
} }
.backport
.badge(subscriptionsBadge)
.id("subscriptions") .id("subscriptions")
} }
@ -99,6 +108,17 @@ struct Sidebar: View {
} }
} }
private var subscriptionsBadge: Text? {
guard let account = accounts.current,
let unwatched = feed.unwatched[account],
unwatched > 0
else {
return nil
}
return Text("\(String(unwatched))")
}
private func scrollScrollViewToItem(scrollView: ScrollViewProxy, for selection: TabSelection) { private func scrollScrollViewToItem(scrollView: ScrollViewProxy, for selection: TabSelection) {
if case .recentlyOpened = selection { if case .recentlyOpened = selection {
scrollView.scrollTo("recentlyOpened") scrollView.scrollTo("recentlyOpened")

View File

@ -90,7 +90,7 @@ struct PlayerQueueRow: View {
player.show() player.show()
} label: { } label: {
VideoBanner(video: item.video, playbackTime: watchStoppedAt, videoDuration: watch?.videoDuration) VideoBanner(video: item.video, playbackTime: watchStoppedAt, videoDuration: watch?.videoDuration, watch: watch)
} }
#if os(tvOS) #if os(tvOS)
.buttonStyle(.card) .buttonStyle(.card)

View File

@ -10,6 +10,8 @@ struct SubscriptionsView: View {
@Default(.subscriptionsViewPage) private var subscriptionsViewPage @Default(.subscriptionsViewPage) private var subscriptionsViewPage
@Default(.subscriptionsListingStyle) private var subscriptionsListingStyle @Default(.subscriptionsListingStyle) private var subscriptionsListingStyle
@ObservedObject private var feed = FeedModel.shared
var body: some View { var body: some View {
SignInRequiredView(title: "Subscriptions".localized()) { SignInRequiredView(title: "Subscriptions".localized()) {
switch subscriptionsViewPage { switch subscriptionsViewPage {
@ -51,6 +53,24 @@ 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")
}
} }
Section { Section {

View File

@ -149,6 +149,7 @@ struct VideoContextMenuView: View {
var markAsWatchedButton: some View { var markAsWatchedButton: some View {
Button { Button {
Watch.markAsWatched(videoID: video.videoID, account: accounts.current, duration: video.length, context: backgroundContext) Watch.markAsWatched(videoID: video.videoID, account: accounts.current, duration: video.length, context: backgroundContext)
FeedModel.shared.calculateUnwatchedFeed()
} label: { } label: {
Label("Mark as watched", systemImage: "checkmark.circle.fill") Label("Mark as watched", systemImage: "checkmark.circle.fill")
} }
@ -156,11 +157,9 @@ struct VideoContextMenuView: View {
var removeFromHistoryButton: some View { var removeFromHistoryButton: some View {
Button { Button {
guard let watch else { guard let watch else { return }
return
}
player.removeWatch(watch) player.removeWatch(watch)
FeedModel.shared.calculateUnwatchedFeed()
} label: { } label: {
Label("Remove from history", systemImage: "delete.left.fill") Label("Remove from history", systemImage: "delete.left.fill")
} }