Improve tvOS settings UI styling and navigation

- 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 <noreply@anthropic.com>
This commit is contained in:
Arkadiusz Fal
2025-11-09 15:36:41 +01:00
parent 495dcec874
commit e0ca48fd44
13 changed files with 166 additions and 9 deletions

View File

@@ -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
}

View File

@@ -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")

View File

@@ -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
}
}

View File

@@ -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)

View File

@@ -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")

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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 = "<group>"; };
37E084AB2753D95F00039B7D /* AccountsNavigationLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsNavigationLink.swift; sourceTree = "<group>"; };
37E21DC52CDE528A008DF47C /* ta */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ta; path = ta.lproj/Localizable.strings; sourceTree = "<group>"; };
37E32DD42EC0D63600A63F29 /* TVOSPlainToggleStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVOSPlainToggleStyle.swift; sourceTree = "<group>"; };
37E64DD026D597EB00C71877 /* SubscribedChannelsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribedChannelsModel.swift; sourceTree = "<group>"; };
37E6D79B2944AE1A00550C3D /* FeedModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedModel.swift; sourceTree = "<group>"; };
37E6D79F2944CD3800550C3D /* CacheStatusHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheStatusHeader.swift; sourceTree = "<group>"; };
@@ -1937,6 +1941,7 @@
376BE50627347B57009AD608 /* SettingsHeader.swift */,
37B044B626F7AB9000E1419D /* SettingsView.swift */,
374C053427242D9F009BDDBE /* SponsorBlockSettings.swift */,
37E32DD42EC0D63600A63F29 /* TVOSPlainToggleStyle.swift */,
);
path = Settings;
sourceTree = "<group>";
@@ -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 */,