Skip to content

Commit 6ac2dfa

Browse files
committed
Mergable Version
1 parent 46f90bc commit 6ac2dfa

File tree

11 files changed

+369
-31
lines changed

11 files changed

+369
-31
lines changed

Shared/Coordinators/CustomizeSettingsCoordinator.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,27 @@ import Stinsen
1010
import SwiftUI
1111

1212
final class CustomizeSettingsCoordinator: NavigationCoordinatable {
13+
1314
let stack = NavigationStack(initial: \CustomizeSettingsCoordinator.start)
1415

1516
@Root
1617
var start = makeStart
1718

1819
@Route(.modal)
1920
var indicatorSettings = makeIndicatorSettings
21+
@Route(.modal)
22+
var listColumnSettings = makeListColumnSettings
2023

2124
func makeIndicatorSettings() -> NavigationViewCoordinator<BasicNavigationViewCoordinator> {
2225
NavigationViewCoordinator {
2326
IndicatorSettingsView()
2427
}
2528
}
2629

30+
func makeListColumnSettings(selection: Binding<Int>) -> some View {
31+
ListColumnsPickerView(selection: selection)
32+
}
33+
2734
@ViewBuilder
2835
func makeStart() -> some View {
2936
CustomizeViewsSettings()

Shared/Coordinators/SettingsCoordinator.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,6 @@ final class SettingsCoordinator: NavigationCoordinatable {
8282
@Route(.modal)
8383
var experimentalSettings = makeExperimentalSettings
8484
@Route(.modal)
85-
var indicatorSettings = makeIndicatorSettings
86-
@Route(.modal)
8785
var log = makeLog
8886
@Route(.modal)
8987
var serverDetail = makeServerDetail

Shared/Strings/Strings.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ internal enum L10n {
168168
internal static let collections = L10n.tr("Localizable", "collections", fallback: "Collections")
169169
/// Color
170170
internal static let color = L10n.tr("Localizable", "color", fallback: "Color")
171+
/// Columns
172+
internal static let columns = L10n.tr("Localizable", "columns", fallback: "Columns")
171173
/// Coming soon
172174
internal static let comingSoon = L10n.tr("Localizable", "comingSoon", fallback: "Coming soon")
173175
/// Compact

Swiftfin tvOS/Components/StepperView.swift

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ struct StepperView<Value: CustomStringConvertible & Strideable>: View {
1313
@Binding
1414
private var value: Value
1515

16+
@State
17+
private var updatedValue: Value
18+
@Environment(\.presentationMode)
19+
private var presentationMode
20+
1621
private var title: String
1722
private var description: String?
1823
private var range: ClosedRange<Value>
@@ -36,16 +41,18 @@ struct StepperView<Value: CustomStringConvertible & Strideable>: View {
3641
}
3742
.frame(maxHeight: .infinity)
3843

39-
formatter(value).text
44+
formatter(updatedValue).text
4045
.font(.title)
4146
.frame(height: 250)
4247

4348
VStack {
4449

4550
HStack {
4651
Button {
47-
guard value >= range.lowerBound else { return }
48-
value = value.advanced(by: -step)
52+
if updatedValue > range.lowerBound {
53+
updatedValue = max(updatedValue.advanced(by: -step), range.lowerBound)
54+
value = updatedValue
55+
}
4956
} label: {
5057
Image(systemName: "minus")
5158
.font(.title2.weight(.bold))
@@ -54,8 +61,10 @@ struct StepperView<Value: CustomStringConvertible & Strideable>: View {
5461
.buttonStyle(.card)
5562

5663
Button {
57-
guard value <= range.upperBound else { return }
58-
value = value.advanced(by: step)
64+
if updatedValue < range.upperBound {
65+
updatedValue = min(updatedValue.advanced(by: step), range.upperBound)
66+
value = updatedValue
67+
}
5968
} label: {
6069
Image(systemName: "plus")
6170
.font(.title2.weight(.bold))
@@ -64,10 +73,9 @@ struct StepperView<Value: CustomStringConvertible & Strideable>: View {
6473
.buttonStyle(.card)
6574
}
6675

67-
Button {
76+
Button(L10n.close) {
6877
onCloseSelected()
69-
} label: {
70-
Text("Close")
78+
presentationMode.wrappedValue.dismiss()
7179
}
7280

7381
Spacer()
@@ -86,15 +94,14 @@ extension StepperView {
8694
range: ClosedRange<Value>,
8795
step: Value.Stride
8896
) {
89-
self.init(
90-
value: value,
91-
title: title,
92-
description: description,
93-
range: range,
94-
step: step,
95-
formatter: { $0.description },
96-
onCloseSelected: {}
97-
)
97+
self._value = value
98+
self._updatedValue = State(initialValue: value.wrappedValue)
99+
self.title = title
100+
self.description = description
101+
self.range = range
102+
self.step = step
103+
self.formatter = { $0.description }
104+
self.onCloseSelected = {}
98105
}
99106

100107
func valueFormatter(_ formatter: @escaping (Value) -> String) -> Self {
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
//
2+
// Swiftfin is subject to the terms of the Mozilla Public
3+
// License, v2.0. If a copy of the MPL was not distributed with this
4+
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
5+
//
6+
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
7+
//
8+
9+
import Defaults
10+
import JellyfinAPI
11+
import SwiftUI
12+
13+
extension PagingLibraryView {
14+
15+
struct LibraryRow: View {
16+
17+
@State
18+
private var contentWidth: CGFloat = 0
19+
@State
20+
private var focusedItem: Element?
21+
22+
@FocusState
23+
private var isFocused: Bool
24+
25+
private let item: Element
26+
private var action: () -> Void
27+
private var contextMenu: () -> any View
28+
private let posterType: PosterDisplayType
29+
30+
private var onFocusChanged: ((Bool) -> Void)?
31+
32+
private func imageView(from element: Element) -> ImageView {
33+
switch posterType {
34+
case .landscape:
35+
ImageView(element.landscapeImageSources(maxWidth: 110))
36+
case .portrait:
37+
ImageView(element.portraitImageSources(maxWidth: 60))
38+
}
39+
}
40+
41+
@ViewBuilder
42+
private func itemAccessoryView(item: BaseItemDto) -> some View {
43+
DotHStack {
44+
if item.type == .episode, let seasonEpisodeLocator = item.seasonEpisodeLabel {
45+
Text(seasonEpisodeLocator)
46+
} else if let premiereYear = item.premiereDateYear {
47+
Text(premiereYear)
48+
}
49+
50+
if let runtime = item.runTimeLabel {
51+
Text(runtime)
52+
}
53+
54+
if let officialRating = item.officialRating {
55+
Text(officialRating)
56+
}
57+
}
58+
}
59+
60+
@ViewBuilder
61+
private func personAccessoryView(person: BaseItemPerson) -> some View {
62+
if let subtitle = person.subtitle {
63+
Text(subtitle)
64+
}
65+
}
66+
67+
@ViewBuilder
68+
private var accessoryView: some View {
69+
switch item {
70+
case let element as BaseItemDto:
71+
itemAccessoryView(item: element)
72+
case let element as BaseItemPerson:
73+
personAccessoryView(person: element)
74+
default:
75+
AssertionFailureView("Used an unexpected type within a `PagingLibaryView`?")
76+
}
77+
}
78+
79+
@ViewBuilder
80+
private var rowContent: some View {
81+
HStack {
82+
VStack(alignment: .leading, spacing: 5) {
83+
Text(item.displayTitle)
84+
.font(posterType == .landscape ? .subheadline : .callout)
85+
.fontWeight(.semibold)
86+
.foregroundColor(.primary)
87+
.lineLimit(2)
88+
.multilineTextAlignment(.leading)
89+
90+
accessoryView
91+
.font(.caption)
92+
.foregroundColor(Color(UIColor.lightGray))
93+
}
94+
Spacer()
95+
}
96+
}
97+
98+
@ViewBuilder
99+
private var rowLeading: some View {
100+
ZStack {
101+
Color.clear
102+
103+
imageView(from: item)
104+
.failure {
105+
SystemImageContentView(systemName: item.systemImage)
106+
}
107+
}
108+
.posterStyle(posterType)
109+
.frame(width: posterType == .landscape ? 110 : 60)
110+
.posterShadow()
111+
.padding(.vertical, 8)
112+
}
113+
114+
// MARK: body
115+
116+
var body: some View {
117+
ListRow(insets: .init(horizontal: EdgeInsets.edgePadding)) {
118+
rowLeading
119+
} content: {
120+
rowContent
121+
}
122+
.onSelect(perform: action)
123+
.contextMenu(menuItems: {
124+
contextMenu()
125+
.eraseToAnyView()
126+
})
127+
.posterShadow()
128+
.ifLet(onFocusChanged) { view, onFocusChanged in
129+
view
130+
.focused($isFocused)
131+
.onChange(of: isFocused) { _, newValue in
132+
onFocusChanged(newValue)
133+
}
134+
}
135+
}
136+
}
137+
}
138+
139+
extension PagingLibraryView.LibraryRow {
140+
141+
init(item: Element, posterType: PosterDisplayType) {
142+
self.init(
143+
item: item,
144+
action: {},
145+
contextMenu: { EmptyView() },
146+
posterType: posterType,
147+
onFocusChanged: nil
148+
)
149+
}
150+
}
151+
152+
extension PagingLibraryView.LibraryRow {
153+
154+
func onSelect(perform action: @escaping () -> Void) -> Self {
155+
copy(modifying: \.action, with: action)
156+
}
157+
158+
func contextMenu(@ViewBuilder perform content: @escaping () -> any View) -> Self {
159+
copy(modifying: \.contextMenu, with: content)
160+
}
161+
162+
func onFocusChanged(perform action: @escaping (Bool) -> Void) -> Self {
163+
copy(modifying: \.onFocusChanged, with: action)
164+
}
165+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//
2+
// Swiftfin is subject to the terms of the Mozilla Public
3+
// License, v2.0. If a copy of the MPL was not distributed with this
4+
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
5+
//
6+
// Copyright (c) 2024 Jellyfin & Jellyfin Contributors
7+
//
8+
9+
import SwiftUI
10+
11+
// TODO: come up with better name along with `ListRowButton`
12+
13+
// Meant to be used when making a custom list without `List` or `Form`
14+
struct ListRow<Leading: View, Content: View>: View {
15+
16+
@State
17+
private var contentSize: CGSize = .zero
18+
19+
private let leading: () -> Leading
20+
private let content: () -> Content
21+
private var action: () -> Void
22+
private var insets: EdgeInsets
23+
private var isSeparatorVisible: Bool
24+
25+
var body: some View {
26+
ZStack(alignment: .bottomTrailing) {
27+
28+
Button {
29+
action()
30+
} label: {
31+
HStack(alignment: .center, spacing: EdgeInsets.edgePadding) {
32+
33+
leading()
34+
35+
content()
36+
.frame(maxHeight: .infinity)
37+
.trackingSize($contentSize)
38+
}
39+
.padding(.top, insets.top)
40+
.padding(.bottom, insets.bottom)
41+
.padding(.leading, insets.leading)
42+
.padding(.trailing, insets.trailing)
43+
}
44+
.foregroundStyle(.primary, .secondary)
45+
.buttonStyle(.plain)
46+
47+
Color.secondarySystemFill
48+
.frame(width: contentSize.width, height: 1)
49+
.padding(.trailing, insets.trailing)
50+
.visible(isSeparatorVisible)
51+
}
52+
}
53+
}
54+
55+
extension ListRow {
56+
57+
init(
58+
insets: EdgeInsets = .zero,
59+
@ViewBuilder leading: @escaping () -> Leading,
60+
@ViewBuilder content: @escaping () -> Content
61+
) {
62+
self.init(
63+
leading: leading,
64+
content: content,
65+
action: {},
66+
insets: insets,
67+
isSeparatorVisible: true
68+
)
69+
}
70+
71+
func isSeparatorVisible(_ isVisible: Bool) -> Self {
72+
copy(modifying: \.isSeparatorVisible, with: isVisible)
73+
}
74+
75+
func onSelect(perform action: @escaping () -> Void) -> Self {
76+
copy(modifying: \.action, with: action)
77+
}
78+
}

0 commit comments

Comments
 (0)