Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 20 additions & 23 deletions Shared/Components/ListRowCheckbox.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,46 +9,43 @@
import Defaults
import SwiftUI

// TODO: remove isEditing observation

struct ListRowCheckbox: View {

@Default(.accentColor)
private var accentColor

// MARK: - Environment Variables

@Environment(\.isEditing)
private var isEditing
@Environment(\.isSelected)
private var isSelected

// MARK: - Sizing Variable

#if os(tvOS)
private let size: CGFloat = 36
#else
private let size: CGFloat = 24
#endif

// MARK: - Body

@ViewBuilder
var body: some View {
if isEditing, isSelected {
Image(systemName: "checkmark.circle.fill")
.resizable()
.fontWeight(.bold)
.aspectRatio(1, contentMode: .fit)
.frame(width: size, height: size)
.symbolRenderingMode(.palette)
.foregroundStyle(accentColor.overlayColor, accentColor)

} else if isEditing {
Image(systemName: "circle")
.resizable()
.fontWeight(.bold)
.aspectRatio(1, contentMode: .fit)
.frame(width: size, height: size)
.foregroundStyle(.secondary)
if isEditing {
if isSelected {
Image(systemName: "checkmark.circle.fill")
.resizable()
.fontWeight(.bold)
.aspectRatio(1, contentMode: .fit)
.frame(width: size, height: size)
.symbolRenderingMode(.palette)
.foregroundStyle(accentColor.overlayColor, accentColor)

} else {
Image(systemName: "circle")
.resizable()
.fontWeight(.bold)
.aspectRatio(1, contentMode: .fit)
.frame(width: size, height: size)
.foregroundStyle(.secondary)
}
}
}
}
108 changes: 46 additions & 62 deletions Shared/Components/SelectorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,126 +6,110 @@
// Copyright (c) 2026 Jellyfin & Jellyfin Contributors
//

import Defaults
import SwiftUI

enum SelectorType {
case single
case multi
}

struct SelectorView<Element: Displayable & Hashable, Label: View>: View {
struct SelectorView<Element: Hashable, Label: View>: View {

@Default(.accentColor)
private var accentColor
@StateObject
private var box: BindingBox<Set<Element>>

@State
private var selectedItems: Set<Element>

private let selectionBinding: Binding<Set<Element>>
private let sources: [Element]
private var label: (Element) -> Label
private let type: SelectorType

private init(
selection: Binding<Set<Element>>,
init(
selection: Binding<[Element]>,
sources: [Element],
type: SelectorType,
label: @escaping (Element) -> Label,
type: SelectorType
) {
self.selectionBinding = selection
self._selectedItems = State(initialValue: selection.wrappedValue)
self._box = StateObject(
wrappedValue: BindingBox(
source: selection.map(
getter: { Set($0) },
setter: { Array($0) }
)
)
)
self.sources = sources
self.label = label
self.type = type
}

private func handleSingleSelect(with element: Element) {
selectedItems = [element]
selectionBinding.wrappedValue = selectedItems
}

private func handleMultiSelect(with element: Element) {
if selectedItems.contains(element) {
selectedItems.remove(element)
} else {
selectedItems.insert(element)
}
selectionBinding.wrappedValue = selectedItems
init(
selection: Binding<Element>,
sources: [Element],
label: @escaping (Element) -> Label,
) {
self._box = StateObject(
wrappedValue: BindingBox(
source: selection.map(
getter: { Set([$0]) },
setter: { $0.first! }
)
)
)
self.sources = sources
self.label = label
self.type = .single
}

var body: some View {
List(sources, id: \.hashValue) { element in
Button {
switch type {
case .single:
handleSingleSelect(with: element)
box.value = [element]
case .multi:
handleMultiSelect(with: element)
box.value.toggle(value: element)
}
} label: {
HStack {
label(element)
.frame(maxWidth: .infinity, alignment: .leading)

if selectedItems.contains(element) {
Image(systemName: "checkmark.circle.fill")
.resizable()
.fontWeight(.bold)
.aspectRatio(1, contentMode: .fit)
.frame(width: 24, height: 24)
.symbolRenderingMode(.palette)
.foregroundStyle(accentColor.overlayColor, accentColor)
let isSelected = box.value.contains(element)

if isSelected {
ListRowCheckbox()
.isEditing(true)
.isSelected(isSelected)
}
}
}
}
.backport
.onChange(of: selectionBinding.wrappedValue) { _, newValue in
selectedItems = newValue
.foregroundStyle(.primary, .secondary)
}
}
}

extension SelectorView where Label == Text {
extension SelectorView where Element: Displayable, Label == Text {

init(selection: Binding<[Element]>, sources: [Element], type: SelectorType) {
let setBinding = Binding<Set<Element>>(
get: { Set(selection.wrappedValue) },
set: { newValue in
selection.wrappedValue = Array(newValue)
}
)

self.init(
selection: setBinding,
selection: selection,
sources: sources,
label: { Text($0.displayTitle).foregroundColor(.primary) },
type: type
type: type,
label: { Text($0.displayTitle) }
)
}

init(selection: Binding<Element>, sources: [Element]) {
let setBinding = Binding<Set<Element>>(
get: { Set([selection.wrappedValue]) },
set: { newValue in
if let first = newValue.first {
selection.wrappedValue = first
}
}
)

self.init(
selection: setBinding,
selection: selection,
sources: sources,
label: { Text($0.displayTitle).foregroundColor(.primary) },
type: .single
label: { Text($0.displayTitle) }
)
}
}

extension SelectorView {

@available(*, deprecated, message: "Use SelectorView(selection:sources:type:label:) instead")
func label(@ViewBuilder _ content: @escaping (Element) -> Label) -> Self {
copy(modifying: \.label, with: content)
}
Expand Down
4 changes: 0 additions & 4 deletions Shared/Objects/Displayable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,3 @@ extension Displayable where Self: CustomStringConvertible {
displayTitle
}
}

struct DisplayableBox<Wrapped>: Displayable, Hashable {
let displayTitle: String
}
27 changes: 27 additions & 0 deletions Shared/Views/FontPickerView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// 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) 2026 Jellyfin & Jellyfin Contributors
//

import SwiftUI

struct FontPickerView: View {

let selection: Binding<String>

var body: some View {
Form(systemImage: "textformat.characters") {
SelectorView(
selection: selection,
sources: UIFont.familyNames
) { fontFamily in
Text(fontFamily)
.font(.custom(fontFamily, size: UIDevice.isTV ? 30 : 18))
}
}
.navigationTitle(L10n.subtitleFont.localizedCapitalized)
}
}
53 changes: 0 additions & 53 deletions Swiftfin tvOS/Views/FontPickerView.swift

This file was deleted.

36 changes: 0 additions & 36 deletions Swiftfin/Views/FontPickerView.swift

This file was deleted.