mirror of
https://github.com/yattee/yattee.git
synced 2026-02-20 01:39:46 +00:00
Yattee v2 rewrite
This commit is contained in:
498
Yattee/Views/Settings/SidebarSettingsView.swift
Normal file
498
Yattee/Views/Settings/SidebarSettingsView.swift
Normal file
@@ -0,0 +1,498 @@
|
||||
//
|
||||
// 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 {
|
||||
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<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
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user