mirror of
https://github.com/yattee/yattee.git
synced 2026-07-20 14:22:02 +00:00
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
619 lines
22 KiB
Swift
619 lines
22 KiB
Swift
//
|
|
// SidebarSettingsView.swift
|
|
// Yattee
|
|
//
|
|
// Settings for customizing sidebar content on iOS 18+, macOS, and tvOS 18+.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct SidebarSettingsView: View {
|
|
@Environment(\.appEnvironment) private var appEnvironment
|
|
|
|
// Local state for main navigation editing (copied from settings on appear, saved on dismiss)
|
|
@State private var mainItemOrder: [SidebarMainItem] = []
|
|
@State private var mainItemVisibility: [SidebarMainItem: Bool] = [:]
|
|
|
|
private var settingsManager: SettingsManager? { appEnvironment?.settingsManager }
|
|
|
|
// MARK: - Sources Bindings
|
|
|
|
private var sourcesEnabledBinding: Binding<Bool> {
|
|
Binding(
|
|
get: { settingsManager?.sidebarSourcesEnabled ?? true },
|
|
set: { settingsManager?.sidebarSourcesEnabled = $0 }
|
|
)
|
|
}
|
|
|
|
private var sourceSortBinding: Binding<SidebarSourceSort> {
|
|
Binding(
|
|
get: { settingsManager?.sidebarSourceSort ?? .name },
|
|
set: { settingsManager?.sidebarSourceSort = $0 }
|
|
)
|
|
}
|
|
|
|
private var sourcesLimitEnabledBinding: Binding<Bool> {
|
|
Binding(
|
|
get: { settingsManager?.sidebarSourcesLimitEnabled ?? false },
|
|
set: { settingsManager?.sidebarSourcesLimitEnabled = $0 }
|
|
)
|
|
}
|
|
|
|
private var maxSourcesBinding: Binding<Int> {
|
|
Binding(
|
|
get: { settingsManager?.sidebarMaxSources ?? SettingsManager.defaultSidebarMaxSources },
|
|
set: { settingsManager?.sidebarMaxSources = $0 }
|
|
)
|
|
}
|
|
|
|
// MARK: - Channels Bindings
|
|
|
|
private var maxChannelsBinding: Binding<Int> {
|
|
Binding(
|
|
get: { settingsManager?.sidebarMaxChannels ?? SettingsManager.defaultSidebarMaxChannels },
|
|
set: { settingsManager?.sidebarMaxChannels = $0 }
|
|
)
|
|
}
|
|
|
|
private var channelSortBinding: Binding<SidebarChannelSort> {
|
|
Binding(
|
|
get: { settingsManager?.sidebarChannelSort ?? .alphabetical },
|
|
set: { settingsManager?.sidebarChannelSort = $0 }
|
|
)
|
|
}
|
|
|
|
private var channelsLimitEnabledBinding: Binding<Bool> {
|
|
Binding(
|
|
get: { settingsManager?.sidebarChannelsLimitEnabled ?? true },
|
|
set: { settingsManager?.sidebarChannelsLimitEnabled = $0 }
|
|
)
|
|
}
|
|
|
|
private var channelsEnabledBinding: Binding<Bool> {
|
|
Binding(
|
|
get: { settingsManager?.sidebarChannelsEnabled ?? true },
|
|
set: { settingsManager?.sidebarChannelsEnabled = $0 }
|
|
)
|
|
}
|
|
|
|
// MARK: - Playlists Bindings
|
|
|
|
private var maxPlaylistsBinding: Binding<Int> {
|
|
Binding(
|
|
get: { settingsManager?.sidebarMaxPlaylists ?? SettingsManager.defaultSidebarMaxPlaylists },
|
|
set: { settingsManager?.sidebarMaxPlaylists = $0 }
|
|
)
|
|
}
|
|
|
|
private var playlistSortBinding: Binding<SidebarPlaylistSort> {
|
|
Binding(
|
|
get: { settingsManager?.sidebarPlaylistSort ?? .alphabetical },
|
|
set: { settingsManager?.sidebarPlaylistSort = $0 }
|
|
)
|
|
}
|
|
|
|
private var playlistsLimitEnabledBinding: Binding<Bool> {
|
|
Binding(
|
|
get: { settingsManager?.sidebarPlaylistsLimitEnabled ?? false },
|
|
set: { settingsManager?.sidebarPlaylistsLimitEnabled = $0 }
|
|
)
|
|
}
|
|
|
|
private var playlistsEnabledBinding: Binding<Bool> {
|
|
Binding(
|
|
get: { settingsManager?.sidebarPlaylistsEnabled ?? true },
|
|
set: { settingsManager?.sidebarPlaylistsEnabled = $0 }
|
|
)
|
|
}
|
|
|
|
// MARK: - Startup Binding
|
|
|
|
private var startupTabBinding: Binding<SidebarMainItem> {
|
|
Binding(
|
|
get: { settingsManager?.sidebarStartupTab ?? .home },
|
|
set: { settingsManager?.sidebarStartupTab = $0 }
|
|
)
|
|
}
|
|
|
|
/// Valid startup tabs based on current visibility settings.
|
|
private var validStartupTabs: [SidebarMainItem] {
|
|
// Filter main items by visibility (respecting required items and platform availability)
|
|
let visibility = mainItemVisibility
|
|
return mainItemOrder
|
|
.filter { $0.isAvailableOnCurrentPlatform }
|
|
.filter { $0.isRequired || (visibility[$0] ?? true) }
|
|
}
|
|
|
|
var body: some View {
|
|
#if os(tvOS)
|
|
NavigationStack {
|
|
sidebarList
|
|
}
|
|
#else
|
|
sidebarList
|
|
#endif
|
|
}
|
|
|
|
private var sidebarList: some View {
|
|
List {
|
|
startupSection
|
|
mainNavigationSection
|
|
sourcesSection
|
|
channelsSection
|
|
playlistsSection
|
|
}
|
|
#if os(iOS)
|
|
.environment(\.editMode, .constant(.active))
|
|
#endif
|
|
#if os(macOS)
|
|
.listStyle(.inset)
|
|
#endif
|
|
.opaqueSettingsFormBackground()
|
|
#if !os(tvOS)
|
|
.navigationTitle(String(localized: "settings.sidebar.title"))
|
|
#endif
|
|
#if os(iOS)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
#endif
|
|
.onAppear {
|
|
loadMainNavigationSettings()
|
|
}
|
|
}
|
|
|
|
// MARK: - Startup Section
|
|
|
|
private var startupSection: some View {
|
|
Section {
|
|
PlatformMenuPicker(String(localized: "settings.sidebar.startup.tab"), selection: startupTabBinding) {
|
|
ForEach(validStartupTabs) { item in
|
|
Text(item.localizedTitle).tag(item)
|
|
}
|
|
}
|
|
} header: {
|
|
Text(String(localized: "settings.sidebar.startup.header"))
|
|
} footer: {
|
|
Text(String(localized: "settings.sidebar.startup.footer"))
|
|
}
|
|
}
|
|
|
|
// MARK: - Main Navigation Section
|
|
|
|
private var mainNavigationSection: some View {
|
|
Section {
|
|
#if os(tvOS)
|
|
let availableItems = mainItemOrder.filter { $0.isAvailableOnCurrentPlatform }
|
|
ForEach(Array(availableItems.enumerated()), id: \.element.id) { index, item in
|
|
TVSidebarMainItemRow(
|
|
icon: item.icon,
|
|
title: item.localizedTitle,
|
|
isRequired: item.isRequired,
|
|
isVisible: mainItemBinding(for: item),
|
|
canMoveUp: index > 0,
|
|
canMoveDown: index < availableItems.count - 1,
|
|
onMoveUp: { moveMainItem(at: index, direction: -1) },
|
|
onMoveDown: { moveMainItem(at: index, direction: 1) }
|
|
)
|
|
}
|
|
#else
|
|
ForEach(mainItemOrder.filter { $0.isAvailableOnCurrentPlatform }) { item in
|
|
SidebarMainItemRow(
|
|
icon: item.icon,
|
|
title: item.localizedTitle,
|
|
isRequired: item.isRequired,
|
|
isVisible: mainItemBinding(for: item)
|
|
)
|
|
}
|
|
.onMove { from, to in
|
|
// Filter to get only platform-available items for correct index mapping
|
|
let availableItems = mainItemOrder.filter { $0.isAvailableOnCurrentPlatform }
|
|
|
|
// Get the items being moved
|
|
guard let fromIndex = from.first,
|
|
fromIndex < availableItems.count,
|
|
to <= availableItems.count else { return }
|
|
|
|
let movedItem = availableItems[fromIndex]
|
|
let targetItem = to < availableItems.count ? availableItems[to] : nil
|
|
|
|
// Find actual indices in mainItemOrder
|
|
guard let actualFromIndex = mainItemOrder.firstIndex(of: movedItem) else { return }
|
|
|
|
// Remove from current position
|
|
mainItemOrder.remove(at: actualFromIndex)
|
|
|
|
// Find target position
|
|
if let targetItem = targetItem,
|
|
let actualToIndex = mainItemOrder.firstIndex(of: targetItem) {
|
|
mainItemOrder.insert(movedItem, at: actualToIndex)
|
|
} else {
|
|
mainItemOrder.append(movedItem)
|
|
}
|
|
|
|
// Save immediately
|
|
saveMainNavigationSettings()
|
|
}
|
|
#endif
|
|
} header: {
|
|
Text(String(localized: "settings.sidebar.mainNavigation.header"))
|
|
} footer: {
|
|
Text(String(localized: "settings.sidebar.mainNavigation.footer"))
|
|
}
|
|
}
|
|
|
|
#if os(tvOS)
|
|
private func moveMainItem(at index: Int, direction: Int) {
|
|
let available = mainItemOrder.filter { $0.isAvailableOnCurrentPlatform }
|
|
let newIndex = index + direction
|
|
guard index >= 0, index < available.count,
|
|
newIndex >= 0, newIndex < available.count else { return }
|
|
|
|
let movedItem = available[index]
|
|
let neighborItem = available[newIndex]
|
|
|
|
guard let fromActual = mainItemOrder.firstIndex(of: movedItem),
|
|
let toActual = mainItemOrder.firstIndex(of: neighborItem) else { return }
|
|
|
|
mainItemOrder.swapAt(fromActual, toActual)
|
|
saveMainNavigationSettings()
|
|
}
|
|
#endif
|
|
|
|
private func mainItemBinding(for item: SidebarMainItem) -> Binding<Bool> {
|
|
Binding(
|
|
get: { mainItemVisibility[item] ?? true },
|
|
set: { newValue in
|
|
mainItemVisibility[item] = newValue
|
|
saveMainNavigationSettings()
|
|
|
|
// Reset startup tab to Home if the hidden item was the startup tab
|
|
if !newValue, settingsManager?.sidebarStartupTab == item {
|
|
settingsManager?.sidebarStartupTab = .home
|
|
}
|
|
}
|
|
)
|
|
}
|
|
|
|
// MARK: - Main Navigation Data Management
|
|
|
|
private func loadMainNavigationSettings() {
|
|
guard let settings = settingsManager else { return }
|
|
mainItemOrder = settings.sidebarMainItemOrder
|
|
mainItemVisibility = settings.sidebarMainItemVisibility
|
|
}
|
|
|
|
private func saveMainNavigationSettings() {
|
|
guard let settings = settingsManager else { return }
|
|
settings.sidebarMainItemOrder = mainItemOrder
|
|
settings.sidebarMainItemVisibility = mainItemVisibility
|
|
notifySidebarSettingsChanged()
|
|
}
|
|
|
|
// MARK: - Sections
|
|
|
|
private var sourcesSection: some View {
|
|
Section {
|
|
// Show in Sidebar toggle
|
|
Toggle(String(localized: "settings.sidebar.showInSidebar"), isOn: sourcesEnabledBinding)
|
|
.onChange(of: sourcesEnabledBinding.wrappedValue) {
|
|
notifySidebarSettingsChanged()
|
|
}
|
|
|
|
// Source sort order
|
|
PlatformMenuPicker(String(localized: "settings.sidebar.sourceSort"), selection: sourceSortBinding) {
|
|
ForEach(SidebarSourceSort.allCases) { sort in
|
|
Text(sort.localizedTitle).tag(sort)
|
|
}
|
|
}
|
|
.disabled(!sourcesEnabledBinding.wrappedValue)
|
|
.onChange(of: sourceSortBinding.wrappedValue) {
|
|
notifySidebarSettingsChanged()
|
|
}
|
|
|
|
// Limit sources toggle
|
|
Toggle(String(localized: "settings.sidebar.sourcesLimitEnabled"), isOn: sourcesLimitEnabledBinding)
|
|
.disabled(!sourcesEnabledBinding.wrappedValue)
|
|
.onChange(of: sourcesLimitEnabledBinding.wrappedValue) {
|
|
notifySidebarSettingsChanged()
|
|
}
|
|
|
|
// Max sources (only shown when limit is enabled)
|
|
if sourcesLimitEnabledBinding.wrappedValue {
|
|
#if os(tvOS)
|
|
// tvOS uses Picker instead of Slider (Slider/Stepper unavailable)
|
|
PlatformMenuPicker(String(localized: "settings.sidebar.maxSources"), selection: maxSourcesBinding) {
|
|
ForEach([5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 60, 70, 80, 90, 100], id: \.self) { value in
|
|
Text("\(value)").tag(value)
|
|
}
|
|
}
|
|
.disabled(!sourcesEnabledBinding.wrappedValue)
|
|
.onChange(of: maxSourcesBinding.wrappedValue) {
|
|
notifySidebarSettingsChanged()
|
|
}
|
|
#else
|
|
// Max sources slider
|
|
HStack {
|
|
Text(String(localized: "settings.sidebar.maxSources"))
|
|
Spacer()
|
|
Text("\(maxSourcesBinding.wrappedValue)")
|
|
.foregroundStyle(.secondary)
|
|
.monospacedDigit()
|
|
}
|
|
.foregroundStyle(sourcesEnabledBinding.wrappedValue ? .primary : .secondary)
|
|
|
|
Slider(
|
|
value: Binding(
|
|
get: { Double(maxSourcesBinding.wrappedValue) },
|
|
set: { maxSourcesBinding.wrappedValue = Int($0) }
|
|
),
|
|
in: 5...100,
|
|
step: 5
|
|
) { editing in
|
|
if !editing {
|
|
notifySidebarSettingsChanged()
|
|
}
|
|
}
|
|
.disabled(!sourcesEnabledBinding.wrappedValue)
|
|
#endif
|
|
}
|
|
} header: {
|
|
Text(String(localized: "settings.sidebar.sources.header"))
|
|
} footer: {
|
|
Text(String(localized: "settings.sidebar.sources.footer"))
|
|
}
|
|
}
|
|
|
|
private var channelsSection: some View {
|
|
Section {
|
|
// Show in Sidebar toggle (first)
|
|
Toggle(String(localized: "settings.sidebar.showInSidebar"), isOn: channelsEnabledBinding)
|
|
.onChange(of: channelsEnabledBinding.wrappedValue) {
|
|
notifySidebarSettingsChanged()
|
|
}
|
|
|
|
// Channel sort order
|
|
PlatformMenuPicker(String(localized: "settings.sidebar.channelSort"), selection: channelSortBinding) {
|
|
ForEach(SidebarChannelSort.allCases.filter { $0 != .custom }) { sort in
|
|
Text(sort.localizedTitle).tag(sort)
|
|
}
|
|
}
|
|
.disabled(!channelsEnabledBinding.wrappedValue)
|
|
.onChange(of: channelSortBinding.wrappedValue) {
|
|
notifySidebarSettingsChanged()
|
|
}
|
|
|
|
// Limit channels toggle
|
|
Toggle(String(localized: "settings.sidebar.channelsLimitEnabled"), isOn: channelsLimitEnabledBinding)
|
|
.disabled(!channelsEnabledBinding.wrappedValue)
|
|
.onChange(of: channelsLimitEnabledBinding.wrappedValue) {
|
|
notifySidebarSettingsChanged()
|
|
}
|
|
|
|
// Max channels (only shown when limit is enabled)
|
|
if channelsLimitEnabledBinding.wrappedValue {
|
|
#if os(tvOS)
|
|
// tvOS uses Picker instead of Slider (Slider/Stepper unavailable)
|
|
PlatformMenuPicker(String(localized: "settings.sidebar.maxChannels"), selection: maxChannelsBinding) {
|
|
ForEach([5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 60, 70, 80, 90, 100], id: \.self) { value in
|
|
Text("\(value)").tag(value)
|
|
}
|
|
}
|
|
.disabled(!channelsEnabledBinding.wrappedValue)
|
|
.onChange(of: maxChannelsBinding.wrappedValue) {
|
|
notifySidebarSettingsChanged()
|
|
}
|
|
#else
|
|
// Max channels slider
|
|
HStack {
|
|
Text(String(localized: "settings.sidebar.maxChannels"))
|
|
Spacer()
|
|
Text("\(maxChannelsBinding.wrappedValue)")
|
|
.foregroundStyle(.secondary)
|
|
.monospacedDigit()
|
|
}
|
|
.foregroundStyle(channelsEnabledBinding.wrappedValue ? .primary : .secondary)
|
|
|
|
Slider(
|
|
value: Binding(
|
|
get: { Double(maxChannelsBinding.wrappedValue) },
|
|
set: { maxChannelsBinding.wrappedValue = Int($0) }
|
|
),
|
|
in: 5...100,
|
|
step: 5
|
|
) { editing in
|
|
if !editing {
|
|
notifySidebarSettingsChanged()
|
|
}
|
|
}
|
|
.disabled(!channelsEnabledBinding.wrappedValue)
|
|
#endif
|
|
}
|
|
} header: {
|
|
Text(String(localized: "settings.sidebar.channels.header"))
|
|
} footer: {
|
|
Text(String(localized: "settings.sidebar.channels.footer"))
|
|
}
|
|
}
|
|
|
|
private var playlistsSection: some View {
|
|
Section {
|
|
// Show in Sidebar toggle (first)
|
|
Toggle(String(localized: "settings.sidebar.showInSidebar"), isOn: playlistsEnabledBinding)
|
|
.onChange(of: playlistsEnabledBinding.wrappedValue) {
|
|
notifySidebarSettingsChanged()
|
|
}
|
|
|
|
// Playlist sort order
|
|
PlatformMenuPicker(String(localized: "settings.sidebar.playlistSort"), selection: playlistSortBinding) {
|
|
ForEach(SidebarPlaylistSort.allCases) { sort in
|
|
Text(sort.localizedTitle).tag(sort)
|
|
}
|
|
}
|
|
.disabled(!playlistsEnabledBinding.wrappedValue)
|
|
.onChange(of: playlistSortBinding.wrappedValue) {
|
|
notifySidebarSettingsChanged()
|
|
}
|
|
|
|
// Limit playlists toggle
|
|
Toggle(String(localized: "settings.sidebar.playlistsLimitEnabled"), isOn: playlistsLimitEnabledBinding)
|
|
.disabled(!playlistsEnabledBinding.wrappedValue)
|
|
.onChange(of: playlistsLimitEnabledBinding.wrappedValue) {
|
|
notifySidebarSettingsChanged()
|
|
}
|
|
|
|
// Max playlists (only shown when limit is enabled)
|
|
if playlistsLimitEnabledBinding.wrappedValue {
|
|
#if os(tvOS)
|
|
// tvOS uses Picker instead of Slider (Slider/Stepper unavailable)
|
|
PlatformMenuPicker(String(localized: "settings.sidebar.maxPlaylists"), selection: maxPlaylistsBinding) {
|
|
ForEach([5, 10, 15, 20, 25, 30], id: \.self) { value in
|
|
Text("\(value)").tag(value)
|
|
}
|
|
}
|
|
.disabled(!playlistsEnabledBinding.wrappedValue)
|
|
.onChange(of: maxPlaylistsBinding.wrappedValue) {
|
|
notifySidebarSettingsChanged()
|
|
}
|
|
#else
|
|
// Max playlists slider
|
|
HStack {
|
|
Text(String(localized: "settings.sidebar.maxPlaylists"))
|
|
Spacer()
|
|
Text("\(maxPlaylistsBinding.wrappedValue)")
|
|
.foregroundStyle(.secondary)
|
|
.monospacedDigit()
|
|
}
|
|
.foregroundStyle(playlistsEnabledBinding.wrappedValue ? .primary : .secondary)
|
|
|
|
Slider(
|
|
value: Binding(
|
|
get: { Double(maxPlaylistsBinding.wrappedValue) },
|
|
set: { maxPlaylistsBinding.wrappedValue = Int($0) }
|
|
),
|
|
in: 5...30,
|
|
step: 5
|
|
) { editing in
|
|
if !editing {
|
|
notifySidebarSettingsChanged()
|
|
}
|
|
}
|
|
.disabled(!playlistsEnabledBinding.wrappedValue)
|
|
#endif
|
|
}
|
|
} header: {
|
|
Text(String(localized: "settings.sidebar.playlists.header"))
|
|
} footer: {
|
|
Text(String(localized: "settings.sidebar.playlists.footer"))
|
|
}
|
|
}
|
|
|
|
// MARK: - Notifications
|
|
|
|
private func notifySidebarSettingsChanged() {
|
|
NotificationCenter.default.post(name: .sidebarSettingsDidChange, object: nil)
|
|
}
|
|
}
|
|
|
|
// MARK: - Sidebar Main Item Row
|
|
|
|
private struct SidebarMainItemRow: View {
|
|
let icon: String
|
|
let title: String
|
|
let isRequired: Bool
|
|
@Binding var isVisible: Bool
|
|
|
|
var body: some View {
|
|
HStack {
|
|
Image(systemName: icon)
|
|
.frame(width: 24)
|
|
.foregroundStyle(.secondary)
|
|
Text(title)
|
|
Spacer()
|
|
Toggle("", isOn: $isVisible)
|
|
.labelsHidden()
|
|
.disabled(isRequired)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - tvOS Sidebar Main Item Row
|
|
|
|
#if os(tvOS)
|
|
private struct TVSidebarMainItemRow: View {
|
|
let icon: String
|
|
let title: String
|
|
let isRequired: Bool
|
|
@Binding var isVisible: Bool
|
|
let canMoveUp: Bool
|
|
let canMoveDown: Bool
|
|
let onMoveUp: () -> Void
|
|
let onMoveDown: () -> Void
|
|
|
|
var body: some View {
|
|
HStack(spacing: 12) {
|
|
VStack(spacing: 4) {
|
|
Button(action: onMoveUp) {
|
|
Image(systemName: "chevron.up")
|
|
.font(.caption)
|
|
.foregroundStyle(canMoveUp ? .primary : .tertiary)
|
|
.frame(width: 30, height: 24)
|
|
}
|
|
.buttonStyle(TVSidebarCompactButtonStyle())
|
|
.disabled(!canMoveUp)
|
|
|
|
Button(action: onMoveDown) {
|
|
Image(systemName: "chevron.down")
|
|
.font(.caption)
|
|
.foregroundStyle(canMoveDown ? .primary : .tertiary)
|
|
.frame(width: 30, height: 24)
|
|
}
|
|
.buttonStyle(TVSidebarCompactButtonStyle())
|
|
.disabled(!canMoveDown)
|
|
}
|
|
|
|
Button {
|
|
guard !isRequired else { return }
|
|
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(isRequired ? .secondary : (isVisible ? .green : .secondary))
|
|
.font(.title3)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
}
|
|
.buttonStyle(TVFormRowButtonStyle())
|
|
.disabled(isRequired)
|
|
}
|
|
.padding(.vertical, 4)
|
|
}
|
|
}
|
|
|
|
private struct TVSidebarCompactButtonStyle: 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
|
|
|
|
// MARK: - Preview
|
|
|
|
#Preview {
|
|
SidebarSettingsView()
|
|
.appEnvironment(.preview)
|
|
}
|