diff --git a/Yattee/Views/Settings/AdvancedSettingsView.swift b/Yattee/Views/Settings/AdvancedSettingsView.swift index ba62db5b..fc9e738f 100644 --- a/Yattee/Views/Settings/AdvancedSettingsView.swift +++ b/Yattee/Views/Settings/AdvancedSettingsView.swift @@ -26,7 +26,7 @@ struct AdvancedSettingsView: View { @State private var isScanningStorage = false var body: some View { - List { + SettingsFormContainer { streamDetailsSection mpvSection settingsSection @@ -98,7 +98,7 @@ struct AdvancedSettingsView: View { @ViewBuilder private func feedSection(settingsManager: SettingsManager) -> some View { - Section { + SettingsFormSection("settings.advanced.feed.sectionTitle") { PlatformMenuPicker(selection: Binding( get: { settingsManager.feedCacheValidityMinutes }, set: { settingsManager.feedCacheValidityMinutes = $0 } @@ -109,17 +109,13 @@ struct AdvancedSettingsView: View { } label: { Label(String(localized: "settings.advanced.feed.cacheValidity"), systemImage: "clock") } - } header: { - Text(String(localized: "settings.advanced.feed.sectionTitle")) } footer: { VStack(alignment: .leading, spacing: 4) { Text(String(localized: "settings.advanced.feed.footer")) if let lastCheck = settingsManager.lastBackgroundCheck { Text(String(localized: "settings.advanced.feed.lastBackgroundRefresh \(lastCheck.formatted(date: .abbreviated, time: .shortened))")) - .foregroundStyle(.secondary) } else { Text(String(localized: "settings.advanced.feed.lastBackgroundRefresh.never")) - .foregroundStyle(.secondary) } } } @@ -127,7 +123,7 @@ struct AdvancedSettingsView: View { @ViewBuilder private func userAgentSection(settingsManager: SettingsManager) -> some View { - Section { + SettingsFormSection("settings.advanced.userAgent.sectionTitle", footer: "settings.advanced.userAgent.footer") { Toggle(isOn: Binding( get: { settingsManager.randomizeUserAgentPerRequest }, set: { @@ -165,25 +161,19 @@ struct AdvancedSettingsView: View { Label(String(localized: "settings.advanced.userAgent.randomize"), systemImage: "arrow.trianglehead.2.clockwise") } } - } header: { - Text(String(localized: "settings.advanced.userAgent.sectionTitle")) - } footer: { - Text(String(localized: "settings.advanced.userAgent.footer")) } } @ViewBuilder private var streamDetailsSection: some View { if let settingsManager = appEnvironment?.settingsManager { - Section { + SettingsFormSection(footer: "settings.advanced.stream.showDetails.footer") { Toggle(isOn: Binding( get: { settingsManager.showAdvancedStreamDetails }, set: { settingsManager.showAdvancedStreamDetails = $0 } )) { Label(String(localized: "settings.advanced.stream.showDetails"), systemImage: "list.bullet.rectangle") } - } footer: { - Text(String(localized: "settings.advanced.stream.showDetails.footer")) } } } @@ -191,7 +181,7 @@ struct AdvancedSettingsView: View { @ViewBuilder private var mpvSection: some View { if let settingsManager = appEnvironment?.settingsManager { - Section { + SettingsFormSection("settings.advanced.mpv.title") { PlatformMenuPicker(selection: Binding( get: { settingsManager.mpvBufferSeconds }, set: { settingsManager.mpvBufferSeconds = $0 } @@ -217,13 +207,17 @@ struct AdvancedSettingsView: View { Label(String(localized: "settings.playback.dash"), systemImage: "bolt.horizontal") } + #if os(tvOS) NavigationLink { MPVOptionsSettingsView() } label: { Label(String(localized: "settings.advanced.mpv.options"), systemImage: "slider.horizontal.3") } - } header: { - Text(String(localized: "settings.advanced.mpv.title")) + #else + SettingsNavigationRow("settings.advanced.mpv.options", systemImage: "slider.horizontal.3") { + MPVOptionsSettingsView() + } + #endif } } } @@ -241,24 +235,26 @@ struct AdvancedSettingsView: View { #if !os(tvOS) @ViewBuilder private var downloadsStorageSection: some View { - Section { + SettingsFormSection("settings.advanced.storage.title") { // Storage diagnostic button - Button { - runStorageDiagnostics() - } label: { - HStack { + HStack { + Button { + runStorageDiagnostics() + } label: { Label(String(localized: "settings.advanced.storage.scan"), systemImage: "internaldrive") - Spacer() - if isScanningStorage { - ProgressView() - .controlSize(.small) - } else if let diagnostics = storageDiagnostics { - Text(diagnostics.formattedTotal) - .foregroundStyle(.secondary) - } } + .disabled(isScanningStorage) + + if isScanningStorage { + ProgressView() + .controlSize(.small) + } else if let diagnostics = storageDiagnostics { + Text(diagnostics.formattedTotal) + .foregroundStyle(.secondary) + } + + Spacer() } - .disabled(isScanningStorage) // Show storage breakdown if scanned if let diagnostics = storageDiagnostics { @@ -275,28 +271,31 @@ struct AdvancedSettingsView: View { } // Clear cache button - Button(role: .destructive) { - showingClearDataConfirmation = true - } label: { - Label(String(localized: "settings.advanced.data.clearCache"), systemImage: "trash") + HStack { + Button(role: .destructive) { + showingClearDataConfirmation = true + } label: { + Label(String(localized: "settings.advanced.data.clearCache"), systemImage: "trash") + } + Spacer() } // Delete orphaned files button - Button(role: .destructive) { - showingOrphanCleanupConfirmation = true - } label: { - HStack { + HStack { + Button(role: .destructive) { + showingOrphanCleanupConfirmation = true + } label: { Label(String(localized: "settings.advanced.storage.deleteOrphaned"), systemImage: "trash") - Spacer() - if isScanning { - ProgressView() - .controlSize(.small) - } } + .disabled(orphanedFilesCount == 0 || isScanning) + + if isScanning { + ProgressView() + .controlSize(.small) + } + + Spacer() } - .disabled(orphanedFilesCount == 0 || isScanning) - } header: { - Text(String(localized: "settings.advanced.storage.title")) } footer: { if isScanning { Text(String(localized: "settings.advanced.storage.scanning")) @@ -314,7 +313,7 @@ struct AdvancedSettingsView: View { @ViewBuilder private var deviceNameSection: some View { if let settingsManager = appEnvironment?.settingsManager { - Section { + SettingsFormSection("settings.advanced.remoteControl.sectionTitle") { TextField( LocalNetworkService.systemDeviceName, text: Binding( @@ -338,8 +337,6 @@ struct AdvancedSettingsView: View { Label(String(localized: "remoteControl.hideWhenBackgrounded"), systemImage: "moon.fill") } #endif - } header: { - Text(String(localized: "settings.advanced.remoteControl.sectionTitle")) } footer: { VStack(alignment: .leading, spacing: 4) { Text(String(localized: "settings.advanced.remoteControl.footer")) @@ -355,22 +352,32 @@ struct AdvancedSettingsView: View { @ViewBuilder private var developerSection: some View { - Section { + SettingsFormSection(footer: "settings.developer.footer") { + #if os(tvOS) NavigationLink { DeveloperSettingsView() } label: { Label(String(localized: "settings.developer.title"), systemImage: "hammer") } + #else + SettingsNavigationRow("settings.developer.title", systemImage: "hammer") { + DeveloperSettingsView() + } + #endif if appEnvironment?.legacyMigrationService.hasLegacyData() == true { + #if os(tvOS) NavigationLink { LegacyDataImportView() } label: { Label(String(localized: "settings.advanced.data.importLegacy"), systemImage: "arrow.up.doc") } + #else + SettingsNavigationRow("settings.advanced.data.importLegacy", systemImage: "arrow.up.doc") { + LegacyDataImportView() + } + #endif } - } footer: { - Text(String(localized: "settings.developer.footer")) } } diff --git a/Yattee/Views/Settings/DeveloperSettingsView.swift b/Yattee/Views/Settings/DeveloperSettingsView.swift index abb87c79..e3f2fb7f 100644 --- a/Yattee/Views/Settings/DeveloperSettingsView.swift +++ b/Yattee/Views/Settings/DeveloperSettingsView.swift @@ -20,7 +20,7 @@ struct DeveloperSettingsView: View { @State private var showingResetiCloudComplete = false var body: some View { - List { + SettingsFormContainer { loggingSection if loggingEnabled { verboseLoggingSection @@ -87,7 +87,7 @@ struct DeveloperSettingsView: View { @ViewBuilder private var loggingSection: some View { - Section { + SettingsFormSection("settings.advanced.logging.sectionTitle", footer: "settings.advanced.logging.footer") { Toggle(isOn: $loggingEnabled) { Label(String(localized: "settings.advanced.logging.enable"), systemImage: "doc.text") } @@ -96,6 +96,7 @@ struct DeveloperSettingsView: View { } if loggingEnabled { + #if os(tvOS) NavigationLink { LogViewerView() } label: { @@ -106,18 +107,23 @@ struct DeveloperSettingsView: View { .foregroundStyle(.secondary) } } + #else + SettingsNavigationRow( + "settings.advanced.logging.viewLogs", + systemImage: "list.bullet.rectangle", + trailing: { Text("\(loggingService.entries.count)") } + ) { + LogViewerView() + } + #endif } - } header: { - Text(String(localized: "settings.advanced.logging.sectionTitle")) - } footer: { - Text(String(localized: "settings.advanced.logging.footer")) } } @ViewBuilder private var verboseLoggingSection: some View { if let settingsManager = appEnvironment?.settingsManager { - Section { + SettingsFormSection("settings.advanced.verboseLogging.sectionTitle") { Toggle(isOn: Binding( get: { settingsManager.verboseMPVLogging }, set: { settingsManager.verboseMPVLogging = $0 } @@ -131,8 +137,6 @@ struct DeveloperSettingsView: View { )) { Label(String(localized: "settings.advanced.debug.verboseRemote"), systemImage: "antenna.radiowaves.left.and.right") } - } header: { - Text(String(localized: "settings.advanced.verboseLogging.sectionTitle")) } } } @@ -140,7 +144,7 @@ struct DeveloperSettingsView: View { @ViewBuilder private var debugSection: some View { if let settingsManager = appEnvironment?.settingsManager { - Section { + SettingsFormSection("settings.advanced.debug.sectionTitle") { Toggle(isOn: Binding( get: { settingsManager.showPlayerAreaDebug }, set: { settingsManager.showPlayerAreaDebug = $0 } @@ -165,8 +169,6 @@ struct DeveloperSettingsView: View { Label(String(localized: "settings.advanced.debug.showTVDebugButton"), systemImage: "ant.circle") } #endif - } header: { - Text(String(localized: "settings.advanced.debug.sectionTitle")) } } } @@ -180,22 +182,24 @@ struct DeveloperSettingsView: View { @ViewBuilder private var dataSection: some View { - Section { - Button { - showingDeduplicateConfirmation = true - } label: { - Label(String(localized: "settings.advanced.data.removeDuplicates"), systemImage: "doc.on.doc") + SettingsFormSection("settings.advanced.data.sectionTitle", footer: "settings.advanced.data.footer") { + HStack { + Button { + showingDeduplicateConfirmation = true + } label: { + Label(String(localized: "settings.advanced.data.removeDuplicates"), systemImage: "doc.on.doc") + } + Spacer() } - Button(role: .destructive) { - showingResetiCloudConfirmation = true - } label: { - Label(String(localized: "settings.advanced.data.resetICloud"), systemImage: "icloud.slash") + HStack { + Button(role: .destructive) { + showingResetiCloudConfirmation = true + } label: { + Label(String(localized: "settings.advanced.data.resetICloud"), systemImage: "icloud.slash") + } + Spacer() } - } header: { - Text(String(localized: "settings.advanced.data.sectionTitle")) - } footer: { - Text(String(localized: "settings.advanced.data.footer")) } } @@ -222,22 +226,24 @@ struct DeveloperSettingsView: View { #if !os(tvOS) @ViewBuilder private var notificationTestingSection: some View { - Section { - Button { - sendTestNotification() - } label: { - Label(String(localized: "settings.advanced.testing.sendTestNotification"), systemImage: "bell.badge") + SettingsFormSection("settings.advanced.testing.notifications.sectionTitle", footer: "settings.advanced.testing.notifications.footer") { + HStack { + Button { + sendTestNotification() + } label: { + Label(String(localized: "settings.advanced.testing.sendTestNotification"), systemImage: "bell.badge") + } + Spacer() } - Button { - triggerBackgroundRefresh() - } label: { - Label(String(localized: "settings.advanced.testing.triggerBackgroundRefresh"), systemImage: "arrow.clockwise") + HStack { + Button { + triggerBackgroundRefresh() + } label: { + Label(String(localized: "settings.advanced.testing.triggerBackgroundRefresh"), systemImage: "arrow.clockwise") + } + Spacer() } - } header: { - Text(String(localized: "settings.advanced.testing.notifications.sectionTitle")) - } footer: { - Text(String(localized: "settings.advanced.testing.notifications.footer")) } } diff --git a/Yattee/Views/Settings/MacOSSettings.swift b/Yattee/Views/Settings/MacOSSettings.swift index 6b2455d5..6e330608 100644 --- a/Yattee/Views/Settings/MacOSSettings.swift +++ b/Yattee/Views/Settings/MacOSSettings.swift @@ -43,20 +43,10 @@ struct SettingsFormContainer: View { /// content with consistent padding, a bottom divider, and an optional /// caption-sized footer. /// - On iOS/tvOS: renders a standard `Section { } header: { } footer: { }`. -struct SettingsFormSection: View { +struct SettingsFormSection: View { let header: LocalizedStringKey? - let footer: LocalizedStringKey? @ViewBuilder let content: () -> Content - - init( - _ header: LocalizedStringKey? = nil, - footer: LocalizedStringKey? = nil, - @ViewBuilder content: @escaping () -> Content - ) { - self.header = header - self.footer = footer - self.content = content - } + @ViewBuilder let footer: () -> Footer var body: some View { #if os(macOS) @@ -91,49 +81,83 @@ struct SettingsFormSection: View { Divider() - if let footer { - Text(footer) - .font(.caption) - .foregroundStyle(.secondary) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, 16) - .padding(.top, 6) - } + footer() + .font(.caption) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 16) + .padding(.top, 6) } .padding(.bottom, 12) } #else @ViewBuilder private var platformSection: some View { - if let header, let footer { + if let header { Section { content() } header: { Text(header) } footer: { - Text(footer) - } - } else if let header { - Section { - content() - } header: { - Text(header) - } - } else if let footer { - Section { - content() - } footer: { - Text(footer) + footer() } } else { Section { content() + } footer: { + footer() } } } #endif } +extension SettingsFormSection where Footer == EmptyView { + init( + _ header: LocalizedStringKey? = nil, + @ViewBuilder content: @escaping () -> Content + ) { + self.header = header + self.content = content + self.footer = { EmptyView() } + } +} + +extension SettingsFormSection where Footer == Text { + init( + _ header: LocalizedStringKey? = nil, + footer: LocalizedStringKey, + @ViewBuilder content: @escaping () -> Content + ) { + self.header = header + self.content = content + self.footer = { Text(footer) } + } + + init( + _ header: LocalizedStringKey? = nil, + footer: LocalizedStringKey?, + @ViewBuilder content: @escaping () -> Content + ) { + self.header = header + self.content = content + let footerKey = footer + self.footer = { footerKey.map { Text($0) } ?? Text(verbatim: "") } + } +} + +extension SettingsFormSection { + init( + _ header: LocalizedStringKey? = nil, + @ViewBuilder content: @escaping () -> Content, + @ViewBuilder footer: @escaping () -> Footer + ) { + self.header = header + self.content = content + self.footer = footer + } +} + /// A label style that forces the icon to a fixed width so adjacent /// labels align regardless of icon glyph width. Use when a section has /// a vertical stack of `Label`s with mixed-width SF Symbols.