jellyflood/Swiftfin tvOS/Components/OrderedSectionSelectorView ...

199 lines
6.3 KiB
Swift

//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import Factory
import SwiftUI
struct OrderedSectionSelectorView<Element: Displayable & Hashable>: View {
@Environment(\.editMode)
private var editMode
@State
private var focusedElement: Element?
@StateObject
private var selection: BindingBox<[Element]>
private var disabledSelection: [Element] {
sources.filter { !selection.value.contains($0) }
}
private var label: (Element) -> any View
private let sources: [Element]
private var systemImage: String
private func move(from source: IndexSet, to destination: Int) {
selection.value.move(fromOffsets: source, toOffset: destination)
editMode?.wrappedValue = .inactive
}
private func select(element: Element) {
if selection.value.contains(element) {
selection.value.removeAll(where: { $0 == element })
} else {
selection.value.append(element)
}
}
var body: some View {
NavigationStack {
SplitFormWindowView()
.descriptionView {
Image(systemName: systemImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: 400)
}
.contentView {
List {
EnabledSection(
elements: $selection.value,
label: label,
isEditing: editMode?.wrappedValue.isEditing ?? false,
select: select,
move: move,
header: {
Group {
HStack {
Text(L10n.enabled)
Spacer()
if editMode?.wrappedValue.isEditing ?? false {
Button(L10n.done) {
withAnimation {
editMode?.wrappedValue = .inactive
}
}
} else {
Button(L10n.edit) {
withAnimation {
editMode?.wrappedValue = .active
}
}
}
}
}
}
)
DisabledSection(
elements: disabledSelection,
label: label,
isEditing: editMode?.wrappedValue.isEditing ?? false,
select: select
)
}
.environment(\.editMode, editMode)
}
.animation(.linear(duration: 0.2), value: selection.value)
}
}
}
private struct EnabledSection<Element: Displayable & Hashable>: View {
@Binding
var elements: [Element]
let label: (Element) -> any View
let isEditing: Bool
let select: (Element) -> Void
let move: (IndexSet, Int) -> Void
let header: () -> any View
var body: some View {
Section {
if elements.isEmpty {
Text(L10n.none)
.foregroundStyle(.secondary)
}
ForEach(elements, id: \.self) { element in
Button {
if !isEditing {
select(element)
}
} label: {
HStack {
label(element)
.eraseToAnyView()
Spacer()
if !isEditing {
Image(systemName: "minus.circle.fill")
.foregroundColor(.red)
}
}
.foregroundColor(.primary)
}
}
.onMove(perform: move)
} header: {
header()
.eraseToAnyView()
}
}
}
private struct DisabledSection<Element: Displayable & Hashable>: View {
let elements: [Element]
let label: (Element) -> any View
let isEditing: Bool
let select: (Element) -> Void
var body: some View {
Section(L10n.disabled) {
if elements.isEmpty {
Text(L10n.none)
.foregroundStyle(.secondary)
}
ForEach(elements, id: \.self) { element in
Button {
if !isEditing {
select(element)
}
} label: {
HStack {
label(element)
.eraseToAnyView()
Spacer()
if !isEditing {
Image(systemName: "plus.circle.fill")
.foregroundColor(.green)
}
}
.foregroundColor(.primary)
}
}
}
}
}
extension OrderedSectionSelectorView {
init(selection: Binding<[Element]>, sources: [Element]) {
self._selection = StateObject(wrappedValue: BindingBox(source: selection))
self.sources = sources
self.label = { Text($0.displayTitle).foregroundColor(.primary).eraseToAnyView() }
self.systemImage = "filemenu.and.selection"
}
func label(@ViewBuilder _ content: @escaping (Element) -> any View) -> Self {
copy(modifying: \.label, with: content)
}
func systemImage(_ systemName: String) -> Self {
copy(modifying: \.systemImage, with: systemName)
}
}