From e0ca48fd44435fe8f7a575c19c557de5fbdccf67 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Sun, 9 Nov 2025 15:36:41 +0100 Subject: [PATCH] Improve tvOS settings UI styling and navigation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add TVOSPlainToggleStyle for cleaner toggle appearance on tvOS - Remove focus overlays from settings navigation links and buttons - Apply plain button and list styles across all settings screens - Implement custom system controls picker for tvOS to avoid focus overlay - Update SettingsPickerModifier with platform-specific styling 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- Shared/Home/HomeView.swift | 3 +- Shared/Settings/AdvancedSettings.swift | 6 ++- Shared/Settings/BrowsingSettings.swift | 7 +++ Shared/Settings/HistorySettings.swift | 7 +++ Shared/Settings/LocationsSettings.swift | 6 ++- Shared/Settings/PlayerControlsSettings.swift | 47 ++++++++++++++++++++ Shared/Settings/PlayerSettings.swift | 7 +++ Shared/Settings/QualitySettings.swift | 7 +++ Shared/Settings/SettingsView.swift | 29 ++++++++++++ Shared/Settings/SponsorBlockSettings.swift | 5 +++ Shared/Settings/TVOSPlainToggleStyle.swift | 17 +++++++ Shared/Views/SettingsPickerModifier.swift | 26 ++++++++--- Yattee.xcodeproj/project.pbxproj | 8 ++++ 13 files changed, 166 insertions(+), 9 deletions(-) create mode 100644 Shared/Settings/TVOSPlainToggleStyle.swift diff --git a/Shared/Home/HomeView.swift b/Shared/Home/HomeView.swift index a450e32d..d85331ed 100644 --- a/Shared/Home/HomeView.swift +++ b/Shared/Home/HomeView.swift @@ -43,7 +43,7 @@ struct HomeView: View { AccentButton(imageSystemName: "ellipsis") { NavigationModel.shared.presentingOpenVideos = true } - .frame(maxWidth: 40) + .frame(maxWidth: 40) } } #endif @@ -74,6 +74,7 @@ struct HomeView: View { #if os(tvOS) .font(.caption) .imageScale(.small) + .foregroundColor(.primary) #endif #endif } diff --git a/Shared/Settings/AdvancedSettings.swift b/Shared/Settings/AdvancedSettings.swift index c511da91..9e374c2d 100644 --- a/Shared/Settings/AdvancedSettings.swift +++ b/Shared/Settings/AdvancedSettings.swift @@ -31,7 +31,9 @@ struct AdvancedSettings: View { List { advancedSettings } - #if os(iOS) + #if os(tvOS) + .listStyle(.plain) + #elseif os(iOS) .sheet(isPresented: $presentingShareSheet) { ShareSheet(activityItems: filesToShare) .id("logs-\(filesToShare.count)") @@ -41,6 +43,8 @@ struct AdvancedSettings: View { #endif } #if os(tvOS) + .buttonStyle(.plain) + .toggleStyle(TVOSPlainToggleStyle()) .frame(maxWidth: 1000) #endif .navigationTitle("Advanced") diff --git a/Shared/Settings/BrowsingSettings.swift b/Shared/Settings/BrowsingSettings.swift index c382fcab..5a0cc579 100644 --- a/Shared/Settings/BrowsingSettings.swift +++ b/Shared/Settings/BrowsingSettings.swift @@ -52,10 +52,14 @@ struct BrowsingSettings: View { } #if os(iOS) .listStyle(.insetGrouped) + #elseif os(tvOS) + .listStyle(.plain) #endif #endif } #if os(tvOS) + .buttonStyle(.plain) + .toggleStyle(TVOSPlainToggleStyle()) .frame(maxWidth: 1200) #else .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) @@ -111,6 +115,9 @@ struct BrowsingSettings: View { NavigationLink(destination: LazyView(HomeSettings())) { Text("Home Settings") } + #if os(tvOS) + .buttonStyle(.plain) + #endif #endif } } diff --git a/Shared/Settings/HistorySettings.swift b/Shared/Settings/HistorySettings.swift index c572fcf8..f6e5d8e4 100644 --- a/Shared/Settings/HistorySettings.swift +++ b/Shared/Settings/HistorySettings.swift @@ -29,9 +29,16 @@ struct HistorySettings: View { List { sections } + #if os(tvOS) + .listStyle(.plain) + #elseif os(iOS) + .listStyle(.insetGrouped) + #endif #endif } #if os(tvOS) + .buttonStyle(.plain) + .toggleStyle(TVOSPlainToggleStyle()) .frame(maxWidth: 1000) #elseif os(iOS) .listStyle(.insetGrouped) diff --git a/Shared/Settings/LocationsSettings.swift b/Shared/Settings/LocationsSettings.swift index 8a84664f..ddfe7396 100644 --- a/Shared/Settings/LocationsSettings.swift +++ b/Shared/Settings/LocationsSettings.swift @@ -22,7 +22,9 @@ struct LocationsSettings: View { List { settings } - #if os(iOS) + #if os(tvOS) + .listStyle(.plain) + #elseif os(iOS) .listStyle(.insetGrouped) #endif #endif @@ -42,6 +44,8 @@ struct LocationsSettings: View { InstanceForm(savedInstanceID: $savedFormInstanceID) } #if os(tvOS) + .buttonStyle(.plain) + .toggleStyle(TVOSPlainToggleStyle()) .frame(maxWidth: 1000) #endif .navigationTitle("Locations") diff --git a/Shared/Settings/PlayerControlsSettings.swift b/Shared/Settings/PlayerControlsSettings.swift index 9efb612a..d0dbfbae 100644 --- a/Shared/Settings/PlayerControlsSettings.swift +++ b/Shared/Settings/PlayerControlsSettings.swift @@ -53,9 +53,16 @@ struct PlayerControlsSettings: View { List { sections } + #if os(tvOS) + .listStyle(.plain) + #elseif os(iOS) + .listStyle(.insetGrouped) + #endif #endif } #if os(tvOS) + .buttonStyle(.plain) + .toggleStyle(TVOSPlainToggleStyle()) .frame(maxWidth: 1000) #elseif os(iOS) .listStyle(.insetGrouped) @@ -143,6 +150,45 @@ struct PlayerControlsSettings: View { #endif } + #if os(tvOS) + // Custom implementation for tvOS to avoid focus overlay + return VStack(alignment: .leading, spacing: 0) { + Text("System controls buttons") + .font(.headline) + .padding(.vertical, 8) + + Button(action: { systemControlsCommands = .seek }) { + HStack { + Text(labelText("Seek".localized())) + Spacer() + if systemControlsCommands == .seek { + Image(systemName: "checkmark") + .foregroundColor(.accentColor) + } + } + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .padding(.vertical, 4) + + Button(action: { + systemControlsCommands = .restartAndAdvanceToNext + player.updateRemoteCommandCenter() + }) { + HStack { + Text(labelText("Restart/Play next".localized())) + Spacer() + if systemControlsCommands == .restartAndAdvanceToNext { + Image(systemName: "checkmark") + .foregroundColor(.accentColor) + } + } + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .padding(.vertical, 4) + } + #else return Picker("System controls buttons", selection: $systemControlsCommands) { Text(labelText("Seek".localized())).tag(SystemControlsCommands.seek) Text(labelText("Restart/Play next".localized())).tag(SystemControlsCommands.restartAndAdvanceToNext) @@ -151,6 +197,7 @@ struct PlayerControlsSettings: View { player.updateRemoteCommandCenter() } .modifier(SettingsPickerModifier()) + #endif } @ViewBuilder private var controlsLayoutFooter: some View { diff --git a/Shared/Settings/PlayerSettings.swift b/Shared/Settings/PlayerSettings.swift index 4bdc40bf..a8a03051 100644 --- a/Shared/Settings/PlayerSettings.swift +++ b/Shared/Settings/PlayerSettings.swift @@ -69,9 +69,16 @@ struct PlayerSettings: View { List { sections } + #if os(tvOS) + .listStyle(.plain) + #elseif os(iOS) + .listStyle(.insetGrouped) + #endif #endif } #if os(tvOS) + .buttonStyle(.plain) + .toggleStyle(TVOSPlainToggleStyle()) .frame(maxWidth: 1000) #elseif os(iOS) .listStyle(.insetGrouped) diff --git a/Shared/Settings/QualitySettings.swift b/Shared/Settings/QualitySettings.swift index afe8ae8d..68ee29a0 100644 --- a/Shared/Settings/QualitySettings.swift +++ b/Shared/Settings/QualitySettings.swift @@ -24,12 +24,19 @@ struct QualitySettings: View { List { sections } + #if os(tvOS) + .listStyle(.plain) + #elseif os(iOS) + .listStyle(.insetGrouped) + #endif #endif } .sheet(isPresented: $presentingProfileForm) { QualityProfileForm(qualityProfileID: $editedProfileID) } #if os(tvOS) + .buttonStyle(.plain) + .toggleStyle(TVOSPlainToggleStyle()) .frame(maxWidth: 1000) #elseif os(iOS) .listStyle(.insetGrouped) diff --git a/Shared/Settings/SettingsView.swift b/Shared/Settings/SettingsView.swift index a73e7b40..aca6ad8f 100644 --- a/Shared/Settings/SettingsView.swift +++ b/Shared/Settings/SettingsView.swift @@ -164,6 +164,7 @@ struct SettingsView: View { Text("Not Selected") } } + .buttonStyle(.plain) } Divider() } @@ -175,30 +176,45 @@ struct SettingsView: View { } label: { Label("Browsing", systemImage: "list.and.film").labelStyle(SettingsLabel()) } + #if os(tvOS) + .buttonStyle(.plain) + #endif NavigationLink { PlayerSettings() } label: { Label("Player", systemImage: "play.rectangle").labelStyle(SettingsLabel()) } + #if os(tvOS) + .buttonStyle(.plain) + #endif NavigationLink { PlayerControlsSettings() } label: { Label("Controls", systemImage: "hand.tap").labelStyle(SettingsLabel()) } + #if os(tvOS) + .buttonStyle(.plain) + #endif NavigationLink { QualitySettings() } label: { Label("Quality", systemImage: "4k.tv").labelStyle(SettingsLabel()) } + #if os(tvOS) + .buttonStyle(.plain) + #endif NavigationLink { HistorySettings() } label: { Label("History", systemImage: "clock.arrow.circlepath").labelStyle(SettingsLabel()) } + #if os(tvOS) + .buttonStyle(.plain) + #endif if !accounts.isEmpty { NavigationLink { @@ -206,6 +222,9 @@ struct SettingsView: View { } label: { Label("SponsorBlock", systemImage: "dollarsign.circle").labelStyle(SettingsLabel()) } + #if os(tvOS) + .buttonStyle(.plain) + #endif } NavigationLink { @@ -213,12 +232,18 @@ struct SettingsView: View { } label: { Label("Locations", systemImage: "globe").labelStyle(SettingsLabel()) } + #if os(tvOS) + .buttonStyle(.plain) + #endif NavigationLink { AdvancedSettings() } label: { Label("Advanced", systemImage: "wrench.and.screwdriver").labelStyle(SettingsLabel()) } + #if os(tvOS) + .buttonStyle(.plain) + #endif } #if os(tvOS) .padding(.horizontal, 20) @@ -232,6 +257,9 @@ struct SettingsView: View { } label: { Label("Help", systemImage: "questionmark.circle").labelStyle(SettingsLabel()) } + #if os(tvOS) + .buttonStyle(.plain) + #endif } #if os(tvOS) .padding(.horizontal, 20) @@ -287,6 +315,7 @@ struct SettingsView: View { Label("Import Settings", systemImage: "square.and.arrow.down") .labelStyle(SettingsLabel()) } + .buttonStyle(.plain) .padding(.horizontal, 20) #else Button(action: importSettings) { diff --git a/Shared/Settings/SponsorBlockSettings.swift b/Shared/Settings/SponsorBlockSettings.swift index 334cf5b6..1bacaf56 100644 --- a/Shared/Settings/SponsorBlockSettings.swift +++ b/Shared/Settings/SponsorBlockSettings.swift @@ -23,9 +23,14 @@ struct SponsorBlockSettings: View { List { sections } + #if os(tvOS) + .listStyle(.plain) + #endif #endif } #if os(tvOS) + .buttonStyle(.plain) + .toggleStyle(TVOSPlainToggleStyle()) .frame(maxWidth: 1000) #else .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) diff --git a/Shared/Settings/TVOSPlainToggleStyle.swift b/Shared/Settings/TVOSPlainToggleStyle.swift new file mode 100644 index 00000000..2c88a43d --- /dev/null +++ b/Shared/Settings/TVOSPlainToggleStyle.swift @@ -0,0 +1,17 @@ +import SwiftUI + +#if os(tvOS) +struct TVOSPlainToggleStyle: ToggleStyle { + func makeBody(configuration: Configuration) -> some View { + Button(action: { configuration.isOn.toggle() }) { + HStack { + configuration.label + Spacer() + Image(systemName: configuration.isOn ? "checkmark.circle.fill" : "circle") + .foregroundColor(configuration.isOn ? .accentColor : .secondary) + } + } + .buttonStyle(.plain) + } +} +#endif diff --git a/Shared/Views/SettingsPickerModifier.swift b/Shared/Views/SettingsPickerModifier.swift index 49dd09b7..01786458 100644 --- a/Shared/Views/SettingsPickerModifier.swift +++ b/Shared/Views/SettingsPickerModifier.swift @@ -3,14 +3,28 @@ import SwiftUI struct SettingsPickerModifier: ViewModifier { func body(content: Content) -> some View { - content #if os(tvOS) - .pickerStyle(.inline) - #endif - #if os(iOS) - .pickerStyle(.automatic) + content + .pickerStyle(.inline) + .onAppear { + // Force refresh to apply button style to picker options + } + #elseif os(iOS) + content + .pickerStyle(.automatic) #else - .labelsHidden() + content + .labelsHidden() #endif } } + +#if os(tvOS) +// Extension to help remove picker row backgrounds +extension View { + func pickerRowStyle() -> some View { + self.buttonStyle(.plain) + .listRowBackground(Color.clear) + } +} +#endif diff --git a/Yattee.xcodeproj/project.pbxproj b/Yattee.xcodeproj/project.pbxproj index fed8112b..4d31e037 100644 --- a/Yattee.xcodeproj/project.pbxproj +++ b/Yattee.xcodeproj/project.pbxproj @@ -941,6 +941,9 @@ 37E04C0F275940FB00172673 /* VerticalScrollingFix.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E04C0E275940FB00172673 /* VerticalScrollingFix.swift */; }; 37E084AC2753D95F00039B7D /* AccountsNavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */; }; 37E084AD2753D95F00039B7D /* AccountsNavigationLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */; }; + 37E32DD52EC0D63600A63F29 /* TVOSPlainToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E32DD42EC0D63600A63F29 /* TVOSPlainToggleStyle.swift */; }; + 37E32DD62EC0D63600A63F29 /* TVOSPlainToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E32DD42EC0D63600A63F29 /* TVOSPlainToggleStyle.swift */; }; + 37E32DD72EC0D63600A63F29 /* TVOSPlainToggleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E32DD42EC0D63600A63F29 /* TVOSPlainToggleStyle.swift */; }; 37E64DD126D597EB00C71877 /* SubscribedChannelsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E64DD026D597EB00C71877 /* SubscribedChannelsModel.swift */; }; 37E64DD226D597EB00C71877 /* SubscribedChannelsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E64DD026D597EB00C71877 /* SubscribedChannelsModel.swift */; }; 37E64DD326D597EB00C71877 /* SubscribedChannelsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37E64DD026D597EB00C71877 /* SubscribedChannelsModel.swift */; }; @@ -1502,6 +1505,7 @@ 37E04C0E275940FB00172673 /* VerticalScrollingFix.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalScrollingFix.swift; sourceTree = ""; }; 37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsNavigationLink.swift; sourceTree = ""; }; 37E21DC52CDE528A008DF47C /* ta */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ta; path = ta.lproj/Localizable.strings; sourceTree = ""; }; + 37E32DD42EC0D63600A63F29 /* TVOSPlainToggleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVOSPlainToggleStyle.swift; sourceTree = ""; }; 37E64DD026D597EB00C71877 /* SubscribedChannelsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribedChannelsModel.swift; sourceTree = ""; }; 37E6D79B2944AE1A00550C3D /* FeedModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedModel.swift; sourceTree = ""; }; 37E6D79F2944CD3800550C3D /* CacheStatusHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheStatusHeader.swift; sourceTree = ""; }; @@ -1937,6 +1941,7 @@ 376BE50627347B57009AD608 /* SettingsHeader.swift */, 37B044B626F7AB9000E1419D /* SettingsView.swift */, 374C053427242D9F009BDDBE /* SponsorBlockSettings.swift */, + 37E32DD42EC0D63600A63F29 /* TVOSPlainToggleStyle.swift */, ); path = Settings; sourceTree = ""; @@ -3303,6 +3308,7 @@ 373031F528383A89000CFD59 /* PiPDelegate.swift in Sources */, 37F5E8BA291BEF69006C15F5 /* BaseCacheModel.swift in Sources */, 371AC09F294D13AA0085989E /* UnwatchedFeedCountModel.swift in Sources */, + 37E32DD72EC0D63600A63F29 /* TVOSPlainToggleStyle.swift in Sources */, 370015A928BBAE7F000149FD /* ProgressBar.swift in Sources */, 37C3A24927235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */, 377E17142928265900894889 /* ListRowSeparator+Backport.swift in Sources */, @@ -3529,6 +3535,7 @@ 37F961A027BD90BB00058149 /* PlayerBackendType.swift in Sources */, 3776924F294630110055EC18 /* ChannelAvatarView.swift in Sources */, 37BA221229526A19000DAD1F /* ControlsGradientView.swift in Sources */, + 37E32DD62EC0D63600A63F29 /* TVOSPlainToggleStyle.swift in Sources */, 37BC50AD2778BCBA00510953 /* HistoryModel.swift in Sources */, 37A362BB2953707F00BDF328 /* ClearQueueButton.swift in Sources */, 3752069E285E910600CA655F /* ChapterView.swift in Sources */, @@ -3854,6 +3861,7 @@ 377692582946476F0055EC18 /* ChannelPlaylistsCacheModel.swift in Sources */, 371B7E5E27596B8400D21217 /* Comment.swift in Sources */, 37732FF22703A26300F04329 /* AccountValidationStatus.swift in Sources */, + 37E32DD52EC0D63600A63F29 /* TVOSPlainToggleStyle.swift in Sources */, 371CC76E29466F5A00979C1A /* AccountsViewModel.swift in Sources */, 3756C2AC2861151C00E4B059 /* NetworkStateModel.swift in Sources */, 37F5E8B8291BE9D0006C15F5 /* URLBookmarkModel.swift in Sources */,