mirror of
https://github.com/yattee/yattee.git
synced 2026-05-12 10:25:02 +00:00
Make tvOS sidebar main navigation toggles focusable
The tvOS Main Navigation list relied on .onMove with native Toggle rows inside editMode, which leaves only the drag handle focusable on tvOS. Replace it with a TVSidebarMainItemRow modeled on the home customization screen: explicit up/down chevrons on the left and a tap-to-toggle checkmark button as the row body. Required items render disabled with a dimmed checkmark.
This commit is contained in:
@@ -133,7 +133,7 @@ struct SidebarSettingsView: View {
|
|||||||
channelsSection
|
channelsSection
|
||||||
playlistsSection
|
playlistsSection
|
||||||
}
|
}
|
||||||
#if os(iOS) || os(tvOS)
|
#if os(iOS)
|
||||||
.environment(\.editMode, .constant(.active))
|
.environment(\.editMode, .constant(.active))
|
||||||
#endif
|
#endif
|
||||||
.navigationTitle(String(localized: "settings.sidebar.title"))
|
.navigationTitle(String(localized: "settings.sidebar.title"))
|
||||||
@@ -169,6 +169,21 @@ struct SidebarSettingsView: View {
|
|||||||
|
|
||||||
private var mainNavigationSection: some View {
|
private var mainNavigationSection: some View {
|
||||||
Section {
|
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
|
ForEach(mainItemOrder.filter { $0.isAvailableOnCurrentPlatform }) { item in
|
||||||
SidebarMainItemRow(
|
SidebarMainItemRow(
|
||||||
icon: item.icon,
|
icon: item.icon,
|
||||||
@@ -206,6 +221,7 @@ struct SidebarSettingsView: View {
|
|||||||
// Save immediately
|
// Save immediately
|
||||||
saveMainNavigationSettings()
|
saveMainNavigationSettings()
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
} header: {
|
} header: {
|
||||||
Text(String(localized: "settings.sidebar.mainNavigation.header"))
|
Text(String(localized: "settings.sidebar.mainNavigation.header"))
|
||||||
} footer: {
|
} 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<Bool> {
|
private func mainItemBinding(for item: SidebarMainItem) -> Binding<Bool> {
|
||||||
Binding(
|
Binding(
|
||||||
get: { mainItemVisibility[item] ?? true },
|
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
|
// MARK: - Preview
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
|
|||||||
Reference in New Issue
Block a user