// // 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 Defaults import SwiftUI // TODO: Label generic not really necessary if just restricting to `Text` // - go back to `any View` implementation instead enum SelectorType { case single case multi } struct SelectorView: View { @Default(.accentColor) private var accentColor @StateObject private var selection: BindingBox> private init( selection: Binding>, sources: [Element], label: @escaping (Element) -> Label, type: SelectorType ) { self._selection = StateObject(wrappedValue: BindingBox(source: selection)) self.sources = sources self.label = label self.type = type } private let sources: [Element] private var label: (Element) -> Label private let type: SelectorType var body: some View { List(sources, id: \.hashValue) { element in Button { switch type { case .single: handleSingleSelect(with: element) case .multi: handleMultiSelect(with: element) } } label: { HStack { label(element) Spacer() if selection.value.contains(element) { Image(systemName: "checkmark.circle.fill") .resizable() .backport .fontWeight(.bold) .aspectRatio(1, contentMode: .fit) .frame(width: 24, height: 24) .symbolRenderingMode(.palette) .foregroundStyle(accentColor.overlayColor, accentColor) } } } } } private func handleSingleSelect(with element: Element) { selection.value = [element] } private func handleMultiSelect(with element: Element) { if selection.value.contains(element) { selection.value.remove(element) } else { selection.value.insert(element) } } } extension SelectorView where Label == Text { init(selection: Binding<[Element]>, sources: [Element], type: SelectorType) { let selectionBinding = Binding { Set(selection.wrappedValue) } set: { newValue in selection.wrappedValue = sources.intersection(newValue) } self.init( selection: selectionBinding, sources: sources, label: { Text($0.displayTitle).foregroundColor(.primary) }, type: type ) } init(selection: Binding, sources: [Element]) { let selectionBinding = Binding { Set([selection.wrappedValue]) } set: { newValue in selection.wrappedValue = newValue.first! } self.init( selection: selectionBinding, sources: sources, label: { Text($0.displayTitle).foregroundColor(.primary) }, type: .single ) } } extension SelectorView { func label(@ViewBuilder _ content: @escaping (Element) -> Label) -> Self { copy(modifying: \.label, with: content) } }