// // 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 { Binding( get: { settingsManager?.sidebarSourcesEnabled ?? true }, set: { settingsManager?.sidebarSourcesEnabled = $0 } ) } private var sourceSortBinding: Binding { Binding( get: { settingsManager?.sidebarSourceSort ?? .name }, set: { settingsManager?.sidebarSourceSort = $0 } ) } private var sourcesLimitEnabledBinding: Binding { Binding( get: { settingsManager?.sidebarSourcesLimitEnabled ?? false }, set: { settingsManager?.sidebarSourcesLimitEnabled = $0 } ) } private var maxSourcesBinding: Binding { Binding( get: { settingsManager?.sidebarMaxSources ?? SettingsManager.defaultSidebarMaxSources }, set: { settingsManager?.sidebarMaxSources = $0 } ) } // MARK: - Channels Bindings private var maxChannelsBinding: Binding { Binding( get: { settingsManager?.sidebarMaxChannels ?? SettingsManager.defaultSidebarMaxChannels }, set: { settingsManager?.sidebarMaxChannels = $0 } ) } private var channelSortBinding: Binding { Binding( get: { settingsManager?.sidebarChannelSort ?? .alphabetical }, set: { settingsManager?.sidebarChannelSort = $0 } ) } private var channelsLimitEnabledBinding: Binding { Binding( get: { settingsManager?.sidebarChannelsLimitEnabled ?? true }, set: { settingsManager?.sidebarChannelsLimitEnabled = $0 } ) } private var channelsEnabledBinding: Binding { Binding( get: { settingsManager?.sidebarChannelsEnabled ?? true }, set: { settingsManager?.sidebarChannelsEnabled = $0 } ) } // MARK: - Playlists Bindings private var maxPlaylistsBinding: Binding { Binding( get: { settingsManager?.sidebarMaxPlaylists ?? SettingsManager.defaultSidebarMaxPlaylists }, set: { settingsManager?.sidebarMaxPlaylists = $0 } ) } private var playlistSortBinding: Binding { Binding( get: { settingsManager?.sidebarPlaylistSort ?? .alphabetical }, set: { settingsManager?.sidebarPlaylistSort = $0 } ) } private var playlistsLimitEnabledBinding: Binding { Binding( get: { settingsManager?.sidebarPlaylistsLimitEnabled ?? false }, set: { settingsManager?.sidebarPlaylistsLimitEnabled = $0 } ) } private var playlistsEnabledBinding: Binding { Binding( get: { settingsManager?.sidebarPlaylistsEnabled ?? true }, set: { settingsManager?.sidebarPlaylistsEnabled = $0 } ) } // MARK: - Startup Binding private var startupTabBinding: Binding { 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 { NavigationStack { List { startupSection mainNavigationSection sourcesSection channelsSection playlistsSection } #if os(iOS) || os(tvOS) .environment(\.editMode, .constant(.active)) #endif .navigationTitle(String(localized: "settings.sidebar.title")) #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif .onAppear { loadMainNavigationSettings() } } #if os(macOS) .frame(minWidth: 400, minHeight: 300) #endif } // MARK: - Startup Section private var startupSection: some View { Section { Picker(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 { 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() } } header: { Text(String(localized: "settings.sidebar.mainNavigation.header")) } footer: { Text(String(localized: "settings.sidebar.mainNavigation.footer")) } } private func mainItemBinding(for item: SidebarMainItem) -> Binding { 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 Picker(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) Picker(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 Picker(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) Picker(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 Picker(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) Picker(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: - Preview #Preview { SidebarSettingsView() .appEnvironment(.preview) }