From 0061bd8c2093d84ee89b4074f6c6886cf1809c42 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Thu, 25 May 2023 14:28:29 +0200 Subject: [PATCH] Home Settings --- ...ObjectContext+ExecuteAndMergeChanges.swift | 14 + Model/{ => Favorites}/FavoriteItem.swift | 7 + Model/{ => Favorites}/FavoritesModel.swift | 41 ++- Model/FeedModel.swift | 1 + Model/HistoryModel.swift | 16 +- Model/NavigationModel.swift | 1 + Model/WatchModel.swift | 11 + Shared/Defaults.swift | 59 +++- Shared/EnvironmentValues.swift | 9 - Shared/Home/FavoriteItemView.swift | 212 ++++++++++-- Shared/Home/HistoryView.swift | 16 +- Shared/Home/HomeView.swift | 169 ++++------ Shared/Home/QueueView.swift | 23 +- Shared/Navigation/ContentView.swift | 31 ++ Shared/Playlists/PlaylistsView.swift | 1 - Shared/Settings/BrowsingSettings.swift | 102 ++---- Shared/Settings/EditFavorites.swift | 119 ------- Shared/Settings/HomeSettings.swift | 302 ++++++++++++++++++ Shared/Videos/ListView.swift | 32 ++ Shared/Videos/VerticalCells.swift | 19 +- Shared/Videos/WatchView.swift | 1 + Shared/Views/AccentButton.swift | 9 +- Shared/Views/HomeSettingsButton.swift | 21 ++ Shared/Views/VideoContextMenuView.swift | 1 + Shared/YatteeApp.swift | 18 ++ Yattee.xcodeproj/project.pbxproj | 72 ++++- 26 files changed, 911 insertions(+), 396 deletions(-) create mode 100644 Extensions/NSManagedObjectContext+ExecuteAndMergeChanges.swift rename Model/{ => Favorites}/FavoriteItem.swift (92%) rename Model/{ => Favorites}/FavoritesModel.swift (52%) create mode 100644 Model/WatchModel.swift delete mode 100644 Shared/Settings/EditFavorites.swift create mode 100644 Shared/Settings/HomeSettings.swift create mode 100644 Shared/Videos/ListView.swift create mode 100644 Shared/Views/HomeSettingsButton.swift diff --git a/Extensions/NSManagedObjectContext+ExecuteAndMergeChanges.swift b/Extensions/NSManagedObjectContext+ExecuteAndMergeChanges.swift new file mode 100644 index 00000000..beabcbd1 --- /dev/null +++ b/Extensions/NSManagedObjectContext+ExecuteAndMergeChanges.swift @@ -0,0 +1,14 @@ +import CoreData + +extension NSManagedObjectContext { + /// Executes the given `NSBatchDeleteRequest` and directly merges the changes to bring the given managed object context up to date. + /// + /// - Parameter batchDeleteRequest: The `NSBatchDeleteRequest` to execute. + /// - Throws: An error if anything went wrong executing the batch deletion. + func executeAndMergeChanges(_ batchDeleteRequest: NSBatchDeleteRequest) throws { + batchDeleteRequest.resultType = .resultTypeObjectIDs + let result = try execute(batchDeleteRequest) as? NSBatchDeleteResult + let changes: [AnyHashable: Any] = [NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []] + NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [self]) + } +} diff --git a/Model/FavoriteItem.swift b/Model/Favorites/FavoriteItem.swift similarity index 92% rename from Model/FavoriteItem.swift rename to Model/Favorites/FavoriteItem.swift index 40b97454..00aa7d58 100644 --- a/Model/FavoriteItem.swift +++ b/Model/Favorites/FavoriteItem.swift @@ -3,6 +3,7 @@ import Foundation struct FavoriteItem: Codable, Equatable, Identifiable, Defaults.Serializable { enum Section: Codable, Equatable, Defaults.Serializable { + case history case subscriptions case popular case trending(String, String?) @@ -13,6 +14,8 @@ struct FavoriteItem: Codable, Equatable, Identifiable, Defaults.Serializable { var label: String { switch self { + case .history: + return "History" case .subscriptions: return "Subscriptions" case .popular: @@ -50,4 +53,8 @@ struct FavoriteItem: Codable, Equatable, Identifiable, Defaults.Serializable { var id = UUID().uuidString var section: Section + + var widgetSettingsKey: String { + "favorites-\(id)" + } } diff --git a/Model/FavoritesModel.swift b/Model/Favorites/FavoritesModel.swift similarity index 52% rename from Model/FavoritesModel.swift rename to Model/Favorites/FavoritesModel.swift index ac2b85f9..350711eb 100644 --- a/Model/FavoritesModel.swift +++ b/Model/Favorites/FavoritesModel.swift @@ -6,6 +6,7 @@ struct FavoritesModel { @Default(.showFavoritesInHome) var showFavoritesInHome @Default(.favorites) var all + @Default(.widgetsSettings) var widgetsSettings var isEnabled: Bool { showFavoritesInHome @@ -74,9 +75,47 @@ struct FavoritesModel { func addableItems() -> [FavoriteItem] { let allItems = [ FavoriteItem(section: .subscriptions), - FavoriteItem(section: .popular) + FavoriteItem(section: .popular), + FavoriteItem(section: .history) ] return allItems.filter { item in !all.contains { $0.section == item.section } } } + + func listingStyle(_ item: FavoriteItem) -> WidgetListingStyle { + widgetSettings(item).listingStyle + } + + func limit(_ item: FavoriteItem) -> Int { + min(WidgetSettings.maxLimit(listingStyle(item)), widgetSettings(item).limit) + } + + func setListingStyle(_ style: WidgetListingStyle, _ item: FavoriteItem) { + if let index = widgetsSettings.firstIndex(where: { $0.id == item.widgetSettingsKey }) { + var settings = widgetsSettings[index] + settings.listingStyle = style + widgetsSettings[index] = settings + } else { + let settings = WidgetSettings(id: item.widgetSettingsKey, listingStyle: style) + widgetsSettings.append(settings) + } + } + + func setLimit(_ limit: Int, _ item: FavoriteItem) { + if let index = widgetsSettings.firstIndex(where: { $0.id == item.widgetSettingsKey }) { + var settings = widgetsSettings[index] + let limit = min(max(1, limit), WidgetSettings.maxLimit(settings.listingStyle)) + settings.limit = limit + widgetsSettings[index] = settings + } else { + var settings = WidgetSettings(id: item.widgetSettingsKey, limit: limit) + let limit = min(max(1, limit), WidgetSettings.maxLimit(settings.listingStyle)) + settings.limit = limit + widgetsSettings.append(settings) + } + } + + func widgetSettings(_ item: FavoriteItem) -> WidgetSettings { + widgetsSettings.first { $0.id == item.widgetSettingsKey } ?? WidgetSettings(id: item.widgetSettingsKey) + } } diff --git a/Model/FeedModel.swift b/Model/FeedModel.swift index f9b5324f..a3ceb0ad 100644 --- a/Model/FeedModel.swift +++ b/Model/FeedModel.swift @@ -223,6 +223,7 @@ final class FeedModel: ObservableObject, CacheModel { try? self.backgroundContext.save() self.calculateUnwatchedFeed() + WatchModel.shared.watchesChanged() } } diff --git a/Model/HistoryModel.swift b/Model/HistoryModel.swift index 4d651767..708ef9d2 100644 --- a/Model/HistoryModel.swift +++ b/Model/HistoryModel.swift @@ -10,18 +10,21 @@ extension PlayerModel { historyVideos.first { $0.videoID == id } } - func loadHistoryVideoDetails(_ watch: Watch) { + func loadHistoryVideoDetails(_ watch: Watch, onCompletion: @escaping () -> Void = {}) { guard historyVideo(watch.videoID).isNil else { + onCompletion() return } if !Video.VideoID.isValid(watch.videoID), let url = URL(string: watch.videoID) { historyVideos.append(.local(url)) + onCompletion() return } if let video = VideosCacheModel.shared.retrieveVideo(watch.video.cacheKey) { historyVideos.append(video) + onCompletion() return } @@ -35,6 +38,7 @@ extension PlayerModel { if let video: Video = response.typedContent() { VideosCacheModel.shared.storeVideo(video) self.historyVideos.append(video) + onCompletion() } } .onCompletion { _ in @@ -107,13 +111,19 @@ extension PlayerModel { try? self.context.save() FeedModel.shared.calculateUnwatchedFeed() + WatchModel.shared.watchesChanged() } } func removeAllWatches() { let watchesFetchRequest = NSFetchRequest(entityName: "Watch") let deleteRequest = NSBatchDeleteRequest(fetchRequest: watchesFetchRequest) - _ = try? context.execute(deleteRequest) - _ = try? context.save() + + do { + try context.executeAndMergeChanges(deleteRequest) + try context.save() + } catch let error as NSError { + logger.info(.init(stringLiteral: error.localizedDescription)) + } } } diff --git a/Model/NavigationModel.swift b/Model/NavigationModel.swift index 94a95d22..55bde054 100644 --- a/Model/NavigationModel.swift +++ b/Model/NavigationModel.swift @@ -83,6 +83,7 @@ final class NavigationModel: ObservableObject { @Published var presentingSettings = false @Published var presentingAccounts = false @Published var presentingWelcomeScreen = false + @Published var presentingHomeSettings = false @Published var presentingChannelSheet = false @Published var channelPresentedInSheet: Channel! diff --git a/Model/WatchModel.swift b/Model/WatchModel.swift new file mode 100644 index 00000000..cc2b2a07 --- /dev/null +++ b/Model/WatchModel.swift @@ -0,0 +1,11 @@ +import SwiftUI + +final class WatchModel: ObservableObject { + static let shared = WatchModel() + + @Published var historyToken = UUID() + + func watchesChanged() { + historyToken = UUID() + } +} diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index 82205744..cfe9b4d3 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -28,7 +28,6 @@ extension Defaults.Keys { static let showFavoritesInHome = Key("showFavoritesInHome", default: true) #if os(iOS) static let showDocuments = Key("showDocuments", default: false) - static let homeRecentDocumentsItems = Key("homeRecentDocumentsItems", default: 3) #endif static let homeHistoryItems = Key("homeHistoryItems", default: 10) static let favorites = Key<[FavoriteItem]>("favorites", default: []) @@ -258,6 +257,7 @@ extension Defaults.Keys { static let hideShorts = Key("hideShorts", default: false) static let hideWatched = Key("hideWatched", default: false) static let showInspector = Key("showInspector", default: .onlyLocal) + static let widgetsSettings = Key<[WidgetSettings]>("widgetsSettings", default: []) } enum ResolutionSetting: String, CaseIterable, Defaults.Serializable { @@ -445,3 +445,60 @@ enum FullScreenRotationSetting: String, CaseIterable, Defaults.Serializable { self != .disabled } } + +struct WidgetSettings: Defaults.Serializable { + static let defaultLimit = 10 + static let maxLimit: [WidgetListingStyle: Int] = [ + .horizontalCells: 50, + .list: 50 + ] + + static var bridge = WidgetSettingsBridge() + + var id: String + var listingStyle = WidgetListingStyle.horizontalCells + var limit = Self.defaultLimit + + var viewID: String { + "\(id)-\(listingStyle.rawValue)-\(limit)" + } + + static func maxLimit(_ style: WidgetListingStyle) -> Int { + Self.maxLimit[style] ?? Self.defaultLimit + } +} + +struct WidgetSettingsBridge: Defaults.Bridge { + typealias Value = WidgetSettings + typealias Serializable = [String: String] + + func serialize(_ value: Value?) -> Serializable? { + guard let value else { return nil } + + return [ + "id": value.id, + "listingStyle": value.listingStyle.rawValue, + "limit": String(value.limit) + ] + } + + func deserialize(_ object: Serializable?) -> Value? { + guard let object, let id = object["id"], !id.isEmpty else { return nil } + var listingStyle = WidgetListingStyle.horizontalCells + if let style = object["listingStyle"] { + listingStyle = WidgetListingStyle(rawValue: style) ?? .horizontalCells + } + let limit = Int(object["limit"] ?? "\(WidgetSettings.defaultLimit)") ?? WidgetSettings.defaultLimit + + return Value( + id: id, + listingStyle: listingStyle, + limit: limit + ) + } +} + +enum WidgetListingStyle: String, CaseIterable, Defaults.Serializable { + case horizontalCells + case list +} diff --git a/Shared/EnvironmentValues.swift b/Shared/EnvironmentValues.swift index 7509ef07..852223d2 100644 --- a/Shared/EnvironmentValues.swift +++ b/Shared/EnvironmentValues.swift @@ -62,10 +62,6 @@ private struct LoadMoreContentHandler: EnvironmentKey { static let defaultValue: LoadMoreContentHandlerType = {} } -private struct ScrollViewBottomPaddingKey: EnvironmentKey { - static let defaultValue: Double = 30 -} - extension EnvironmentValues { var inChannelView: Bool { get { self[InChannelViewKey.self] } @@ -97,11 +93,6 @@ extension EnvironmentValues { set { self[LoadMoreContentHandler.self] = newValue } } - var scrollViewBottomPadding: Double { - get { self[ScrollViewBottomPaddingKey.self] } - set { self[ScrollViewBottomPaddingKey.self] = newValue } - } - var listingStyle: ListingStyle { get { self[ListingStyleKey.self] } set { self[ListingStyleKey.self] = newValue } diff --git a/Shared/Home/FavoriteItemView.swift b/Shared/Home/FavoriteItemView.swift index 285c1b8e..07e0aac4 100644 --- a/Shared/Home/FavoriteItemView.swift +++ b/Shared/Home/FavoriteItemView.swift @@ -13,6 +13,16 @@ struct FavoriteItemView: View { private var playlists = PlaylistsModel.shared private var favoritesModel = FavoritesModel.shared private var navigation = NavigationModel.shared + @ObservedObject private var player = PlayerModel.shared + @ObservedObject private var watchModel = WatchModel.shared + + @FetchRequest(sortDescriptors: [.init(key: "watchedAt", ascending: false)]) + var watches: FetchedResults + @State private var visibleWatches = [Watch]() + + @Default(.hideShorts) private var hideShorts + @Default(.hideWatched) private var hideWatched + @Default(.widgetsSettings) private var widgetsSettings init(item: FavoriteItem) { self.item = item @@ -23,13 +33,7 @@ struct FavoriteItemView: View { if isVisible { VStack(alignment: .leading, spacing: 2) { itemControl - .contextMenu { - Button { - favoritesModel.remove(item) - } label: { - Label("Remove from Favorites", systemImage: "trash") - } - } + .contextMenu { contextMenu } .contentShape(Rectangle()) #if os(tvOS) .padding(.leading, 40) @@ -37,20 +41,173 @@ struct FavoriteItemView: View { .padding(.leading, 15) #endif - HorizontalCells(items: store.contentItems) + if limitedItems.isEmpty, !(resource?.isLoading ?? false) { + VStack(alignment: .leading) { + Text(emptyItemsText) + .frame(maxWidth: .infinity, alignment: .leading) + .foregroundColor(.secondary) + + if hideShorts || hideWatched { + AccentButton(text: "Disable filters", maxWidth: nil, verticalPadding: 0, minHeight: 30) { + hideShorts = false + hideWatched = false + reloadVisibleWatches() + } + } + } + .padding(.vertical, 10) + #if os(tvOS) + .padding(.horizontal, 40) + #else + .padding(.horizontal, 15) + #endif + } else { + Group { + switch widgetListingStyle { + case .horizontalCells: + HorizontalCells(items: limitedItems) + case .list: + ListView(items: limitedItems) + .padding(.vertical, 10) + #if os(tvOS) + .padding(.leading, 40) + #else + .padding(.leading, 15) + #endif + } + } .environment(\.inChannelView, inChannelView) + } } .contentShape(Rectangle()) + .onAppear { - resource?.addObserver(store) - loadCacheAndResource() + if item.section == .history { + reloadVisibleWatches() + } else { + resource?.addObserver(store) + loadCacheAndResource() + } } + .onChange(of: player.currentVideo) { _ in reloadVisibleWatches() } + .onChange(of: hideShorts) { _ in reloadVisibleWatches() } + .onChange(of: hideWatched) { _ in reloadVisibleWatches() } } } + .id(watchModel.historyToken) .onChange(of: accounts.current) { _ in resource?.addObserver(store) loadCacheAndResource(force: true) } + .onChange(of: watchModel.historyToken) { _ in + Delay.by(0.5) { + reloadVisibleWatches() + } + } + .onAppear { + Defaults.observe(.widgetsSettings) { _ in + watchModel.watchesChanged() + } + .tieToLifetime(of: accounts) + } + } + + var emptyItemsText: String { + var filterText = "" + if hideShorts && hideWatched { + filterText = "(watched and shorts hidden)" + } else if hideShorts { + filterText = "(shorts hidden)" + } else if hideWatched { + filterText = "(watched hidden)" + } + + return "No videos to show".localized() + " " + filterText.localized() + } + + var contextMenu: some View { + Group { + if item.section == .history { + Section { + Button { + navigation.presentAlert( + Alert( + title: Text("Are you sure you want to clear history of watched videos?"), + message: Text("This cannot be reverted"), + primaryButton: .destructive(Text("Clear All")) { + PlayerModel.shared.removeHistory() + visibleWatches = [] + }, + secondaryButton: .cancel() + ) + ) + } label: { + Label("Clear History", systemImage: "trash") + } + } + } + + Button { + favoritesModel.remove(item) + } label: { + Label("Remove from Favorites", systemImage: "trash") + } + + #if os(tvOS) + Button("Cancel", role: .cancel) {} + #endif + } + } + + func reloadVisibleWatches() { + guard item.section == .history else { return } + + visibleWatches = [] + + let watches = Array( + watches + .filter { $0.videoID != player.currentVideo?.videoID && itemVisible(.init(video: $0.video)) } + .prefix(favoritesModel.limit(item)) + ) + let last = watches.last + watches.forEach { watch in + player.loadHistoryVideoDetails(watch) { + guard let video = player.historyVideo(watch.videoID), itemVisible(.init(video: video)) else { return } + visibleWatches.append(watch) + guard watch == last else { return } + visibleWatches.sort { $0.watchedAt ?? Date() > $1.watchedAt ?? Date() } + } + } + } + + var limitedItems: [ContentItem] { + var items: [ContentItem] + if item.section == .history { + items = visibleWatches.map { ContentItem(video: player.historyVideo($0.videoID) ?? $0.video) } + } else { + items = store.contentItems.filter { itemVisible($0) } + } + return Array(items.prefix(favoritesModel.limit(item))) + } + + func itemVisible(_ item: ContentItem) -> Bool { + if hideWatched, watch(item)?.finished ?? false { + return false + } + + guard hideShorts, item.contentType == .video, let video = item.video else { + return true + } + + return !video.short + } + + func watch(_ item: ContentItem) -> Watch? { + watches.first { $0.videoID == item.video.videoID } + } + + var widgetListingStyle: WidgetListingStyle { + favoritesModel.listingStyle(item) } func loadCacheAndResource(force: Bool = false) { @@ -127,6 +284,10 @@ struct FavoriteItemView: View { } } + var navigatableItem: Bool { + item.section != .history + } + var inChannelView: Bool { switch item.section { case .channel: @@ -138,15 +299,20 @@ struct FavoriteItemView: View { var itemControl: some View { VStack { - #if os(tvOS) - itemButton - #else - if itemIsNavigationLink { - itemNavigationLink - } else { + if navigatableItem { + #if os(tvOS) itemButton - } - #endif + #else + if itemIsNavigationLink { + itemNavigationLink + } else { + itemButton + } + #endif + } else { + itemLabel + .foregroundColor(.secondary) + } } } @@ -220,6 +386,8 @@ struct FavoriteItemView: View { navigation.openSearchQuery(text) case let .playlist(_, id): navigation.tabSelection = .playlist(id) + case .history: + print("should not happen") } } @@ -227,8 +395,10 @@ struct FavoriteItemView: View { HStack { Text(label) .font(.title3.bold()) - Image(systemName: "chevron.right") - .imageScale(.small) + if navigatableItem { + Image(systemName: "chevron.right") + .imageScale(.small) + } } .lineLimit(1) .padding(.trailing, 10) @@ -255,6 +425,8 @@ struct FavoriteItemView: View { private var resource: Resource? { switch item.section { + case .history: + return nil case .subscriptions: if accounts.app.supportsSubscriptions { return accounts.api.feed(1) diff --git a/Shared/Home/HistoryView.swift b/Shared/Home/HistoryView.swift index 26efb06d..4daea5bc 100644 --- a/Shared/Home/HistoryView.swift +++ b/Shared/Home/HistoryView.swift @@ -24,25 +24,19 @@ struct HistoryView: View { }.foregroundColor(.secondary) } } else { - ForEach(visibleWatches, id: \.videoID) { watch in - let video = player.historyVideo(watch.videoID) ?? watch.video - - ContentItemView(item: .init(video: video)) - .environment(\.listingStyle, .list) - .contextMenu { - VideoContextMenuView(video: video) - } - } + ListView(items: contentItems, limit: limit) } } .animation(nil, value: visibleWatches) - .onAppear(perform: reloadVisibleWatches) .onChange(of: player.currentVideo) { _ in reloadVisibleWatches() } } + var contentItems: [ContentItem] { + visibleWatches.map { .init(video: player.historyVideo($0.videoID) ?? $0.video) } + } + func reloadVisibleWatches() { visibleWatches = Array(watches.filter { $0.videoID != player.currentVideo?.videoID }.prefix(limit)) - visibleWatches.forEach(player.loadHistoryVideoDetails) } } diff --git a/Shared/Home/HomeView.swift b/Shared/Home/HomeView.swift index 2db07c48..a61ab316 100644 --- a/Shared/Home/HomeView.swift +++ b/Shared/Home/HomeView.swift @@ -6,7 +6,7 @@ import UniformTypeIdentifiers struct HomeView: View { @ObservedObject private var accounts = AccountsModel.shared - @State private var presentingEditFavorites = false + @State private var presentingHomeSettings = false @State private var favoritesChanged = false @FetchRequest(sortDescriptors: [.init(key: "watchedAt", ascending: false)]) @@ -20,9 +20,7 @@ struct HomeView: View { #if !os(tvOS) @Default(.favorites) private var favorites - #endif - #if os(iOS) - @Default(.homeRecentDocumentsItems) private var homeRecentDocumentsItems + @Default(.widgetsSettings) private var widgetsSettings #endif @Default(.homeHistoryItems) private var homeHistoryItems @Default(.showFavoritesInHome) private var showFavoritesInHome @@ -33,33 +31,45 @@ struct HomeView: View { var body: some View { ScrollView(.vertical, showsIndicators: false) { - HStack { - #if os(tvOS) - Group { - if showOpenActionsInHome { - AccentButton(text: "Open Video", imageSystemName: "globe") { - NavigationModel.shared.presentingOpenVideos = true + VStack { + HStack { + #if os(tvOS) + Group { + if showOpenActionsInHome { + AccentButton(text: "Open Video", imageSystemName: "globe") { + NavigationModel.shared.presentingOpenVideos = true + } + } + AccentButton(text: "Locations", imageSystemName: "globe") { + NavigationModel.shared.presentingAccounts = true + } + + AccentButton(text: "Settings", imageSystemName: "gear") { + NavigationModel.shared.presentingSettings = true } } - AccentButton(text: "Locations", imageSystemName: "globe") { - NavigationModel.shared.presentingAccounts = true + #else + if showOpenActionsInHome { + AccentButton(text: "Files", imageSystemName: "folder") { + NavigationModel.shared.presentingFileImporter = true + } + AccentButton(text: "Paste", imageSystemName: "doc.on.clipboard.fill") { + OpenVideosModel.shared.openURLsFromClipboard(playbackMode: .playNow) + } + AccentButton(imageSystemName: "ellipsis") { + NavigationModel.shared.presentingOpenVideos = true + } + .frame(maxWidth: 40) } - AccentButton(text: "Settings", imageSystemName: "gear") { - NavigationModel.shared.presentingSettings = true - } - } - #else - if showOpenActionsInHome { - AccentButton(text: "Files", imageSystemName: "folder") { - NavigationModel.shared.presentingFileImporter = true - } - AccentButton(text: "Paste", imageSystemName: "doc.on.clipboard.fill") { - OpenVideosModel.shared.openURLsFromClipboard(playbackMode: .playNow) - } - AccentButton(imageSystemName: "ellipsis") { - NavigationModel.shared.presentingOpenVideos = true - } - .frame(maxWidth: 40) + #endif + } + + #if os(tvOS) + HStack { + Spacer() + HideWatchedButtons() + HideShortsButtons() + HomeSettingsButton() } #endif } @@ -80,7 +90,7 @@ struct HomeView: View { } if !accounts.current.isNil, showFavoritesInHome { - LazyVStack(alignment: .leading) { + VStack(alignment: .leading) { #if os(tvOS) ForEach(Defaults[.favorites]) { item in FavoriteItemView(item: item) @@ -96,87 +106,6 @@ struct HomeView: View { } } - #if os(iOS) - if homeRecentDocumentsItems > 0 { - VStack { - HStack { - NavigationLink(destination: DocumentsView()) { - HStack { - Text("Documents") - .font(.title3.bold()) - Image(systemName: "chevron.right") - .imageScale(.small) - } - .lineLimit(1) - } - .padding(.leading, 15) - - Spacer() - - Button { - recentDocumentsID = UUID() - } label: { - Label("Refresh", systemImage: "arrow.clockwise") - .font(.headline) - .labelStyle(.iconOnly) - .foregroundColor(.secondary) - } - } - - RecentDocumentsView(limit: homeRecentDocumentsItems) - .id(recentDocumentsID) - } - .frame(maxWidth: .infinity, alignment: .leading) - #if os(tvOS) - .padding(.trailing, 40) - #else - .padding(.trailing, 15) - #endif - } - #endif - - if homeHistoryItems > 0 { - VStack { - HStack { - sectionLabel("History") - Spacer() - Button { - navigation.presentAlert( - Alert( - title: Text("Are you sure you want to clear history of watched videos?"), - message: Text("This cannot be reverted"), - primaryButton: .destructive(Text("Clear All")) { - PlayerModel.shared.removeHistory() - historyID = UUID() - }, - secondaryButton: .cancel() - ) - ) - } label: { - Label("Clear History", systemImage: "trash") - .font(.headline) - .labelStyle(.iconOnly) - .foregroundColor(.secondary) - } - .buttonStyle(.plain) - } - .frame(maxWidth: .infinity, alignment: .leading) - #if os(tvOS) - .padding(.trailing, 40) - #else - .padding(.trailing, 15) - #endif - - HistoryView(limit: homeHistoryItems) - #if os(tvOS) - .padding(.horizontal, 40) - #else - .padding(.horizontal, 15) - #endif - .id(historyID) - } - } - #if !os(tvOS) Color.clear.padding(.bottom, 60) #endif @@ -186,6 +115,10 @@ struct HomeView: View { favoritesChanged.toggle() } .tieToLifetime(of: accounts) + Defaults.observe(.widgetsSettings) { _ in + favoritesChanged.toggle() + } + .tieToLifetime(of: accounts) } .redrawOn(change: favoritesChanged) @@ -198,6 +131,13 @@ struct HomeView: View { #if os(macOS) .background(Color.secondaryBackground) .frame(minWidth: 360) + .toolbar { + ToolbarItemGroup(placement: .automatic) { + HideWatchedButtons() + HideShortsButtons() + HomeSettingsButton() + } + } #endif #if os(iOS) .navigationBarTitleDisplayMode(.inline) @@ -226,13 +166,20 @@ struct HomeView: View { .foregroundColor(.secondary) } -#if os(iOS) + #if os(iOS) var homeMenu: some View { Menu { Section { HideWatchedButtons() HideShortsButtons() } + Section { + Button { + navigation.presentingHomeSettings = true + } label: { + Label("Home Settings", systemImage: "gear") + } + } } label: { HStack(spacing: 12) { Text("Home") diff --git a/Shared/Home/QueueView.swift b/Shared/Home/QueueView.swift index 3a3530b7..298126d2 100644 --- a/Shared/Home/QueueView.swift +++ b/Shared/Home/QueueView.swift @@ -28,15 +28,8 @@ struct QueueView: View { } .buttonStyle(.plain) - LazyVStack(alignment: .leading) { - ForEach(limitedItems) { item in - ContentItemView(item: .init(video: item.video)) - .environment(\.listingStyle, .list) - .environment(\.inQueueListing, true) - .environment(\.noListingDividers, limit == 1) - .transition(.opacity) - } - } + ListView(items: items, limit: limit) + .environment(\.inQueueListing, true) } } .padding(.vertical, items.isEmpty ? 0 : 15) @@ -50,16 +43,8 @@ struct QueueView: View { return "Next in Queue".localized() + " (\(items.count))" } - var limitedItems: [ContentItem] { - if let limit { - return Array(items.prefix(limit).map(\.contentItem)) - } - - return items.map(\.contentItem) - } - - var items: [PlayerQueueItem] { - player.queue + var items: [ContentItem] { + player.queue.map(\.contentItem) } var limit: Int? { diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index 89ef9649..f850f246 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -73,6 +73,37 @@ struct ContentView: View { AccountsView() } ) + .background( + EmptyView().sheet(isPresented: $navigation.presentingHomeSettings) { + #if os(macOS) + VStack(alignment: .leading) { + Button("Done") { + navigation.presentingHomeSettings = false + } + .padding() + .keyboardShortcut(.cancelAction) + + HomeSettings() + } + .frame(width: 500, height: 800) + #else + NavigationView { + HomeSettings() + #if os(iOS) + .toolbar { + ToolbarItem(placement: .navigation) { + Button { + navigation.presentingHomeSettings = false + } label: { + Text("Done") + } + } + } + #endif + } + #endif + } + ) #if !os(tvOS) .fileImporter( isPresented: $navigation.presentingFileImporter, diff --git a/Shared/Playlists/PlaylistsView.swift b/Shared/Playlists/PlaylistsView.swift index d71e484d..9f927ee1 100644 --- a/Shared/Playlists/PlaylistsView.swift +++ b/Shared/Playlists/PlaylistsView.swift @@ -64,7 +64,6 @@ struct PlaylistsView: View { SignInRequiredView(title: "Playlists".localized()) { VStack { VerticalCells(items: items, allowEmpty: true) { if shouldDisplayHeader { header } } - .environment(\.scrollViewBottomPadding, 70) .environment(\.currentPlaylistID, currentPlaylist?.id) .environment(\.listingStyle, playlistListingStyle) diff --git a/Shared/Settings/BrowsingSettings.swift b/Shared/Settings/BrowsingSettings.swift index 68bd4bb2..cabb140a 100644 --- a/Shared/Settings/BrowsingSettings.swift +++ b/Shared/Settings/BrowsingSettings.swift @@ -9,19 +9,13 @@ struct BrowsingSettings: View { @Default(.accountPickerDisplaysAnonymousAccounts) private var accountPickerDisplaysAnonymousAccounts @Default(.showUnwatchedFeedBadges) private var showUnwatchedFeedBadges #if os(iOS) - @Default(.homeRecentDocumentsItems) private var homeRecentDocumentsItems @Default(.lockPortraitWhenBrowsing) private var lockPortraitWhenBrowsing @Default(.showDocuments) private var showDocuments #endif @Default(.thumbnailsQuality) private var thumbnailsQuality @Default(.channelOnThumbnail) private var channelOnThumbnail @Default(.timeOnThumbnail) private var timeOnThumbnail - @Default(.showHome) private var showHome - @Default(.showFavoritesInHome) private var showFavoritesInHome - @Default(.showQueueInHome) private var showQueueInHome - @Default(.showOpenActionsInHome) private var showOpenActionsInHome @Default(.showOpenActionsToolbarItem) private var showOpenActionsToolbarItem - @Default(.homeHistoryItems) private var homeHistoryItems @Default(.visibleSections) private var visibleSections @Default(.playerButtonSingleTapGesture) private var playerButtonSingleTapGesture @Default(.playerButtonDoubleTapGesture) private var playerButtonDoubleTapGesture @@ -32,12 +26,11 @@ struct BrowsingSettings: View { @ObservedObject private var accounts = AccountsModel.shared - @State private var homeHistoryItemsText = "" #if os(iOS) @State private var homeRecentDocumentsItemsText = "" #endif #if os(macOS) - @State private var presentingEditFavoritesSheet = false + @State private var presentingHomeSettingsSheet = false #endif var body: some View { @@ -83,79 +76,32 @@ struct BrowsingSettings: View { } } - private var homeSettings: some View { - Section(header: SettingsHeader(text: "Home".localized())) { - #if !os(tvOS) - if !accounts.isEmpty { - Toggle("Show Home", isOn: $showHome) - } - #endif - Toggle("Show Open Videos quick actions", isOn: $showOpenActionsInHome) - Toggle("Show Next in Queue", isOn: $showQueueInHome) - - #if os(iOS) - HStack { - Text("Recent Documents") - TextField("Recent Documents", text: $homeRecentDocumentsItemsText) - .multilineTextAlignment(.trailing) - .labelsHidden() - #if !os(macOS) - .keyboardType(.numberPad) - #endif - .onAppear { - homeRecentDocumentsItemsText = String(homeRecentDocumentsItems) - } - .onChange(of: homeRecentDocumentsItemsText) { newValue in - homeRecentDocumentsItems = Int(newValue) ?? 3 - } - } - #endif - - HStack { - Text("Recent History") - TextField("Recent History", text: $homeHistoryItemsText) - .multilineTextAlignment(.trailing) - .labelsHidden() - #if !os(macOS) - .keyboardType(.numberPad) - #endif - .onAppear { - homeHistoryItemsText = String(homeHistoryItems) + @ViewBuilder private var homeSettings: some View { + if !accounts.isEmpty { + Section(header: SettingsHeader(text: "Home".localized())) { + #if os(macOS) + Button { + presentingHomeSettingsSheet = true + } label: { + Text("Home Settings") } - .onChange(of: homeHistoryItemsText) { newValue in - homeHistoryItems = Int(newValue) ?? 10 - } - } - - if !accounts.isEmpty { - Toggle("Show Favorites", isOn: $showFavoritesInHome) - - Group { - #if os(macOS) - Button { - presentingEditFavoritesSheet = true - } label: { - Text("Edit Favorites…") - } - .sheet(isPresented: $presentingEditFavoritesSheet) { - VStack(alignment: .leading) { - Button("Done") { - presentingEditFavoritesSheet = false - } - .padding() - .keyboardShortcut(.cancelAction) - - EditFavorites() + .sheet(isPresented: $presentingHomeSettingsSheet) { + VStack(alignment: .leading) { + Button("Done") { + presentingHomeSettingsSheet = false } - .frame(width: 500, height: 300) + .padding() + .keyboardShortcut(.cancelAction) + + HomeSettings() } - #else - NavigationLink(destination: LazyView(EditFavorites())) { - Text("Edit Favorites…") - } - #endif - } - .disabled(!showFavoritesInHome) + .frame(width: 500, height: 800) + } + #else + NavigationLink(destination: LazyView(HomeSettings())) { + Text("Home Settings") + } + #endif } } } diff --git a/Shared/Settings/EditFavorites.swift b/Shared/Settings/EditFavorites.swift deleted file mode 100644 index 05065467..00000000 --- a/Shared/Settings/EditFavorites.swift +++ /dev/null @@ -1,119 +0,0 @@ -import Defaults -import SwiftUI - -struct EditFavorites: View { - private var playlistsModel = PlaylistsModel.shared - private var model = FavoritesModel.shared - - @Default(.favorites) private var favorites - - var body: some View { - Group { - #if os(tvOS) - ScrollView { - VStack { - editor - } - } - .frame(width: 1000) - #else - List { - editor - } - #endif - } - .navigationTitle("Favorites") - } - - var editor: some View { - Group { - Section(header: Text("Favorites")) { - if favorites.isEmpty { - Text("Favorites is empty") - .foregroundColor(.secondary) - } - ForEach(favorites) { item in - HStack { - Text(label(item)) - - Spacer() - HStack(spacing: 30) { - Button { - model.moveUp(item) - } label: { - Label("Move Up", systemImage: "arrow.up") - } - - Button { - model.moveDown(item) - } label: { - Label("Move Down", systemImage: "arrow.down") - } - - Button { - model.remove(item) - } label: { - Label("Remove", systemImage: "trash") - } - } - #if !os(tvOS) - .buttonStyle(.borderless) - #endif - } - } - } - #if os(tvOS) - .padding(.trailing, 40) - #endif - - #if os(tvOS) - Divider() - .padding(20) - #endif - - if !model.addableItems().isEmpty { - Section(header: Text("Available")) { - ForEach(model.addableItems()) { item in - HStack { - Text(label(item)) - - Spacer() - - Button { - model.add(item) - } label: { - Label("Add to Favorites", systemImage: "heart") - #if os(tvOS) - .font(.system(size: 30)) - #endif - } - #if !os(tvOS) - .buttonStyle(.borderless) - #endif - } - } - } - #if os(tvOS) - .padding(.trailing, 40) - #endif - } - } - .labelStyle(.iconOnly) - } - - func label(_ item: FavoriteItem) -> String { - switch item.section { - case let .playlist(_, id): - return playlistsModel.find(id: id)?.title ?? "Playlist".localized() - default: - return item.section.label.localized() - } - } -} - -struct EditFavorites_Previews: PreviewProvider { - static var previews: some View { - EditFavorites() - .injectFixtureEnvironmentObjects() - } -} diff --git a/Shared/Settings/HomeSettings.swift b/Shared/Settings/HomeSettings.swift new file mode 100644 index 00000000..2903f684 --- /dev/null +++ b/Shared/Settings/HomeSettings.swift @@ -0,0 +1,302 @@ +import Defaults +import SwiftUI + +struct HomeSettings: View { + private var model = FavoritesModel.shared + + @Default(.favorites) private var favorites + @Default(.showHome) private var showHome + @Default(.showFavoritesInHome) private var showFavoritesInHome + @Default(.showQueueInHome) private var showQueueInHome + @Default(.showOpenActionsInHome) private var showOpenActionsInHome + @Default(.showOpenActionsToolbarItem) private var showOpenActionsToolbarItem + + @ObservedObject private var accounts = AccountsModel.shared + + var body: some View { + Group { + #if os(tvOS) + ScrollView { + LazyVStack { + homeSettings + .padding(.horizontal) + editor + } + } + .frame(width: 1000) + #else + List { + homeSettings + editor + } + #endif + } + .navigationTitle("Home Settings") + } + + var editor: some View { + Group { + Section(header: SettingsHeader(text: "Favorites")) { + if favorites.isEmpty { + Text("Favorites is empty") + .padding(.vertical) + .foregroundColor(.secondary) + } + ForEach(favorites) { item in + FavoriteItemEditor(item: item) + } + } + #if os(tvOS) + .padding(.trailing, 40) + #endif + + if !model.addableItems().isEmpty { + Section(header: SettingsHeader(text: "Available")) { + ForEach(model.addableItems()) { item in + HStack { + FavoriteItemLabel(item: item) + + Spacer() + + Button { + model.add(item) + } label: { + Label("Add to Favorites", systemImage: "heart") + #if os(tvOS) + .font(.system(size: 30)) + #endif + } + #if !os(tvOS) + .buttonStyle(.borderless) + #endif + } + } + } + #if os(tvOS) + .padding(.trailing, 40) + #endif + } + } + .labelStyle(.iconOnly) + } + + private var homeSettings: some View { + Section(header: SettingsHeader(text: "Home".localized())) { + #if !os(tvOS) + if !accounts.isEmpty { + Toggle("Show Home", isOn: $showHome) + } + #endif + Toggle("Show Open Videos quick actions", isOn: $showOpenActionsInHome) + Toggle("Show Next in Queue", isOn: $showQueueInHome) + + if !accounts.isEmpty { + Toggle("Show Favorites", isOn: $showFavoritesInHome) + } + } + } +} + +struct FavoriteItemLabel: View { + var item: FavoriteItem + var body: some View { + Text(label) + .fontWeight(.bold) + } + + var label: String { + switch item.section { + case let .playlist(_, id): + return PlaylistsModel.shared.find(id: id)?.title ?? "Playlist".localized() + default: + return item.section.label.localized() + } + } +} + +struct FavoriteItemEditor: View { + var item: FavoriteItem + + private var model: FavoritesModel { .shared } + + @State private var listingStyle = WidgetListingStyle.horizontalCells + @State private var limit = 3 + + @State private var presentingRemoveAlert = false + + var body: some View { + VStack(alignment: .leading) { + HStack { + FavoriteItemLabel(item: item) + + Spacer() + + HStack(spacing: 10) { + FavoriteItemEditorButton { + Label("Move Up", systemImage: "arrow.up") + } onTapGesture: { + model.moveUp(item) + } + + FavoriteItemEditorButton { + Label("Move Down", systemImage: "arrow.down") + } onTapGesture: { + model.moveDown(item) + } + + FavoriteItemEditorButton(color: .init("AppRedColor")) { + Label("Remove", systemImage: "trash") + } onTapGesture: { + presentingRemoveAlert = true + } + .alert(isPresented: $presentingRemoveAlert) { + Alert( + title: Text( + String( + format: "Are you sure you want to remove %@ from Favorites?".localized(), + item.section.label.localized() + ) + ), + message: Text("This cannot be reverted"), + primaryButton: .destructive(Text("Remove")) { + model.remove(item) + }, + secondaryButton: .cancel() + ) + } + } + } + + listingStylePicker + .padding(.vertical, 5) + + limitInput + + #if !os(iOS) + Divider() + #endif + } + .onAppear(perform: setupEditor) + #if !os(tvOS) + .buttonStyle(.borderless) + #endif + } + + var listingStylePicker: some View { + Picker("Listing Style", selection: $listingStyle) { + Text("Cells").tag(WidgetListingStyle.horizontalCells) + Text("List").tag(WidgetListingStyle.list) + } + .onChange(of: listingStyle) { newValue in + model.setListingStyle(newValue, item) + limit = min(limit, WidgetSettings.maxLimit(newValue)) + } + .labelsHidden() + .pickerStyle(.segmented) + } + + var limitInput: some View { + HStack { + Text("Limit") + Spacer() + + #if !os(tvOS) + limitMinusButton + .disabled(limit == 1) + #endif + + #if os(tvOS) + let textFieldWidth = 100.00 + #else + let textFieldWidth = 30.00 + #endif + + TextField("Limit", value: $limit, formatter: NumberFormatter()) + #if !os(macOS) + .keyboardType(.numberPad) + #endif + .labelsHidden() + .frame(width: textFieldWidth, alignment: .trailing) + .multilineTextAlignment(.center) + .onChange(of: limit) { newValue in + let value = min(limit, WidgetSettings.maxLimit(listingStyle)) + if newValue <= 0 || newValue != value { + limit = value + } else { + model.setLimit(value, item) + } + } + #if !os(tvOS) + limitPlusButton + .disabled(limit == WidgetSettings.maxLimit(listingStyle)) + #endif + } + } + + #if !os(tvOS) + var limitMinusButton: some View { + FavoriteItemEditorButton { + Label("Minus", systemImage: "minus") + } onTapGesture: { + limit = max(1, limit - 1) + } + } + + var limitPlusButton: some View { + FavoriteItemEditorButton { + Label("Plus", systemImage: "plus") + } onTapGesture: { + limit = max(1, limit + 1) + } + } + #endif + + func setupEditor() { + listingStyle = model.listingStyle(item) + limit = model.limit(item) + } +} + +struct FavoriteItemEditorButton: View { + var color = Color.accentColor + var label: LabelView + var onTapGesture: () -> Void = {} + + init( + color: Color = .accentColor, + @ViewBuilder label: () -> LabelView, + onTapGesture: @escaping () -> Void = {} + ) { + self.color = color + self.label = label() + self.onTapGesture = onTapGesture + } + + var body: some View { + #if os(tvOS) + Button(action: onTapGesture) { + label + } + #else + label + .imageScale(.medium) + .labelStyle(.iconOnly) + .padding(7) + .frame(minWidth: 40, minHeight: 40) + .foregroundColor(color) + .accessibilityAddTraits(.isButton) + #if os(iOS) + .background(RoundedRectangle(cornerRadius: 4).strokeBorder(lineWidth: 1).foregroundColor(color)) + #endif + .contentShape(Rectangle()) + .onTapGesture(perform: onTapGesture) + #endif + } +} + +struct HomeSettings_Previews: PreviewProvider { + static var previews: some View { + HomeSettings() + .injectFixtureEnvironmentObjects() + } +} diff --git a/Shared/Videos/ListView.swift b/Shared/Videos/ListView.swift new file mode 100644 index 00000000..21797e03 --- /dev/null +++ b/Shared/Videos/ListView.swift @@ -0,0 +1,32 @@ + +import SwiftUI + +struct ListView: View { + var items: [ContentItem] + var limit: Int? = 10 + + var body: some View { + LazyVStack(alignment: .leading) { + ForEach(limitedItems) { item in + ContentItemView(item: item) + .environment(\.listingStyle, .list) + .environment(\.noListingDividers, limit == 1) + .transition(.opacity) + } + } + } + + var limitedItems: [ContentItem] { + if let limit, limit >= 0 { + return Array(items.prefix(limit)) + } + + return items + } +} + +struct ListView_Previews: PreviewProvider { + static var previews: some View { + ListView(items: [.init(video: .fixture)]) + } +} diff --git a/Shared/Videos/VerticalCells.swift b/Shared/Videos/VerticalCells.swift index 77cb6cdd..9bf6f207 100644 --- a/Shared/Videos/VerticalCells.swift +++ b/Shared/Videos/VerticalCells.swift @@ -6,7 +6,6 @@ struct VerticalCells: View { @Environment(\.verticalSizeClass) private var verticalSizeClass #endif - @Environment(\.scrollViewBottomPadding) private var scrollViewBottomPadding @Environment(\.loadMoreContentHandler) private var loadMoreContentHandler @Environment(\.listingStyle) private var listingStyle @@ -15,14 +14,25 @@ struct VerticalCells: View { var edgesIgnoringSafeArea = Edge.Set.horizontal let header: Header? - init(items: [ContentItem], allowEmpty: Bool = false, edgesIgnoringSafeArea: Edge.Set = .horizontal, @ViewBuilder header: @escaping () -> Header? = { nil }) { + + @State private var gridSize = CGSize.zero + + init( + items: [ContentItem], + allowEmpty: Bool = false, + edgesIgnoringSafeArea: Edge.Set = .horizontal, + @ViewBuilder header: @escaping () -> Header? = { nil } + ) { self.items = items self.allowEmpty = allowEmpty self.edgesIgnoringSafeArea = edgesIgnoringSafeArea self.header = header() } - init(items: [ContentItem], allowEmpty: Bool = false) where Header == EmptyView { + init( + items: [ContentItem], + allowEmpty: Bool = false + ) where Header == EmptyView { self.init(items: items, allowEmpty: allowEmpty) { EmptyView() } } @@ -37,9 +47,6 @@ struct VerticalCells: View { } } .padding() - #if !os(tvOS) - Color.clear.padding(.bottom, scrollViewBottomPadding) - #endif } .animation(nil) .edgesIgnoringSafeArea(edgesIgnoringSafeArea) diff --git a/Shared/Videos/WatchView.swift b/Shared/Videos/WatchView.swift index a1261562..5d735b22 100644 --- a/Shared/Videos/WatchView.swift +++ b/Shared/Videos/WatchView.swift @@ -53,6 +53,7 @@ struct WatchView: View { } FeedModel.shared.calculateUnwatchedFeed() + WatchModel.shared.watchesChanged() } var imageSystemName: String { diff --git a/Shared/Views/AccentButton.swift b/Shared/Views/AccentButton.swift index bd3696e2..2bac94c2 100644 --- a/Shared/Views/AccentButton.swift +++ b/Shared/Views/AccentButton.swift @@ -5,6 +5,9 @@ struct AccentButton: View { var imageSystemName: String? var maxWidth: CGFloat? = .infinity var bold = true + var verticalPadding = 10.0 + var horizontalPadding = 10.0 + var minHeight = 45.0 var action: () -> Void = {} var body: some View { @@ -18,9 +21,9 @@ struct AccentButton: View { .fontWeight(bold ? .bold : .regular) } } - .padding(.vertical, 10) - .padding(.horizontal, 10) - .frame(minHeight: 45) + .padding(.vertical, verticalPadding) + .padding(.horizontal, horizontalPadding) + .frame(minHeight: minHeight) .frame(maxWidth: maxWidth) .contentShape(Rectangle()) } diff --git a/Shared/Views/HomeSettingsButton.swift b/Shared/Views/HomeSettingsButton.swift new file mode 100644 index 00000000..4e6e0c8b --- /dev/null +++ b/Shared/Views/HomeSettingsButton.swift @@ -0,0 +1,21 @@ +import SwiftUI + +struct HomeSettingsButton: View { + var navigation = NavigationModel.shared + + var body: some View { + Button { + navigation.presentingHomeSettings = true + } label: { + Label("Home Settings", systemImage: "gear") + } + .font(.caption) + .imageScale(.small) + } +} + +struct HomeSettingsButton_Previews: PreviewProvider { + static var previews: some View { + HomeSettingsButton() + } +} diff --git a/Shared/Views/VideoContextMenuView.swift b/Shared/Views/VideoContextMenuView.swift index 52bbc601..c65efedc 100644 --- a/Shared/Views/VideoContextMenuView.swift +++ b/Shared/Views/VideoContextMenuView.swift @@ -160,6 +160,7 @@ struct VideoContextMenuView: View { Button { Watch.markAsWatched(videoID: video.videoID, account: accounts.current, duration: video.length, context: backgroundContext) FeedModel.shared.calculateUnwatchedFeed() + WatchModel.shared.watchesChanged() } label: { Label("Mark as watched", systemImage: "checkmark.circle.fill") } diff --git a/Shared/YatteeApp.swift b/Shared/YatteeApp.swift index 18d3dd10..48dc409c 100644 --- a/Shared/YatteeApp.swift +++ b/Shared/YatteeApp.swift @@ -47,6 +47,7 @@ struct YatteeApp: App { let persistenceController = PersistenceController.shared + var favorites: FavoritesModel { .shared } var playerControls: PlayerControlsModel { .shared } var body: some Scene { @@ -180,5 +181,22 @@ struct YatteeApp: App { #endif URLBookmarkModel.shared.refreshAll() + + migrateHomeHistoryItems() + } + + func migrateHomeHistoryItems() { + guard Defaults[.homeHistoryItems] > 0 else { return } + + if favorites.addableItems().contains(where: { $0.section == .history }) { + let historyItem = FavoriteItem(section: .history) + favorites.add(historyItem) + favorites.setListingStyle(.list, historyItem) + favorites.setLimit(Defaults[.homeHistoryItems], historyItem) + + print("migrated home history items: \(favorites.limit(historyItem))") + } + + Defaults[.homeHistoryItems] = -1 } } diff --git a/Yattee.xcodeproj/project.pbxproj b/Yattee.xcodeproj/project.pbxproj index 087c17d6..44d8a4c7 100644 --- a/Yattee.xcodeproj/project.pbxproj +++ b/Yattee.xcodeproj/project.pbxproj @@ -83,6 +83,9 @@ 37095E8D291DD5DA00301883 /* URLBookmarkModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F5E8B5291BE9D0006C15F5 /* URLBookmarkModel.swift */; }; 370B79C9286279810045DB77 /* NSObject+Swizzle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370B79C8286279810045DB77 /* NSObject+Swizzle.swift */; }; 370B79CC286279BA0045DB77 /* UIViewController+HideHomeIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370B79CB286279BA0045DB77 /* UIViewController+HideHomeIndicator.swift */; }; + 370E990A2A1EA8C500D144E9 /* WatchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370E99092A1EA8C500D144E9 /* WatchModel.swift */; }; + 370E990B2A1EA8C500D144E9 /* WatchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370E99092A1EA8C500D144E9 /* WatchModel.swift */; }; + 370E990C2A1EA8C600D144E9 /* WatchModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 370E99092A1EA8C500D144E9 /* WatchModel.swift */; }; 370F4FA927CC163A001B35DC /* PlayerBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EBD8C327AF0DA800F1C24B /* PlayerBackend.swift */; }; 370F4FAA27CC163B001B35DC /* PlayerBackend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EBD8C327AF0DA800F1C24B /* PlayerBackend.swift */; }; 370F4FAB27CC164D001B35DC /* PlayerControlsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375E45F727B1AC4700BA7902 /* PlayerControlsModel.swift */; }; @@ -304,8 +307,8 @@ 373CFAEF2697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */; }; 373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */; }; 373CFAF12697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */; }; - 373EBD68291F1EAF002ADB9C /* EditFavorites.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FADFFF272ED58000330459 /* EditFavorites.swift */; }; - 373EBD69291F252D002ADB9C /* EditFavorites.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FADFFF272ED58000330459 /* EditFavorites.swift */; }; + 373EBD68291F1EAF002ADB9C /* HomeSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FADFFF272ED58000330459 /* HomeSettings.swift */; }; + 373EBD69291F252D002ADB9C /* HomeSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FADFFF272ED58000330459 /* HomeSettings.swift */; }; 3741A32C27E7EFFD00D266D1 /* PlayerControls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37030FFE27B04DCC00ECDDAA /* PlayerControls.swift */; }; 3743B86927216D3600261544 /* ChannelCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3743B86727216D3600261544 /* ChannelCell.swift */; }; 3743B86A27216D3600261544 /* ChannelCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3743B86727216D3600261544 /* ChannelCell.swift */; }; @@ -670,6 +673,14 @@ 379775942689365600DD52A8 /* Array+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379775922689365600DD52A8 /* Array+Next.swift */; }; 379775952689365600DD52A8 /* Array+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379775922689365600DD52A8 /* Array+Next.swift */; }; 3799AC0928B03CED001376F9 /* ActiveLabel in Frameworks */ = {isa = PBXBuildFile; productRef = 3799AC0828B03CED001376F9 /* ActiveLabel */; }; + 379ACB4C2A1F8A4100E01914 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379ACB4B2A1F8A4100E01914 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift */; }; + 379ACB4D2A1F8A4100E01914 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379ACB4B2A1F8A4100E01914 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift */; }; + 379ACB4E2A1F8A4100E01914 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379ACB4B2A1F8A4100E01914 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift */; }; + 379ACB4F2A1F8A4100E01914 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379ACB4B2A1F8A4100E01914 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift */; }; + 379ACB512A1F8DB000E01914 /* HomeSettingsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379ACB502A1F8DB000E01914 /* HomeSettingsButton.swift */; }; + 379ACB522A1F8DB000E01914 /* HomeSettingsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379ACB502A1F8DB000E01914 /* HomeSettingsButton.swift */; }; + 379ACB532A1F8DB000E01914 /* HomeSettingsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379ACB502A1F8DB000E01914 /* HomeSettingsButton.swift */; }; + 379ACB542A1F8DB000E01914 /* HomeSettingsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379ACB502A1F8DB000E01914 /* HomeSettingsButton.swift */; }; 379B0253287A1CDF001015B5 /* OrientationTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379B0252287A1CDF001015B5 /* OrientationTracker.swift */; }; 379C0F49291DA5AB00256D07 /* FavoriteButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37599F37272B4D740087F250 /* FavoriteButton.swift */; }; 379DC3D128BA4EB400B09677 /* Seek.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379DC3D028BA4EB400B09677 /* Seek.swift */; }; @@ -988,6 +999,9 @@ 37F4AE7226828F0900BD60EA /* VerticalCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AE7126828F0900BD60EA /* VerticalCells.swift */; }; 37F4AE7326828F0900BD60EA /* VerticalCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AE7126828F0900BD60EA /* VerticalCells.swift */; }; 37F4AE7426828F0900BD60EA /* VerticalCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F4AE7126828F0900BD60EA /* VerticalCells.swift */; }; + 37F5C7E02A1E2AF300927B73 /* ListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F5C7DF2A1E2AF300927B73 /* ListView.swift */; }; + 37F5C7E12A1E2AF300927B73 /* ListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F5C7DF2A1E2AF300927B73 /* ListView.swift */; }; + 37F5C7E22A1E2AF300927B73 /* ListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F5C7DF2A1E2AF300927B73 /* ListView.swift */; }; 37F5E8B6291BE9D0006C15F5 /* URLBookmarkModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F5E8B5291BE9D0006C15F5 /* URLBookmarkModel.swift */; }; 37F5E8B7291BE9D0006C15F5 /* URLBookmarkModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F5E8B5291BE9D0006C15F5 /* URLBookmarkModel.swift */; }; 37F5E8B8291BE9D0006C15F5 /* URLBookmarkModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F5E8B5291BE9D0006C15F5 /* URLBookmarkModel.swift */; }; @@ -1006,7 +1020,7 @@ 37F9619F27BD90BB00058149 /* PlayerBackendType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F9619E27BD90BB00058149 /* PlayerBackendType.swift */; }; 37F961A027BD90BB00058149 /* PlayerBackendType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F9619E27BD90BB00058149 /* PlayerBackendType.swift */; }; 37F961A127BD90BB00058149 /* PlayerBackendType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F9619E27BD90BB00058149 /* PlayerBackendType.swift */; }; - 37FAE000272ED58000330459 /* EditFavorites.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FADFFF272ED58000330459 /* EditFavorites.swift */; }; + 37FAE000272ED58000330459 /* HomeSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FADFFF272ED58000330459 /* HomeSettings.swift */; }; 37FB28412721B22200A57617 /* ContentItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FB28402721B22200A57617 /* ContentItem.swift */; }; 37FB28422721B22200A57617 /* ContentItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FB28402721B22200A57617 /* ContentItem.swift */; }; 37FB28432721B22200A57617 /* ContentItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FB28402721B22200A57617 /* ContentItem.swift */; }; @@ -1146,6 +1160,7 @@ 370B79C8286279810045DB77 /* NSObject+Swizzle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSObject+Swizzle.swift"; sourceTree = ""; }; 370B79CB286279BA0045DB77 /* UIViewController+HideHomeIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+HideHomeIndicator.swift"; sourceTree = ""; }; 370D5E4F292423F400D053A6 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; + 370E99092A1EA8C500D144E9 /* WatchModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchModel.swift; sourceTree = ""; }; 370F4FAE27CC16CA001B35DC /* libssl.3.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; path = libssl.3.dylib; sourceTree = ""; }; 370F4FAF27CC16CA001B35DC /* libXau.6.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; path = libXau.6.dylib; sourceTree = ""; }; 370F4FB027CC16CA001B35DC /* libxcb.1.1.0.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; path = libxcb.1.1.0.dylib; sourceTree = ""; }; @@ -1367,6 +1382,8 @@ 3797758A2689345500DD52A8 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = ""; }; 379775922689365600DD52A8 /* Array+Next.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Next.swift"; sourceTree = ""; }; 37992DC726CC50BC003D4C27 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 379ACB4B2A1F8A4100E01914 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+ExecuteAndMergeChanges.swift"; sourceTree = ""; }; + 379ACB502A1F8DB000E01914 /* HomeSettingsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeSettingsButton.swift; sourceTree = ""; }; 379B0252287A1CDF001015B5 /* OrientationTracker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OrientationTracker.swift; sourceTree = ""; }; 379DC3D028BA4EB400B09677 /* Seek.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Seek.swift; sourceTree = ""; }; 379EF9DF29AA585F009FE6C6 /* HideShortsButtons.swift */ = {isa = PBXFileReference; indentWidth = 3; lastKnownFileType = sourcecode.swift; path = HideShortsButtons.swift; sourceTree = ""; }; @@ -1511,6 +1528,7 @@ 37F4AD1E28612DFD004D0F66 /* Buffering.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Buffering.swift; sourceTree = ""; }; 37F4AD2528613B81004D0F66 /* Color+Debug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Debug.swift"; sourceTree = ""; }; 37F4AE7126828F0900BD60EA /* VerticalCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalCells.swift; sourceTree = ""; }; + 37F5C7DF2A1E2AF300927B73 /* ListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListView.swift; sourceTree = ""; }; 37F5E8B5291BE9D0006C15F5 /* URLBookmarkModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLBookmarkModel.swift; sourceTree = ""; }; 37F5E8B9291BEF69006C15F5 /* BaseCacheModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseCacheModel.swift; sourceTree = ""; }; 37F64FE326FE70A60081B69E /* RedrawOnModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedrawOnModifier.swift; sourceTree = ""; }; @@ -1519,7 +1537,7 @@ 37F7AB5428A951B200FB46B5 /* Power.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Power.swift; sourceTree = ""; }; 37F7D82B289EB05F00E2B3D0 /* SettingsPickerModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsPickerModifier.swift; sourceTree = ""; }; 37F9619E27BD90BB00058149 /* PlayerBackendType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerBackendType.swift; sourceTree = ""; }; - 37FADFFF272ED58000330459 /* EditFavorites.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditFavorites.swift; sourceTree = ""; }; + 37FADFFF272ED58000330459 /* HomeSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeSettings.swift; sourceTree = ""; }; 37FB28402721B22200A57617 /* ContentItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentItem.swift; sourceTree = ""; }; 37FB285D272225E800A57617 /* ContentItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentItemView.swift; sourceTree = ""; }; 37FD43DB270470B70073EE42 /* InstancesSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesSettings.swift; sourceTree = ""; }; @@ -1860,6 +1878,7 @@ isa = PBXGroup; children = ( 37A9965926D6F8CA006E3224 /* HorizontalCells.swift */, + 37F5C7DF2A1E2AF300927B73 /* ListView.swift */, 378E9C37294552A700B2D696 /* ThumbnailView.swift */, 37F4AE7126828F0900BD60EA /* VerticalCells.swift */, 37CC3F4F270D010D00608308 /* VideoBanner.swift */, @@ -1881,6 +1900,7 @@ 37599F37272B4D740087F250 /* FavoriteButton.swift */, 379EF9DF29AA585F009FE6C6 /* HideShortsButtons.swift */, 37758C0A2A1D1C8B001FD900 /* HideWatchedButtons.swift */, + 379ACB502A1F8DB000E01914 /* HomeSettingsButton.swift */, 37152EE926EFEB95004FB96D /* LazyView.swift */, 371CC7732946963000979C1A /* ListingStyleButtons.swift */, 37030FF627B0347C00ECDDAA /* MPVPlayerView.swift */, @@ -2011,20 +2031,20 @@ 37732FEF2703A26300F04329 /* AccountValidationStatus.swift */, 37F0F4ED286F734400C06C2E /* AdvancedSettings.swift */, 376BE50A27349108009AD608 /* BrowsingSettings.swift */, - 37FADFFF272ED58000330459 /* EditFavorites.swift */, 37579D5C27864F5F00FD0B98 /* Help.swift */, 37BC50A72778A84700510953 /* HistorySettings.swift */, + 37FADFFF272ED58000330459 /* HomeSettings.swift */, 37484C2426FC83E000287258 /* InstanceForm.swift */, 37484C2C26FC844700287258 /* InstanceSettings.swift */, 374924D92921050B0017D862 /* LocationsSettings.swift */, + 375EC971289F2ABF00751258 /* MultiselectRow.swift */, 37D9BA0529507F69002586BD /* PlayerControlsSettings.swift */, 37484C1826FC837400287258 /* PlayerSettings.swift */, - 374C053427242D9F009BDDBE /* SponsorBlockSettings.swift */, + 375EC958289EEB8200751258 /* QualityProfileForm.swift */, + 379F141E289ECE7F00DE48B5 /* QualitySettings.swift */, 376BE50627347B57009AD608 /* SettingsHeader.swift */, 37B044B626F7AB9000E1419D /* SettingsView.swift */, - 379F141E289ECE7F00DE48B5 /* QualitySettings.swift */, - 375EC958289EEB8200751258 /* QualityProfileForm.swift */, - 375EC971289F2ABF00751258 /* MultiselectRow.swift */, + 374C053427242D9F009BDDBE /* SponsorBlockSettings.swift */, ); path = Settings; sourceTree = ""; @@ -2254,6 +2274,7 @@ 37E8B0EF27B326F30024006F /* Comparable+Clamped.swift */, 37C3A240272359900087A57A /* Double+Format.swift */, 37BA794E26DC3E0E002A0235 /* Int+Format.swift */, + 379ACB4B2A1F8A4100E01914 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift */, 370B79C8286279810045DB77 /* NSObject+Swizzle.swift */, 3782B95C2755858100990149 /* NSTextField+FocusRingType.swift */, 377ABC47286E5887009C986F /* Sequence+Unique.swift */, @@ -2397,8 +2418,7 @@ 37FB28402721B22200A57617 /* ContentItem.swift */, 37141672267A8E10006CA35D /* Country.swift */, 37494EA629200E0B000DF176 /* DocumentsModel.swift */, - 37599F2F272B42810087F250 /* FavoriteItem.swift */, - 37599F33272B44000087F250 /* FavoritesModel.swift */, + 37F032CA2A1D5C9F00A7DAE7 /* Favorites */, 37E6D79B2944AE1A00550C3D /* FeedModel.swift */, 37BC50AB2778BCBA00510953 /* HistoryModel.swift */, 377ABC3F286E4AD5009C986F /* InstancesManifest.swift */, @@ -2432,6 +2452,7 @@ 37F5E8B5291BE9D0006C15F5 /* URLBookmarkModel.swift */, 37D4B19626717E1500C925CA /* Video.swift */, 3784CDDE27772EE40055BBF2 /* Watch.swift */, + 370E99092A1EA8C500D144E9 /* WatchModel.swift */, 37130A59277657090033018A /* Yattee.xcdatamodeld */, ); path = Model; @@ -2498,6 +2519,15 @@ path = Backends; sourceTree = ""; }; + 37F032CA2A1D5C9F00A7DAE7 /* Favorites */ = { + isa = PBXGroup; + children = ( + 37599F2F272B42810087F250 /* FavoriteItem.swift */, + 37599F33272B44000087F250 /* FavoritesModel.swift */, + ); + path = Favorites; + sourceTree = ""; + }; 37FB283F2721B20800A57617 /* Search */ = { isa = PBXGroup; children = ( @@ -3137,6 +3167,7 @@ 37EBD8C427AF0DA800F1C24B /* PlayerBackend.swift in Sources */, 376A33E02720CAD6000C1D6B /* VideosApp.swift in Sources */, 37758C0B2A1D1C8B001FD900 /* HideWatchedButtons.swift in Sources */, + 379ACB4C2A1F8A4100E01914 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift in Sources */, 374AB3DB28BCAF7E00DF56FB /* SeekType.swift in Sources */, 37192D5728B179D60012EEDD /* ChaptersView.swift in Sources */, 37D836BC294927E700005E5E /* ChannelsCacheModel.swift in Sources */, @@ -3162,6 +3193,7 @@ 3748186626A7627F0084E870 /* Video+Fixtures.swift in Sources */, 37599F34272B44000087F250 /* FavoritesModel.swift in Sources */, 3717407D2949D40800FDDBC7 /* ChannelLinkView.swift in Sources */, + 379ACB512A1F8DB000E01914 /* HomeSettingsButton.swift in Sources */, 37030FF727B0347C00ECDDAA /* MPVPlayerView.swift in Sources */, 37BA794726DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */, 377FC7DC267A081800A6BBAF /* PopularView.swift in Sources */, @@ -3203,7 +3235,7 @@ 3700155B271B0D4D0049C794 /* PipedAPI.swift in Sources */, 373031F32838388A000CFD59 /* PlayerLayerView.swift in Sources */, 376E331228AD3B320070E30C /* ScrollDismissesKeyboard+Backport.swift in Sources */, - 373EBD68291F1EAF002ADB9C /* EditFavorites.swift in Sources */, + 373EBD68291F1EAF002ADB9C /* HomeSettings.swift in Sources */, 371CC76829466ED000979C1A /* AccountsView.swift in Sources */, 37B044B726F7AB9000E1419D /* SettingsView.swift in Sources */, 377FC7E3267A084A00A6BBAF /* VideoCell.swift in Sources */, @@ -3219,6 +3251,7 @@ 375B8AB328B580D300397B31 /* KeychainModel.swift in Sources */, 37BC50AC2778BCBA00510953 /* HistoryModel.swift in Sources */, 37AAF29026740715007FC770 /* Channel.swift in Sources */, + 37F5C7E02A1E2AF300927B73 /* ListView.swift in Sources */, 37DD9DBA2785D60300539416 /* FramePreferenceKey.swift in Sources */, 3748186A26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */, 37B81AFF26D2CA3700675966 /* VideoDetails.swift in Sources */, @@ -3302,6 +3335,7 @@ 37F4AD1B28612B23004D0F66 /* OpeningStream.swift in Sources */, 373CFAEB26975CBF003CB2C6 /* PlaylistFormView.swift in Sources */, 372915E62687E3B900F5A35B /* Defaults.swift in Sources */, + 370E990A2A1EA8C500D144E9 /* WatchModel.swift in Sources */, 371AC0B6294D1D6E0085989E /* PlayingIndicatorView.swift in Sources */, 37E084AC2753D95F00039B7D /* AccountsNavigationLink.swift in Sources */, 378E9C38294552A700B2D696 /* ThumbnailView.swift in Sources */, @@ -3361,6 +3395,7 @@ 37B7CFEC2A197844001B0564 /* AppleAVPlayerView.swift in Sources */, 37F0F4EB286F397E00C06C2E /* SettingsModel.swift in Sources */, 378AE93F274EDFB5006A4EE1 /* Tint+Backport.swift in Sources */, + 379ACB4D2A1F8A4100E01914 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift in Sources */, 37C194C826F6A9C8005D3B96 /* RecentsModel.swift in Sources */, 37737786276F9858000521C1 /* Windows.swift in Sources */, 3786D05F294C737300D23E82 /* RequestErrorButton.swift in Sources */, @@ -3405,6 +3440,7 @@ 377FC7DD267A081A00A6BBAF /* PopularView.swift in Sources */, 374924DB2921050B0017D862 /* LocationsSettings.swift in Sources */, 371AC0A0294D13AA0085989E /* UnwatchedFeedCountModel.swift in Sources */, + 37F5C7E12A1E2AF300927B73 /* ListView.swift in Sources */, 37192D5828B179D60012EEDD /* ChaptersView.swift in Sources */, 3784CDE327772EE40055BBF2 /* Watch.swift in Sources */, 371AC0B7294D1D6E0085989E /* PlayingIndicatorView.swift in Sources */, @@ -3426,7 +3462,7 @@ 37CC3F4D270CFE1700608308 /* PlayerQueueView.swift in Sources */, 37B81B0026D2CA3700675966 /* VideoDetails.swift in Sources */, 3752069A285E8DD300CA655F /* Chapter.swift in Sources */, - 373EBD69291F252D002ADB9C /* EditFavorites.swift in Sources */, + 373EBD69291F252D002ADB9C /* HomeSettings.swift in Sources */, 37B7CFEE2A19789F001B0564 /* MacOSPiPDelegate.swift in Sources */, 37484C1A26FC837400287258 /* PlayerSettings.swift in Sources */, 3776925329463C310055EC18 /* PlaylistsCacheModel.swift in Sources */, @@ -3522,6 +3558,7 @@ 37B81AFD26D2C9C900675966 /* VideoDetailsPaddingModifier.swift in Sources */, 37C8E702294FC97D00EEAB14 /* QueueView.swift in Sources */, 37C0697F2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */, + 379ACB522A1F8DB000E01914 /* HomeSettingsButton.swift in Sources */, 37A9965B26D6F8CA006E3224 /* HorizontalCells.swift in Sources */, 37E6D79D2944AE1A00550C3D /* FeedModel.swift in Sources */, 37732FF52703D32400F04329 /* Sidebar.swift in Sources */, @@ -3590,6 +3627,7 @@ 378E9C39294552A700B2D696 /* ThumbnailView.swift in Sources */, 370F4FAB27CC164D001B35DC /* PlayerControlsModel.swift in Sources */, 37E8B0ED27B326C00024006F /* TimelineView.swift in Sources */, + 370E990B2A1EA8C500D144E9 /* WatchModel.swift in Sources */, 3717407E2949D40800FDDBC7 /* ChannelLinkView.swift in Sources */, 37FB28422721B22200A57617 /* ContentItem.swift in Sources */, 376CD21726FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */, @@ -3633,6 +3671,7 @@ 3774125727387D2300423605 /* FavoriteItem.swift in Sources */, 3774126B27387D6D00423605 /* CMTime+DefaultTimescale.swift in Sources */, 3774126027387D2D00423605 /* AccountsBridge.swift in Sources */, + 379ACB4F2A1F8A4100E01914 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift in Sources */, 3774125827387D2300423605 /* TrendingCategory.swift in Sources */, 3774126827387D6D00423605 /* Double+Format.swift in Sources */, 3774126E27387D8800423605 /* PlayerQueueItem.swift in Sources */, @@ -3660,6 +3699,7 @@ 371B7E5F27596B8400D21217 /* Comment.swift in Sources */, 3774126F27387D8D00423605 /* SearchQuery.swift in Sources */, 3774127127387D9E00423605 /* PlayerQueueItemBridge.swift in Sources */, + 379ACB542A1F8DB000E01914 /* HomeSettingsButton.swift in Sources */, 3774125227387D2300423605 /* Thumbnail.swift in Sources */, 37D4B0E32671614900C925CA /* Tests_macOS.swift in Sources */, 3774126527387D6D00423605 /* Int+Format.swift in Sources */, @@ -3690,6 +3730,7 @@ 37CC3F52270D010D00608308 /* VideoBanner.swift in Sources */, 37F49BA526CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */, 376CD21826FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */, + 379ACB532A1F8DB000E01914 /* HomeSettingsButton.swift in Sources */, 3718B9A22921A9670003DB2E /* VideoActions.swift in Sources */, 37CEE4BF2677B670005A1EFE /* SingleAssetStream.swift in Sources */, 37648B69286CF5F1003D330B /* TVControls.swift in Sources */, @@ -3724,6 +3765,7 @@ 37F13B64285E43C000B137E4 /* ControlsOverlay.swift in Sources */, 376B2E0926F920D600B1D64D /* SignInRequiredView.swift in Sources */, 378FFBC628660172009E3FBE /* URLParser.swift in Sources */, + 370E990C2A1EA8C600D144E9 /* WatchModel.swift in Sources */, 37141671267A8ACC006CA35D /* TrendingView.swift in Sources */, 37A2B348294723850050933E /* CacheModel.swift in Sources */, 37C3A24727235DA70087A57A /* ChannelPlaylist.swift in Sources */, @@ -3869,7 +3911,7 @@ 37F4AD2128612DFD004D0F66 /* Buffering.swift in Sources */, 379EF9E229AA585F009FE6C6 /* HideShortsButtons.swift in Sources */, 37FD43E52704847C0073EE42 /* View+Fixtures.swift in Sources */, - 37FAE000272ED58000330459 /* EditFavorites.swift in Sources */, + 37FAE000272ED58000330459 /* HomeSettings.swift in Sources */, 37F961A127BD90BB00058149 /* PlayerBackendType.swift in Sources */, 37D9BA0829507F69002586BD /* PlayerControlsSettings.swift in Sources */, 37599F32272B42810087F250 /* FavoriteItem.swift in Sources */, @@ -3882,7 +3924,9 @@ 37152EEC26EFEB95004FB96D /* LazyView.swift in Sources */, 371AC0A1294D13AA0085989E /* UnwatchedFeedCountModel.swift in Sources */, 37EF9A78275BEB8E0043B585 /* CommentView.swift in Sources */, + 379ACB4E2A1F8A4100E01914 /* NSManagedObjectContext+ExecuteAndMergeChanges.swift in Sources */, 37484C2726FC83E000287258 /* InstanceForm.swift in Sources */, + 37F5C7E22A1E2AF300927B73 /* ListView.swift in Sources */, 37E6D7A22944CD3800550C3D /* CacheStatusHeader.swift in Sources */, 37F49BA826CB0FCE00304AC0 /* PlaylistFormView.swift in Sources */, 37F0F4F0286F734400C06C2E /* AdvancedSettings.swift in Sources */,