diff --git a/Yattee/Core/Settings/SettingsManager+Home.swift b/Yattee/Core/Settings/SettingsManager+Home.swift index 49419dce..51fd628c 100644 --- a/Yattee/Core/Settings/SettingsManager+Home.swift +++ b/Yattee/Core/Settings/SettingsManager+Home.swift @@ -586,13 +586,6 @@ extension SettingsManager { return existingCards.contains(card.id) ? [] : [card] } - /// Returns all available section items for a media source that are NOT already added. - func availableSections(for source: MediaSource) -> [HomeSectionItem] { - let section = HomeSectionItem.mediaSource(sourceID: source.id) - let existingSections = Set(homeSectionOrder.map { $0.id }) - return existingSections.contains(section.id) ? [] : [section] - } - /// Returns all available cards across all media sources, grouped by source. func allAvailableMediaSourceShortcuts(sources: [MediaSource]) -> [(source: MediaSource, cards: [HomeShortcutItem])] { sources.compactMap { source in @@ -601,14 +594,6 @@ extension SettingsManager { } } - /// Returns all available sections across all media sources, grouped by source. - func allAvailableMediaSourceSections(sources: [MediaSource]) -> [(source: MediaSource, sections: [HomeSectionItem])] { - sources.compactMap { source in - let sections = availableSections(for: source) - return sections.isEmpty ? nil : (source, sections) - } - } - /// Removes all Home items for media sources that no longer exist. func cleanupOrphanedHomeMediaSourceItems(validSourceIDs: Set) { var hadOrphans = false @@ -682,6 +667,45 @@ extension SettingsManager { } } + /// Removes ALL media-source Home *sections* (regardless of whether the source + /// still exists). Media sources are shortcuts-only now — a media-source + /// "section" was just a browse link and the feature was removed. Idempotent: + /// only writes when something changed, so it's safe to call on every load and + /// it also cleans up media-source sections that sync in from an older device. + /// Leaves media-source *shortcuts* (`homeShortcutOrder`/`homeShortcutVisibility`) untouched. + func removeAllHomeMediaSourceSections() { + var didChange = false + + var sectionOrder = homeSectionOrder + let originalCount = sectionOrder.count + sectionOrder.removeAll { item in + if case .mediaSource = item { return true } + return false + } + if sectionOrder.count != originalCount { + LoggingService.shared.logCloudKit("removeAllHomeMediaSourceSections: removed \(originalCount - sectionOrder.count) media-source sections") + homeSectionOrder = sectionOrder + didChange = true + } + + var sectionVis = homeSectionVisibility + let orphanedKeys = sectionVis.keys.filter { item in + if case .mediaSource = item { return true } + return false + } + if !orphanedKeys.isEmpty { + for key in orphanedKeys { + sectionVis.removeValue(forKey: key) + } + homeSectionVisibility = sectionVis + didChange = true + } + + if didChange { + LoggingService.shared.logCloudKit("removeAllHomeMediaSourceSections: cleaned up media-source sections") + } + } + // MARK: - Tab Bar Settings (Compact Size Class) /// Ordered list of tab bar items. Default order is subscriptions first, then others. diff --git a/Yattee/Localizable.xcstrings b/Yattee/Localizable.xcstrings index b37413eb..ca607170 100644 --- a/Yattee/Localizable.xcstrings +++ b/Yattee/Localizable.xcstrings @@ -4216,6 +4216,17 @@ } } }, + "home.settings.shortcuts.color" : { + "comment" : "Label/header for the card color emphasis picker", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Color" + } + } + } + }, "home.settings.shortcuts.header" : { "comment" : "Header for shortcuts section in library settings", "localizations" : { @@ -4238,28 +4249,6 @@ } } }, - "home.settings.shortcuts.color" : { - "comment" : "Label/header for the card color emphasis picker", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Color" - } - } - } - }, - "home.settings.shortcuts.layout" : { - "comment" : "Header for the card layout picker (Compact/Regular)", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Layout" - } - } - } - }, "home.settings.shortcuts.palette" : { "comment" : "Label/header for the colorful palette picker", "localizations" : { @@ -4406,39 +4395,6 @@ } } }, - "home.shortcuts.layout.cards" : { - "comment" : "Cards layout option for library shortcuts", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Cards" - } - } - } - }, - "home.shortcuts.layout.list" : { - "comment" : "List layout option for library shortcuts", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "List" - } - } - } - }, - "home.shortcuts.palette.berry" : { - "comment" : "Colorful palette name", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Berry" - } - } - } - }, "home.shortcuts.color.soft" : { "comment" : "Soft card color option (tinted background)", "localizations" : { @@ -4461,6 +4417,28 @@ } } }, + "home.shortcuts.layout.cards" : { + "comment" : "Cards layout option for library shortcuts", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cards" + } + } + } + }, + "home.shortcuts.layout.list" : { + "comment" : "List layout option for library shortcuts", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "List" + } + } + } + }, "home.shortcuts.palette.accent" : { "comment" : "Palette name for the uniform accent-color fill", "localizations" : { @@ -4472,6 +4450,17 @@ } } }, + "home.shortcuts.palette.berry" : { + "comment" : "Colorful palette name", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Berry" + } + } + } + }, "home.shortcuts.palette.classic" : { "comment" : "Colorful palette name", "localizations" : { @@ -5512,6 +5501,7 @@ } }, "mediaSources.browse %@" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { diff --git a/Yattee/Views/Home/HomeItem.swift b/Yattee/Views/Home/HomeItem.swift index 0fdeed66..5b44cc46 100644 --- a/Yattee/Views/Home/HomeItem.swift +++ b/Yattee/Views/Home/HomeItem.swift @@ -435,6 +435,11 @@ enum HomeSectionItem: Codable, Hashable, Identifiable, Sendable { case history case downloads case instanceContent(instanceID: UUID, contentType: InstanceContentType) + /// Legacy, decode-only. Media sources are shortcuts-only now — a media-source + /// "section" was just a browse link. Kept so existing saved `homeSectionOrder` + /// data still decodes (the array decodes all-or-nothing); any such items are + /// stripped on load by `SettingsManager.removeAllHomeMediaSourceSections()`. + /// No longer user-addable and not rendered. case mediaSource(sourceID: UUID) var id: String { diff --git a/Yattee/Views/Home/HomeSettingsView.swift b/Yattee/Views/Home/HomeSettingsView.swift index a91552f4..0dac031e 100644 --- a/Yattee/Views/Home/HomeSettingsView.swift +++ b/Yattee/Views/Home/HomeSettingsView.swift @@ -27,7 +27,6 @@ struct HomeSettingsView: View { @State private var availableShortcutsByInstance: [(instance: Instance, cards: [HomeShortcutItem])] = [] @State private var availableSectionsByInstance: [(instance: Instance, sections: [HomeSectionItem])] = [] @State private var availableShortcutsByMediaSource: [(source: MediaSource, cards: [HomeShortcutItem])] = [] - @State private var availableSectionsByMediaSource: [(source: MediaSource, sections: [HomeSectionItem])] = [] // Edit mode for delete functionality @State private var isEditMode = false @@ -238,7 +237,7 @@ struct HomeSettingsView: View { private var availableSectionsSection: some View { Section { - if availableSectionsByInstance.isEmpty && availableSectionsByMediaSource.isEmpty { + if availableSectionsByInstance.isEmpty { Text(String(localized: "home.settings.availableSections.empty")) .foregroundStyle(.secondary) .italic() @@ -248,11 +247,6 @@ struct HomeSettingsView: View { availableSectionRow(for: section, instance: item.instance) } } - ForEach(availableSectionsByMediaSource, id: \.source.id) { item in - ForEach(item.sections) { section in - availableMediaSourceSectionRow(for: section, source: item.source) - } - } } } header: { Text(String(localized: "home.settings.availableSections.header")) @@ -350,7 +344,6 @@ struct HomeSettingsView: View { let sources = env.mediaSourcesManager.sources availableShortcutsByMediaSource = settings.allAvailableMediaSourceShortcuts(sources: sources) - availableSectionsByMediaSource = settings.allAvailableMediaSourceSections(sources: sources) } private func saveSettingsIfLoaded() { @@ -407,16 +400,14 @@ struct HomeSettingsView: View { switch section { case .instanceContent(let instanceID, let contentType): settingsManager?.addToHome(instanceID: instanceID, contentType: contentType, asCard: false) - case .mediaSource(let sourceID): - settingsManager?.addToHome(sourceID: sourceID, asCard: false) default: break } - + // Reload available items loadSettings() } - + private func removeShortcut(_ card: HomeShortcutItem) { // Remove from local state shortcutOrder.removeAll { $0.id == card.id } @@ -445,16 +436,14 @@ struct HomeSettingsView: View { switch section { case .instanceContent(let instanceID, let contentType): settingsManager?.removeFromHome(instanceID: instanceID, contentType: contentType) - case .mediaSource(let sourceID): - settingsManager?.removeFromHome(sourceID: sourceID) default: break } - + // Reload available items loadSettings() } - + private func canDelete(shortcut: HomeShortcutItem) -> Bool { if case .instanceContent = shortcut { return true @@ -469,9 +458,6 @@ struct HomeSettingsView: View { if case .instanceContent = section { return true } - if case .mediaSource = section { - return true - } return false } @@ -579,35 +565,6 @@ struct HomeSettingsView: View { } } } - case .mediaSource(let sourceID): - if let source = appEnvironment?.mediaSourcesManager.sources.first(where: { $0.id == sourceID }) { - let isDisabled = !source.isEnabled - - HomeItemRow( - icon: source.type.systemImage, - title: "\(source.name) (\(source.type.displayName))", - isVisible: sectionBinding(for: section), - isDisabled: isDisabled, - disabledReason: isDisabled ? String(localized: "home.settings.sourceDisabled") : nil - ) - #if os(iOS) - .swipeActions(edge: .trailing, allowsFullSwipe: true) { - Button(role: .destructive) { - removeSection(section) - } label: { - Label(String(localized: "home.settings.remove"), systemImage: "trash") - } - .tint(.red) - } - #endif - .contextMenu { - Button(role: .destructive) { - removeSection(section) - } label: { - Label(String(localized: "home.settings.remove"), systemImage: "trash") - } - } - } default: HomeItemRow( icon: section.icon, @@ -616,7 +573,7 @@ struct HomeSettingsView: View { ) } } - + @ViewBuilder private func availableShortcutRow(for card: HomeShortcutItem, instance: Instance) -> some View { if case .instanceContent(_, let contentType) = card { @@ -728,37 +685,6 @@ struct HomeSettingsView: View { #endif } } - - @ViewBuilder - private func availableMediaSourceSectionRow(for section: HomeSectionItem, source: MediaSource) -> some View { - if case .mediaSource = section { - let isDisabled = !source.isEnabled - - HStack { - Image(systemName: source.type.systemImage) - .frame(width: 24) - .foregroundStyle(isDisabled ? .tertiary : .secondary) - - Text("\(source.name) (\(source.type.displayName))") - .foregroundStyle(isDisabled ? .tertiary : .primary) - - Spacer() - - Button { - addSection(section) - } label: { - Image(systemName: "plus.circle.fill") - .foregroundStyle(.green) - .imageScale(.large) - } - .buttonStyle(.plain) - .disabled(isDisabled) - } - #if !os(tvOS) - .help(isDisabled ? String(localized: "home.settings.sourceDisabled") : "") - #endif - } - } } // MARK: - Home Item Row diff --git a/Yattee/Views/Home/HomeView.swift b/Yattee/Views/Home/HomeView.swift index bd5aed79..e514177d 100644 --- a/Yattee/Views/Home/HomeView.swift +++ b/Yattee/Views/Home/HomeView.swift @@ -1057,8 +1057,11 @@ struct HomeView: View { #endif case .instanceContent(let instanceID, let contentType): instanceContentSection(instanceID: instanceID, contentType: contentType) - case .mediaSource(let sourceID): - mediaSourceSection(sourceID: sourceID) + case .mediaSource: + // Media sources are shortcuts-only; any legacy media-source section is + // stripped by removeAllHomeMediaSourceSections() on load, so this is + // unreachable in practice. Rendered as empty for defense-in-depth. + EmptyView() } } @@ -1422,44 +1425,6 @@ struct HomeView: View { } } - @ViewBuilder - private func mediaSourceSection(sourceID: UUID) -> some View { - if let source = appEnvironment?.mediaSourcesManager.sources.first(where: { $0.id == sourceID }), - source.isEnabled { - VStack(alignment: .leading, spacing: 0) { - Text(source.name) - .font(.subheadline.weight(.semibold)) - .foregroundStyle(.secondary) - .padding(.horizontal, 32) - .padding(.top, 16) - .padding(.bottom, 8) - .frame(maxWidth: .infinity, alignment: .leading) - - VideoListContent(listStyle: listStyle) { - VideoListRow( - isLast: true, - rowStyle: .regular, - listStyle: listStyle - ) { - Button { - appEnvironment?.navigationCoordinator.navigate(to: .mediaBrowser(source, path: "/")) - } label: { - HStack { - Image(systemName: source.type.systemImage) - .foregroundStyle(.secondary) - Text("mediaSources.browse \(source.name)") - Spacer() - Image(systemName: "chevron.right") - .foregroundStyle(.tertiary) - } - } - .buttonStyle(.plain) - } - } - } - } - } - // MARK: - Data Loading private func loadData() { @@ -1471,6 +1436,8 @@ struct HomeView: View { loadRemoteDevicesData() cleanupOrphanedHomeInstanceItems() cleanupOrphanedHomeMediaSourceItems() + // Media sources are shortcuts-only; drop any legacy media-source sections. + appEnvironment?.settingsManager.removeAllHomeMediaSourceSections() } private func cleanupOrphanedHomeInstanceItems() {