Files
yattee/Yattee/Views/Home/HomeSettingsView.swift
Arkadiusz Fal 9e2b0fa095 Extend opaque background to settings and other pushed pages on macOS
The NavigationStack-level background wasn't enough: macOS draws its own
translucent material behind pushed navigation pages, and Form/List
containers draw a translucent scroll background on top of anything
placed behind them, so several pages kept the wallpaper-tinted look.

Bake the opaque background into SettingsFormContainer (covers all pages
built on it, pushed or not) and add opaqueSettingsFormBackground(),
which hides the container's scroll background before applying the
opaque one, to every macOS-reachable Form/List settings page: player
controls and its sub-editors, sidebar settings, edit source, legacy
data import, subscription/playlist import, customize home and its
shortcut style page, and the log viewer. Contributors, translators and
the remote control device page get the plain opaque background.

Claude-Session: https://claude.ai/code/session_0154KH8RAVAvm6iVanhmoW8o
2026-07-07 22:16:09 +02:00

845 lines
31 KiB
Swift

//
// HomeSettingsView.swift
// Yattee
//
// Settings sheet for customizing the Home view layout.
//
import SwiftUI
struct HomeSettingsView: View {
@Environment(\.appEnvironment) private var appEnvironment
// Local state for editing (copied from settings on appear, saved on dismiss)
@State private var shortcutLayout: HomeShortcutLayout = .cards
@State private var shortcutCardStyle: HomeShortcutCardStyle = .regular
@State private var shortcutCardColor: HomeShortcutCardColor = .soft
@State private var shortcutColorfulPalette: HomeShortcutColorfulPalette = .accent
@State private var shortcutCustomPaletteColors: [String] = []
@State private var shortcutOrder: [HomeShortcutItem] = []
@State private var shortcutVisibility: [HomeShortcutItem: Bool] = [:]
@State private var sectionOrder: [HomeSectionItem] = []
@State private var sectionVisibility: [HomeSectionItem: Bool] = [:]
@State private var sectionItemsLimit: Int = 5
@State private var sectionLayout: HomeSectionLayout = HomeSectionLayout.platformDefault
// Available items (not yet added to Home)
@State private var availableShortcutsByInstance: [(instance: Instance, cards: [HomeShortcutItem])] = []
@State private var availableSectionsByInstance: [(instance: Instance, sections: [HomeSectionItem])] = []
@State private var availableShortcutsByMediaSource: [(source: MediaSource, cards: [HomeShortcutItem])] = []
// Edit mode for delete functionality
@State private var isEditMode = false
// Load settings only once per presentation: pushing a child page (e.g. the
// card style view) fires onDisappear/onAppear on this view, and re-running
// loadSettings on pop-back would clobber edits made through bindings.
@State private var hasLoadedSettings = false
#if os(macOS)
// macOS: drive the style page programmatically. A List-embedded NavigationLink
// gets stuck in the selected (blue) state after popping back and can't be
// re-activated, so navigate via navigationDestination(isPresented:) instead.
@State private var showingStyleSettings = false
#endif
private var settingsManager: SettingsManager? { appEnvironment?.settingsManager }
var body: some View {
List {
#if !os(tvOS)
shortcutsSection
availableShortcutsSection
#endif
sectionsSection
availableSectionsSection
itemsLimitSection
}
#if os(iOS)
.environment(\.editMode, isEditMode ? .constant(.active) : .constant(.inactive))
#endif
#if os(macOS)
.listStyle(.inset)
.navigationDestination(isPresented: $showingStyleSettings) {
HomeShortcutStyleView(
style: $shortcutCardStyle,
color: $shortcutCardColor,
palette: $shortcutColorfulPalette,
customColors: $shortcutCustomPaletteColors,
onSave: saveSettingsIfLoaded
)
}
#endif
.opaqueSettingsFormBackground()
#if !os(tvOS)
.navigationTitle(String(localized: "home.settings.title"))
#endif
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
.onAppear {
guard !hasLoadedSettings else { return }
hasLoadedSettings = true
loadSettings()
}
.onDisappear {
saveSettings()
}
}
// MARK: - Sections
private var shortcutsSection: some View {
Section {
#if !os(tvOS)
// Layout picker (List vs Cards)
Picker(String(localized: "home.settings.shortcuts.layout"), selection: $shortcutLayout) {
ForEach(HomeShortcutLayout.allCases, id: \.self) { layout in
Label(layout.displayName, systemImage: layout.systemImage)
.tag(layout)
}
}
.pickerStyle(.segmented)
// Card style (Compact / Regular) navigates to a page with a
// selector and a live preview. Only for cards layout.
if shortcutLayout == .cards {
#if os(macOS)
Button {
showingStyleSettings = true
} label: {
HStack(spacing: 8) {
Label(String(localized: "home.settings.shortcuts.style"), systemImage: "paintpalette")
Spacer()
Text(shortcutCardStyle.displayName)
.foregroundStyle(.secondary)
Image(systemName: "chevron.right")
.font(.caption)
.foregroundStyle(.tertiary)
}
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
#else
SettingsNavigationRow(
"home.settings.shortcuts.style",
systemImage: "paintpalette",
trailing: { Text(shortcutCardStyle.displayName) }
) {
HomeShortcutStyleView(
style: $shortcutCardStyle,
color: $shortcutCardColor,
palette: $shortcutColorfulPalette,
customColors: $shortcutCustomPaletteColors,
onSave: saveSettingsIfLoaded
)
}
#endif
}
#endif
#if os(tvOS)
ForEach(Array(shortcutOrder.enumerated()), id: \.element.id) { index, card in
if card != .downloads {
TVHomeItemRow(
icon: card.icon,
title: card.localizedTitle,
isVisible: shortcutBinding(for: card),
canMoveUp: index > 0 && shortcutOrder[index - 1] != .downloads,
canMoveDown: index < shortcutOrder.count - 1,
onMoveUp: { moveShortcut(at: index, direction: -1) },
onMoveDown: { moveShortcut(at: index, direction: 1) },
canDelete: canDelete(shortcut: card),
onDelete: { removeShortcut(card) }
)
}
}
#else
ForEach(shortcutOrder) { card in
shortcutRowView(for: card)
}
.onMove { from, to in
shortcutOrder.move(fromOffsets: from, toOffset: to)
}
#endif
} header: {
Text(String(localized: "home.settings.shortcuts.header"))
}
}
private var availableShortcutsSection: some View {
Section {
if availableShortcutsByInstance.isEmpty && availableShortcutsByMediaSource.isEmpty {
Text(String(localized: "home.settings.availableShortcuts.empty"))
.foregroundStyle(.secondary)
.italic()
} else {
ForEach(availableShortcutsByInstance, id: \.instance.id) { item in
ForEach(item.cards) { card in
availableShortcutRow(for: card, instance: item.instance)
}
}
ForEach(availableShortcutsByMediaSource, id: \.source.id) { item in
ForEach(item.cards) { card in
availableMediaSourceShortcutRow(for: card, source: item.source)
}
}
}
} header: {
Text(String(localized: "home.settings.availableShortcuts.header"))
} footer: {
Text(String(localized: "home.settings.availableShortcuts.footer"))
}
}
private var sectionsSection: some View {
Section {
Picker(String(localized: "home.settings.sections.layout"), selection: $sectionLayout) {
ForEach(HomeSectionLayout.allCases, id: \.self) { layout in
Label(layout.displayName, systemImage: layout.systemImage)
.tag(layout)
}
}
#if !os(tvOS)
.pickerStyle(.segmented)
#endif
#if os(tvOS)
ForEach(Array(sectionOrder.enumerated()), id: \.element.id) { index, section in
if section != .downloads {
TVHomeItemRow(
icon: section.icon,
title: section.localizedTitle,
isVisible: sectionBinding(for: section),
canMoveUp: index > 0 && sectionOrder[index - 1] != .downloads,
canMoveDown: index < sectionOrder.count - 1,
onMoveUp: { moveSection(at: index, direction: -1) },
onMoveDown: { moveSection(at: index, direction: 1) },
canDelete: canDelete(section: section),
onDelete: { removeSection(section) }
)
}
}
#else
ForEach(sectionOrder) { section in
sectionRowView(for: section)
}
.onMove { from, to in
sectionOrder.move(fromOffsets: from, toOffset: to)
}
#endif
} header: {
Text(String(localized: "home.settings.sections.header"))
} footer: {
Text(String(localized: "home.settings.sections.footer"))
}
}
private var availableSectionsSection: some View {
Section {
if availableSectionsByInstance.isEmpty {
Text(String(localized: "home.settings.availableSections.empty"))
.foregroundStyle(.secondary)
.italic()
} else {
ForEach(availableSectionsByInstance, id: \.instance.id) { item in
ForEach(item.sections) { section in
availableSectionRow(for: section, instance: item.instance)
}
}
}
} header: {
Text(String(localized: "home.settings.availableSections.header"))
} footer: {
Text(String(localized: "home.settings.availableSections.footer"))
}
}
#if os(tvOS)
private func moveShortcut(at index: Int, direction: Int) {
let newIndex = index + direction
guard newIndex >= 0, newIndex < shortcutOrder.count else { return }
shortcutOrder.swapAt(index, newIndex)
}
private func moveSection(at index: Int, direction: Int) {
let newIndex = index + direction
guard newIndex >= 0, newIndex < sectionOrder.count else { return }
sectionOrder.swapAt(index, newIndex)
}
#endif
private var itemsLimitSection: some View {
Section {
#if os(tvOS)
HStack {
Text(String(localized: "home.settings.itemsLimit"))
Spacer()
Button {
if sectionItemsLimit > 1 { sectionItemsLimit -= 1 }
} label: {
Image(systemName: "minus.circle")
}
.buttonStyle(TVSettingsButtonStyle())
Text("\(sectionItemsLimit)")
.monospacedDigit()
Button {
if sectionItemsLimit < 20 { sectionItemsLimit += 1 }
} label: {
Image(systemName: "plus.circle")
}
.buttonStyle(TVSettingsButtonStyle())
}
#else
Stepper(value: $sectionItemsLimit, in: 1...20) {
HStack {
Text(String(localized: "home.settings.itemsLimit"))
Spacer()
Text("\(sectionItemsLimit)")
.foregroundStyle(.secondary)
}
}
#endif
}
}
// MARK: - Bindings
private func shortcutBinding(for card: HomeShortcutItem) -> Binding<Bool> {
Binding(
get: { shortcutVisibility[card] ?? true },
set: { shortcutVisibility[card] = $0 }
)
}
private func sectionBinding(for section: HomeSectionItem) -> Binding<Bool> {
Binding(
get: { sectionVisibility[section] ?? false },
set: { sectionVisibility[section] = $0 }
)
}
// MARK: - Data Management
private func loadSettings() {
guard let settings = settingsManager,
let env = appEnvironment else { return }
shortcutLayout = settings.homeShortcutLayout
shortcutCardStyle = settings.homeShortcutCardStyle
shortcutCardColor = settings.homeShortcutCardColor
shortcutColorfulPalette = settings.homeShortcutColorfulPalette
shortcutCustomPaletteColors = settings.homeShortcutCustomPaletteColors
shortcutOrder = settings.homeShortcutOrder
shortcutVisibility = settings.homeShortcutVisibility
sectionOrder = settings.homeSectionOrder
sectionVisibility = settings.homeSectionVisibility
sectionItemsLimit = settings.homeSectionItemsLimit
sectionLayout = settings.homeSectionLayout
// Load available items
let instances = env.instancesManager.instances
availableShortcutsByInstance = settings.allAvailableShortcuts(instances: instances)
availableSectionsByInstance = settings.allAvailableSections(instances: instances)
let sources = env.mediaSourcesManager.sources
availableShortcutsByMediaSource = settings.allAvailableMediaSourceShortcuts(sources: sources)
}
private func saveSettingsIfLoaded() {
guard hasLoadedSettings else { return }
saveSettings()
}
private func saveSettings() {
guard let settings = settingsManager else { return }
settings.homeShortcutLayout = shortcutLayout
settings.homeShortcutCardStyle = shortcutCardStyle
settings.homeShortcutCardColor = shortcutCardColor
settings.homeShortcutColorfulPalette = shortcutColorfulPalette
settings.homeShortcutCustomPaletteColors = shortcutCustomPaletteColors
settings.homeShortcutOrder = shortcutOrder
settings.homeShortcutVisibility = shortcutVisibility
settings.homeSectionOrder = sectionOrder
settings.homeSectionVisibility = sectionVisibility
settings.homeSectionItemsLimit = sectionItemsLimit
settings.homeSectionLayout = sectionLayout
}
// MARK: - Available Item Management
private func addShortcut(_ card: HomeShortcutItem) {
// Add to local state
if !shortcutOrder.contains(where: { $0.id == card.id }) {
shortcutOrder.append(card)
shortcutVisibility[card] = true // Visible by default
}
// Persist to settings
switch card {
case .instanceContent(let instanceID, let contentType):
settingsManager?.addToHome(instanceID: instanceID, contentType: contentType, asCard: true)
case .mediaSource(let sourceID):
settingsManager?.addToHome(sourceID: sourceID, asCard: true)
default:
break
}
// Reload available items
loadSettings()
}
private func addSection(_ section: HomeSectionItem) {
// Add to local state
if !sectionOrder.contains(where: { $0.id == section.id }) {
sectionOrder.append(section)
sectionVisibility[section] = true // Visible by default
}
// Persist to settings
switch section {
case .instanceContent(let instanceID, let contentType):
settingsManager?.addToHome(instanceID: instanceID, contentType: contentType, asCard: false)
default:
break
}
// Reload available items
loadSettings()
}
private func removeShortcut(_ card: HomeShortcutItem) {
// Remove from local state
shortcutOrder.removeAll { $0.id == card.id }
shortcutVisibility.removeValue(forKey: card)
// Persist to settings
switch card {
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 removeSection(_ section: HomeSectionItem) {
// Remove from local state
sectionOrder.removeAll { $0.id == section.id }
sectionVisibility.removeValue(forKey: section)
// Persist to settings
switch section {
case .instanceContent(let instanceID, let contentType):
settingsManager?.removeFromHome(instanceID: instanceID, contentType: contentType)
default:
break
}
// Reload available items
loadSettings()
}
private func canDelete(shortcut: HomeShortcutItem) -> Bool {
if case .instanceContent = shortcut {
return true
}
if case .mediaSource = shortcut {
return true
}
return false
}
private func canDelete(section: HomeSectionItem) -> Bool {
if case .instanceContent = section {
return true
}
return false
}
// MARK: - Card and Section Row Views
@ViewBuilder
private func shortcutRowView(for card: HomeShortcutItem) -> some View {
switch card {
case .instanceContent(let instanceID, let contentType):
if let instance = instanceFromID(instanceID) {
let isDisabled = !instance.isEnabled || (contentType == .feed && !isLoggedIn(instance))
HomeItemRow(
icon: contentType.icon,
title: "\(instance.displayName) - \(contentType.localizedTitle)",
isVisible: shortcutBinding(for: card),
isDisabled: isDisabled,
disabledReason: disabledReason(instance: instance, contentType: contentType)
)
#if os(iOS)
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button(role: .destructive) {
removeShortcut(card)
} label: {
Label(String(localized: "home.settings.remove"), systemImage: "trash")
}
.tint(.red)
}
#endif
.contextMenu {
Button(role: .destructive) {
removeShortcut(card)
} label: {
Label(String(localized: "home.settings.remove"), systemImage: "trash")
}
}
}
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: shortcutBinding(for: card),
isDisabled: isDisabled,
disabledReason: isDisabled ? String(localized: "home.settings.sourceDisabled") : nil
)
#if os(iOS)
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button(role: .destructive) {
removeShortcut(card)
} label: {
Label(String(localized: "home.settings.remove"), systemImage: "trash")
}
.tint(.red)
}
#endif
.contextMenu {
Button(role: .destructive) {
removeShortcut(card)
} label: {
Label(String(localized: "home.settings.remove"), systemImage: "trash")
}
}
}
default:
HomeItemRow(
icon: card.icon,
title: card.localizedTitle,
isVisible: shortcutBinding(for: card)
)
}
}
@ViewBuilder
private func sectionRowView(for section: HomeSectionItem) -> some View {
switch section {
case .instanceContent(let instanceID, let contentType):
if let instance = instanceFromID(instanceID) {
let isDisabled = !instance.isEnabled || (contentType == .feed && !isLoggedIn(instance))
HomeItemRow(
icon: contentType.icon,
title: "\(instance.displayName) - \(contentType.localizedTitle)",
isVisible: sectionBinding(for: section),
isDisabled: isDisabled,
disabledReason: disabledReason(instance: instance, contentType: contentType)
)
#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,
title: section.localizedTitle,
isVisible: sectionBinding(for: section)
)
}
}
@ViewBuilder
private func availableShortcutRow(for card: HomeShortcutItem, instance: Instance) -> some View {
if case .instanceContent(_, let contentType) = card {
let isDisabled = !instance.isEnabled || (contentType == .feed && !isLoggedIn(instance))
HStack {
Image(systemName: contentType.icon)
.frame(width: 24)
.foregroundStyle(isDisabled ? .tertiary : .secondary)
Text("\(instance.displayName) - \(contentType.localizedTitle)")
.foregroundStyle(isDisabled ? .tertiary : .primary)
Spacer()
Button {
addShortcut(card)
} label: {
Image(systemName: "plus.circle.fill")
.foregroundStyle(.green)
.imageScale(.large)
}
.buttonStyle(.plain)
.disabled(isDisabled)
}
#if !os(tvOS)
.help(isDisabled ? (disabledReason(instance: instance, contentType: contentType) ?? "") : "")
#endif
}
}
@ViewBuilder
private func availableSectionRow(for section: HomeSectionItem, instance: Instance) -> some View {
if case .instanceContent(_, let contentType) = section {
let isDisabled = !instance.isEnabled || (contentType == .feed && !isLoggedIn(instance))
HStack {
Image(systemName: contentType.icon)
.frame(width: 24)
.foregroundStyle(isDisabled ? .tertiary : .secondary)
Text("\(instance.displayName) - \(contentType.localizedTitle)")
.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 ? (disabledReason(instance: instance, contentType: contentType) ?? "") : "")
#endif
}
}
private func instanceFromID(_ id: UUID) -> Instance? {
appEnvironment?.instancesManager.instances.first(where: { $0.id == id })
}
private func isLoggedIn(_ instance: Instance) -> Bool {
guard instance.supportsFeed else { return false }
return appEnvironment?.credentialsManager(for: instance)?.isLoggedIn(for: instance) ?? false
}
private func disabledReason(instance: Instance, contentType: InstanceContentType) -> String? {
if !instance.isEnabled {
return String(localized: "home.settings.instanceDisabled")
}
if contentType == .feed && !isLoggedIn(instance) {
return String(localized: "home.settings.feedRequiresLogin")
}
return nil
}
@ViewBuilder
private func availableMediaSourceShortcutRow(for card: HomeShortcutItem, source: MediaSource) -> some View {
if case .mediaSource = card {
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 {
addShortcut(card)
} 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
#if os(tvOS)
private struct TVHomeItemRow: View {
let icon: String
let title: String
@Binding var isVisible: Bool
let canMoveUp: Bool
let canMoveDown: Bool
let onMoveUp: () -> Void
let onMoveDown: () -> Void
var canDelete: Bool = false
var onDelete: (() -> Void)? = nil
@State private var showingDeleteConfirmation = false
var body: some View {
HStack(spacing: 12) {
// Move buttons - compact style
VStack(spacing: 4) {
Button {
onMoveUp()
} label: {
Image(systemName: "chevron.up")
.font(.caption)
.foregroundStyle(canMoveUp ? .primary : .tertiary)
.frame(width: 30, height: 24)
}
.buttonStyle(TVCompactButtonStyle())
.disabled(!canMoveUp)
Button {
onMoveDown()
} label: {
Image(systemName: "chevron.down")
.font(.caption)
.foregroundStyle(canMoveDown ? .primary : .tertiary)
.frame(width: 30, height: 24)
}
.buttonStyle(TVCompactButtonStyle())
.disabled(!canMoveDown)
}
// Main content - toggle visibility
Button {
isVisible.toggle()
} label: {
HStack {
Image(systemName: icon)
.frame(width: 24)
.foregroundStyle(.secondary)
Text(title)
.foregroundStyle(.primary)
Spacer()
Image(systemName: isVisible ? "checkmark.circle.fill" : "circle")
.foregroundColor(isVisible ? .green : .secondary)
.font(.title3)
}
.frame(maxWidth: .infinity)
}
.buttonStyle(TVFormRowButtonStyle())
// Delete button (only for instance content)
if canDelete {
Button {
showingDeleteConfirmation = true
} label: {
Image(systemName: "trash")
.font(.caption)
.foregroundStyle(.red)
.frame(width: 30, height: 24)
}
.buttonStyle(TVCompactButtonStyle())
.alert(String(localized: "home.removeConfirmation.title"), isPresented: $showingDeleteConfirmation) {
Button(String(localized: "common.cancel"), role: .cancel) { }
Button("common.remove", role: .destructive) {
onDelete?()
}
}
}
}
.padding(.vertical, 4)
}
}
/// Compact button style for small controls like up/down arrows
private struct TVCompactButtonStyle: ButtonStyle {
@Environment(\.isFocused) private var isFocused
func makeBody(configuration: Configuration) -> some View {
configuration.label
.background(
RoundedRectangle(cornerRadius: 6)
.fill(isFocused ? Color.white.opacity(0.2) : Color.clear)
)
.scaleEffect(configuration.isPressed ? 0.9 : (isFocused ? 1.1 : 1.0))
.animation(.easeInOut(duration: 0.1), value: isFocused)
.animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
}
}
#endif
private struct HomeItemRow: View {
let icon: String
let title: String
@Binding var isVisible: Bool
var isDisabled: Bool = false
var disabledReason: String? = nil
var body: some View {
#if os(tvOS)
Button {
if !isDisabled {
isVisible.toggle()
}
} label: {
HStack {
Image(systemName: icon)
.frame(width: 24)
.foregroundStyle(isDisabled ? .tertiary : .secondary)
Text(title)
.foregroundStyle(isDisabled ? .tertiary : .primary)
Spacer()
Image(systemName: isVisible ? "checkmark.circle.fill" : "circle")
.foregroundColor(isDisabled ? .secondary.opacity(0.5) : (isVisible ? .green : .secondary))
.font(.title3)
}
.contentShape(Rectangle())
}
.buttonStyle(TVFormRowButtonStyle())
.disabled(isDisabled)
#else
HStack {
Image(systemName: icon)
.frame(width: 24)
.foregroundStyle(isDisabled ? .tertiary : .secondary)
Text(title)
.foregroundStyle(isDisabled ? .tertiary : .primary)
Spacer()
Toggle("", isOn: $isVisible)
.labelsHidden()
.disabled(isDisabled)
}
.help(disabledReason ?? "")
#endif
}
}
// MARK: - Preview
#Preview {
HomeSettingsView()
.appEnvironment(.preview)
}