mirror of
https://github.com/yattee/yattee.git
synced 2024-11-10 00:08:21 +00:00
Unwatched videos in subscriptions
This commit is contained in:
parent
02b30394ed
commit
8c1d900a63
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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")
|
||||||
|
@ -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)
|
||||||
|
@ -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 {
|
||||||
|
@ -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")
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user