diff --git a/Yattee/Views/Settings/SidebarSettingsView.swift b/Yattee/Views/Settings/SidebarSettingsView.swift index 5b099be5..f7e9d6ab 100644 --- a/Yattee/Views/Settings/SidebarSettingsView.swift +++ b/Yattee/Views/Settings/SidebarSettingsView.swift @@ -133,7 +133,7 @@ struct SidebarSettingsView: View { channelsSection playlistsSection } - #if os(iOS) || os(tvOS) + #if os(iOS) .environment(\.editMode, .constant(.active)) #endif .navigationTitle(String(localized: "settings.sidebar.title")) @@ -169,6 +169,21 @@ struct SidebarSettingsView: View { 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, @@ -206,6 +221,7 @@ struct SidebarSettingsView: View { // Save immediately saveMainNavigationSettings() } + #endif } header: { Text(String(localized: "settings.sidebar.mainNavigation.header")) } footer: { @@ -213,6 +229,24 @@ struct SidebarSettingsView: View { } } + #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 { Binding( get: { mainItemVisibility[item] ?? true }, @@ -490,6 +524,81 @@ private struct SidebarMainItemRow: View { } } +// 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 {