This commit is contained in:
Ethan Pippin 2025-04-07 09:08:54 -04:00 committed by GitHub
parent 84fd2e82a5
commit cfc0105dc7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 563 additions and 517 deletions

View File

@ -1,96 +0,0 @@
//
// 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 SwiftUI
// TODO: find better name
struct ChevronAlertButton<Content>: View where Content: View {
@State
private var isSelected = false
private let content: () -> Content
private let description: String?
private let onCancel: (() -> Void)?
private let onSave: (() -> Void)?
private let subtitle: Text?
private let title: String
// MARK: - Body
var body: some View {
ChevronButton(title, subtitle: subtitle)
.onSelect {
isSelected = true
}
.alert(title, isPresented: $isSelected) {
content()
if let onSave {
Button(L10n.save) {
onSave()
isSelected = false
}
}
if let onCancel {
Button(L10n.cancel, role: .cancel) {
onCancel()
isSelected = false
}
}
} message: {
if let description {
Text(description)
}
}
}
}
extension ChevronAlertButton {
init(
_ title: String,
subtitle: String?,
description: String? = nil,
@ViewBuilder content: @escaping () -> Content,
onSave: (() -> Void)? = nil,
onCancel: (() -> Void)? = nil
) {
self.init(
content: content,
description: description,
onCancel: onCancel,
onSave: onSave,
subtitle: subtitle != nil ? Text(subtitle!) : nil,
title: title
)
}
// MARK: - Initializer: Text Inputs with Save/Cancel Actions
init(
_ title: String,
subtitle: Text?,
description: String? = nil,
@ViewBuilder content: @escaping () -> Content,
onSave: (() -> Void)? = nil,
onCancel: (() -> Void)? = nil
) {
self.init(
content: content,
description: description,
onCancel: onCancel,
onSave: onSave,
subtitle: subtitle,
title: title
)
}
}

View File

@ -8,16 +8,17 @@
import SwiftUI
struct ChevronButton<Icon: View>: View {
struct ChevronButton<Icon: View, Subtitle: View>: View {
private let icon: Icon
private let isExternal: Bool
private let title: Text
private let subtitle: Text?
private var onSelect: () -> Void
private let subtitle: Subtitle
var body: some View {
Button(action: onSelect) {
private let innerContent: (AnyView) -> any View
@ViewBuilder
private var label: some View {
HStack {
icon
@ -27,135 +28,354 @@ struct ChevronButton<Icon: View>: View {
Spacer()
if let subtitle {
subtitle
.foregroundStyle(.secondary)
}
Image(systemName: isExternal ? "arrow.up.forward" : "chevron.right")
.font(.body.weight(.regular))
.foregroundStyle(.secondary)
}
}
.foregroundStyle(.primary, .secondary)
}
}
extension ChevronButton where Icon == EmptyView {
init(
_ title: String,
subtitle: String? = nil,
external: Bool = false
) {
self.init(
icon: EmptyView(),
isExternal: external,
title: Text(title),
subtitle: {
if let subtitle {
Text(subtitle)
} else {
nil
}
}(),
onSelect: {}
)
}
init(
_ title: String,
subtitle: Text?,
external: Bool = false
) {
self.init(
icon: EmptyView(),
isExternal: external,
title: Text(title),
subtitle: subtitle,
onSelect: {}
)
}
}
extension ChevronButton where Icon == Image {
init(
_ title: String,
subtitle: String? = nil,
systemName: String,
external: Bool = false
) {
self.init(
icon: Image(systemName: systemName),
isExternal: external,
title: Text(title),
subtitle: {
if let subtitle {
Text(subtitle)
} else {
nil
}
}(),
onSelect: {}
)
}
init(
_ title: String,
subtitle: Text?,
systemName: String,
external: Bool = false
) {
self.init(
icon: Image(systemName: systemName),
isExternal: external,
title: Text(title),
subtitle: subtitle,
onSelect: {}
)
}
init(
_ title: String,
subtitle: String? = nil,
image: Image,
external: Bool = false
) {
self.init(
icon: image,
isExternal: external,
title: Text(title),
subtitle: {
if let subtitle {
Text(subtitle)
} else {
nil
}
}(),
onSelect: {}
)
}
init(
_ title: String,
subtitle: Text?,
image: Image,
external: Bool = false
) {
self.init(
icon: image,
isExternal: external,
title: Text(title),
subtitle: subtitle,
onSelect: {}
)
var body: some View {
innerContent(label.eraseToAnyView())
.eraseToAnyView()
}
}
extension ChevronButton {
func onSelect(perform action: @escaping () -> Void) -> Self {
copy(modifying: \.onSelect, with: action)
private struct AlertContentView<Content: View, Label: View>: View {
@State
private var isPresented: Bool = false
let alertTitle: String
let content: () -> Content
let description: String?
let label: Label
let onCancel: (() -> Void)?
let onSave: (() -> Void)?
var body: some View {
Button {
isPresented = true
} label: {
label
}
.foregroundStyle(.primary, .secondary)
.alert(alertTitle, isPresented: $isPresented) {
content()
if let onSave {
Button(L10n.save) {
onSave()
isPresented = false
}
}
if let onCancel {
Button(L10n.cancel, role: .cancel) {
onCancel()
isPresented = false
}
}
} message: {
if let description {
Text(description)
}
}
}
}
private struct ButtonContentView<Label: View>: View {
let label: Label
let action: () -> Void
var body: some View {
Button(action: action) {
label
}
.foregroundStyle(.primary, .secondary)
}
}
}
extension ChevronButton {
init(
_ title: String,
external: Bool = false,
action: @escaping () -> Void,
@ViewBuilder icon: @escaping () -> Icon,
@ViewBuilder subtitle: @escaping () -> Subtitle
) {
self.icon = icon()
self.isExternal = external
self.title = Text(title)
self.subtitle = subtitle()
self.innerContent = { label in
ButtonContentView(
label: label,
action: action
)
}
}
init(
_ title: Text,
external: Bool = false,
action: @escaping () -> Void,
@ViewBuilder icon: @escaping () -> Icon,
@ViewBuilder subtitle: @escaping () -> Subtitle
) {
self.icon = icon()
self.isExternal = external
self.title = title
self.subtitle = subtitle()
self.innerContent = { label in
ButtonContentView(
label: label,
action: action
)
}
}
}
extension ChevronButton where Icon == EmptyView, Subtitle == Text {
init(
_ title: String,
subtitle: String,
external: Bool = false,
action: @escaping () -> Void
) {
self.icon = EmptyView()
self.isExternal = external
self.title = Text(title)
self.subtitle = Text(subtitle)
self.innerContent = { label in
ButtonContentView(
label: label,
action: action
)
}
}
init(
_ title: String,
subtitle: Text,
external: Bool = false,
action: @escaping () -> Void
) {
self.icon = EmptyView()
self.isExternal = external
self.title = Text(title)
self.subtitle = subtitle
self.innerContent = { label in
ButtonContentView(
label: label,
action: action
)
}
}
}
extension ChevronButton where Icon == EmptyView, Subtitle == EmptyView {
init(
_ title: String,
external: Bool = false,
action: @escaping () -> Void
) {
self.icon = EmptyView()
self.isExternal = external
self.title = Text(title)
self.subtitle = EmptyView()
self.innerContent = { label in
ButtonContentView(
label: label,
action: action
)
}
}
}
extension ChevronButton where Icon == Image, Subtitle == Text {
// systemName
init(
_ title: String,
subtitle: String,
systemName: String,
external: Bool = false,
action: @escaping () -> Void
) {
self.icon = Image(systemName: systemName)
self.isExternal = external
self.title = Text(title)
self.subtitle = Text(subtitle)
self.innerContent = { label in
ButtonContentView(
label: label,
action: action
)
}
}
init(
_ title: String,
subtitle: Text,
systemName: String,
external: Bool = false,
action: @escaping () -> Void
) {
self.icon = Image(systemName: systemName)
self.isExternal = external
self.title = Text(title)
self.subtitle = subtitle
self.innerContent = { label in
ButtonContentView(
label: label,
action: action
)
}
}
// ImageResource
init(
_ title: String,
subtitle: String,
image: ImageResource,
external: Bool = false,
action: @escaping () -> Void
) {
self.icon = Image(image)
self.isExternal = external
self.title = Text(title)
self.subtitle = Text(subtitle)
self.innerContent = { label in
ButtonContentView(
label: label,
action: action
)
}
}
init(
_ title: String,
subtitle: Text,
image: ImageResource,
external: Bool = false,
action: @escaping () -> Void
) {
self.icon = Image(image)
self.isExternal = external
self.title = Text(title)
self.subtitle = subtitle
self.innerContent = { label in
ButtonContentView(
label: label,
action: action
)
}
}
}
extension ChevronButton where Icon == Image, Subtitle == EmptyView {
// systemName
init(
_ title: String,
systemName: String,
external: Bool = false,
action: @escaping () -> Void
) {
self.icon = Image(systemName: systemName)
self.isExternal = external
self.title = Text(title)
self.subtitle = EmptyView()
self.innerContent = { label in
ButtonContentView(
label: label,
action: action
)
}
}
// ImageResource
init(
_ title: String,
image: ImageResource,
external: Bool = false,
action: @escaping () -> Void
) {
self.icon = Image(image)
self.isExternal = external
self.title = Text(title)
self.subtitle = EmptyView()
self.innerContent = { label in
ButtonContentView(
label: label,
action: action
)
}
}
}
extension ChevronButton where Icon == EmptyView, Subtitle == Text {
init<Content: View>(
_ title: String,
subtitle: String? = nil,
description: String? = nil,
@ViewBuilder content: @escaping () -> Content,
onSave: (() -> Void)? = nil,
onCancel: (() -> Void)? = nil
) {
self.icon = EmptyView()
self.isExternal = false
self.title = Text(title)
self.subtitle = Text(subtitle ?? "")
self.innerContent = { label in
AlertContentView(
alertTitle: title,
content: content,
description: description,
label: label,
onCancel: onCancel,
onSave: onSave
)
}
}
init<Content: View>(
_ title: String,
subtitle: Text? = nil,
description: String? = nil,
@ViewBuilder content: @escaping () -> Content,
onSave: (() -> Void)? = nil,
onCancel: (() -> Void)? = nil
) {
self.icon = EmptyView()
self.isExternal = false
self.title = Text(title)
self.subtitle = subtitle ?? Text("")
self.innerContent = { label in
AlertContentView(
alertTitle: title,
content: content,
description: description,
label: label,
onCancel: onCancel,
onSave: onSave
)
}
}
}

View File

@ -8,7 +8,7 @@
import Foundation
extension URL: Identifiable {
extension URL: @retroactive Identifiable {
public var id: String {
absoluteString

View File

@ -87,8 +87,7 @@ struct AppSettingsView: View {
SignOutIntervalSection()
ChevronButton(L10n.logs)
.onSelect {
ChevronButton(L10n.logs) {
router.route(to: \.log)
}
}

View File

@ -40,8 +40,7 @@ extension AppSettingsView {
ChevronButton(
L10n.duration,
subtitle: Text(backgroundSignOutInterval, format: .hourMinute)
)
.onSelect {
) {
router.route(to: \.hourPicker)
}
}

View File

@ -30,7 +30,7 @@ extension CustomizeViewsSettings {
Toggle(L10n.nextUpRewatch, isOn: $resumeNextUp)
ChevronAlertButton(
ChevronButton(
L10n.nextUpDays,
subtitle: {
if maxNextUp > 0 {

View File

@ -35,8 +35,7 @@ extension CustomizeViewsSettings {
var body: some View {
Section(L10n.items) {
ChevronButton(L10n.mediaAttributes)
.onSelect {
ChevronButton(L10n.mediaAttributes) {
router.route(to: \.itemViewAttributes, $itemViewAttributes)
}

View File

@ -57,8 +57,7 @@ extension CustomizeViewsSettings {
ChevronButton(
L10n.columns,
subtitle: listColumnCount.description
)
.onSelect {
) {
router.route(to: \.listColumnSettings, $listColumnCount)
}
}

View File

@ -53,8 +53,7 @@ struct CustomizeViewsSettings: View {
Section(L10n.posters) {
ChevronButton(L10n.indicators)
.onSelect {
ChevronButton(L10n.indicators) {
router.route(to: \.indicatorSettings)
}

View File

@ -61,8 +61,7 @@ struct PlaybackQualitySettingsView: View {
.focused($focusedItem, equals: .compatibility)
if compatibilityMode == .custom {
ChevronButton(L10n.profiles)
.onSelect {
ChevronButton(L10n.profiles) {
router.route(to: \.customDeviceProfileSettings)
}
}

View File

@ -40,8 +40,7 @@ struct SettingsView: View {
ChevronButton(
L10n.server,
subtitle: viewModel.userSession.server.name
)
.onSelect {
) {
router.route(to: \.serverDetail, viewModel.userSession.server)
}
}
@ -58,21 +57,18 @@ struct SettingsView: View {
ListRowMenu(L10n.videoPlayerType, selection: $videoPlayerType)
ChevronButton(L10n.videoPlayer)
.onSelect {
ChevronButton(L10n.videoPlayer) {
router.route(to: \.videoPlayerSettings)
}
ChevronButton(L10n.playbackQuality)
.onSelect {
ChevronButton(L10n.playbackQuality) {
router.route(to: \.playbackQualitySettings)
}
}
Section(L10n.accessibility) {
ChevronButton(L10n.customize)
.onSelect {
ChevronButton(L10n.customize) {
router.route(to: \.customizeViewsSettings)
}
//
@ -84,8 +80,7 @@ struct SettingsView: View {
Section {
ChevronButton(L10n.logs)
.onSelect {
ChevronButton(L10n.logs) {
router.route(to: \.log)
}
}

View File

@ -145,7 +145,7 @@ struct UserLocalSecurityView: View {
if signInPolicy == .requirePin {
Section {
ChevronAlertButton(
ChevronButton(
L10n.hint,
subtitle: pinHint,
description: L10n.setPinHintDescription

View File

@ -53,8 +53,7 @@ struct UserProfileSettingsView: View {
// }
Section {
ChevronButton(L10n.security)
.onSelect {
ChevronButton(L10n.security) {
router.route(to: \.localSecurity)
}
}

View File

@ -47,8 +47,7 @@ struct VideoPlayerSettingsView: View {
ChevronButton(
L10n.offset,
subtitle: resumeOffset.secondLabel
)
.onSelect {
) {
isPresentingResumeOffsetStepper = true
}
} header: {
@ -59,8 +58,7 @@ struct VideoPlayerSettingsView: View {
Section {
ChevronButton(L10n.subtitleFont, subtitle: subtitleFontName)
.onSelect {
ChevronButton(L10n.subtitleFont, subtitle: subtitleFontName) {
router.route(to: \.fontPicker, $subtitleFontName)
}
} header: {

View File

@ -93,7 +93,6 @@
4E49DEE32CE55FB900352DCD /* SyncPlayUserAccessType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DEE22CE55FB500352DCD /* SyncPlayUserAccessType.swift */; };
4E49DEE42CE55FB900352DCD /* SyncPlayUserAccessType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DEE22CE55FB500352DCD /* SyncPlayUserAccessType.swift */; };
4E49DEE62CE5616800352DCD /* UserProfileImagePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E49DEE52CE5616800352DCD /* UserProfileImagePickerView.swift */; };
4E4A53222CBE0A1C003BD24D /* ChevronAlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB7B33A2CBDE63F004A342E /* ChevronAlertButton.swift */; };
4E4DAC372D11EE5E00E13FF9 /* SplitLoginWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4DAC362D11EE4F00E13FF9 /* SplitLoginWindowView.swift */; };
4E4DAC3D2D11F94400E13FF9 /* LocalServerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4DAC3C2D11F94000E13FF9 /* LocalServerButton.swift */; };
4E4E9C672CFEBF2A00A6946F /* StudioEditorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E4E9C662CFEBF2500A6946F /* StudioEditorViewModel.swift */; };
@ -203,15 +202,12 @@
4EB3F0372D8CD33300EBEDAA /* ActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB3F0362D8CD33100EBEDAA /* ActionButton.swift */; };
4EB3F0392D8CD5CF00EBEDAA /* TrailerMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB3F0382D8CD5CC00EBEDAA /* TrailerMenu.swift */; };
4EB3F03B2D8CD6A900EBEDAA /* VersionMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB3F03A2D8CD6A700EBEDAA /* VersionMenu.swift */; };
4EB4ECE32CBEFC4D002FF2FC /* SessionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB4ECE22CBEFC49002FF2FC /* SessionInfo.swift */; };
4EB4ECE42CBEFC4D002FF2FC /* SessionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB4ECE22CBEFC49002FF2FC /* SessionInfo.swift */; };
4EB538B52CE3C77200EB72D5 /* ServerUserPermissionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB538B42CE3C76D00EB72D5 /* ServerUserPermissionsView.swift */; };
4EB538BD2CE3CCD100EB72D5 /* MediaPlaybackSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB538BC2CE3CCCF00EB72D5 /* MediaPlaybackSection.swift */; };
4EB538C12CE3CF0F00EB72D5 /* ManagementSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB538C02CE3CF0E00EB72D5 /* ManagementSection.swift */; };
4EB538C32CE3E21800EB72D5 /* SyncPlaySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB538C22CE3E21500EB72D5 /* SyncPlaySection.swift */; };
4EB538C52CE3E25700EB72D5 /* ExternalAccessSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB538C42CE3E25500EB72D5 /* ExternalAccessSection.swift */; };
4EB538C82CE3E8A600EB72D5 /* RemoteControlSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB538C72CE3E8A100EB72D5 /* RemoteControlSection.swift */; };
4EB7B33B2CBDE645004A342E /* ChevronAlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB7B33A2CBDE63F004A342E /* ChevronAlertButton.swift */; };
4EB7C8D52CCED6E7000CC011 /* AddServerUserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB7C8D42CCED6E1000CC011 /* AddServerUserView.swift */; };
4EBE06462C7E9509004A6C03 /* PlaybackCompatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBE06452C7E9509004A6C03 /* PlaybackCompatibility.swift */; };
4EBE06472C7E9509004A6C03 /* PlaybackCompatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBE06452C7E9509004A6C03 /* PlaybackCompatibility.swift */; };
@ -1440,14 +1436,12 @@
4EB3F0362D8CD33100EBEDAA /* ActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButton.swift; sourceTree = "<group>"; };
4EB3F0382D8CD5CC00EBEDAA /* TrailerMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrailerMenu.swift; sourceTree = "<group>"; };
4EB3F03A2D8CD6A700EBEDAA /* VersionMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionMenu.swift; sourceTree = "<group>"; };
4EB4ECE22CBEFC49002FF2FC /* SessionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionInfo.swift; sourceTree = "<group>"; };
4EB538B42CE3C76D00EB72D5 /* ServerUserPermissionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerUserPermissionsView.swift; sourceTree = "<group>"; };
4EB538BC2CE3CCCF00EB72D5 /* MediaPlaybackSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlaybackSection.swift; sourceTree = "<group>"; };
4EB538C02CE3CF0E00EB72D5 /* ManagementSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagementSection.swift; sourceTree = "<group>"; };
4EB538C22CE3E21500EB72D5 /* SyncPlaySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncPlaySection.swift; sourceTree = "<group>"; };
4EB538C42CE3E25500EB72D5 /* ExternalAccessSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalAccessSection.swift; sourceTree = "<group>"; };
4EB538C72CE3E8A100EB72D5 /* RemoteControlSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteControlSection.swift; sourceTree = "<group>"; };
4EB7B33A2CBDE63F004A342E /* ChevronAlertButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChevronAlertButton.swift; sourceTree = "<group>"; };
4EB7C8D42CCED6E1000CC011 /* AddServerUserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddServerUserView.swift; sourceTree = "<group>"; };
4EBE06452C7E9509004A6C03 /* PlaybackCompatibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackCompatibility.swift; sourceTree = "<group>"; };
4EBE064C2C7EB6D3004A6C03 /* VideoPlayerType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerType.swift; sourceTree = "<group>"; };
@ -5136,7 +5130,6 @@
E18E0203288749200022598C /* BlurView.swift */,
E145EB212BDCCA43003BF6F3 /* BulletedList.swift */,
E11982B92DA04F9B0008FC3F /* CenteredLazyVGrid.swift */,
4EB7B33A2CBDE63F004A342E /* ChevronAlertButton.swift */,
E1A1528728FD229500600579 /* ChevronButton.swift */,
E11982D62DA0E8240008FC3F /* ConditionalMenu.swift */,
E1153DCB2BBB633B00424D36 /* FastSVGView.swift */,
@ -6195,7 +6188,6 @@
E1763A722BF3F67C004DF6AB /* SwiftfinStore+Mappings.swift in Sources */,
E1937A3C288E54AD00CB80AA /* BaseItemDto+Images.swift in Sources */,
E18A17F0298C68B700C22F62 /* Overlay.swift in Sources */,
4E4A53222CBE0A1C003BD24D /* ChevronAlertButton.swift in Sources */,
4E7315752D1485C900EA2A95 /* UserProfileImage.swift in Sources */,
E1A42E4A28CA6CCD00A14DCB /* CinematicItemSelector.swift in Sources */,
4E2AC4CF2C6C4A0600DD600D /* PlaybackQualitySettingsCoordinator.swift in Sources */,
@ -6918,7 +6910,6 @@
BD3957792C113EC40078CEF8 /* SubtitleSection.swift in Sources */,
091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */,
E1721FAE28FB801C00762992 /* SmallPlaybackButtons.swift in Sources */,
4EB7B33B2CBDE645004A342E /* ChevronAlertButton.swift in Sources */,
E1E750682A33E9B400B2C1EE /* OverviewCard.swift in Sources */,
E1CCF13128AC07EC006CAC9E /* PosterHStack.swift in Sources */,
4E2AC4C22C6C491200DD600D /* AudoCodec.swift in Sources */,

View File

@ -40,10 +40,9 @@ struct AboutAppView: View {
ChevronButton(
L10n.sourceCode,
image: Image(.logoGithub),
image: .logoGithub,
external: true
)
.onSelect {
) {
UIApplication.shared.open(.swiftfinGithub)
}
@ -51,8 +50,7 @@ struct AboutAppView: View {
L10n.bugsAndFeatures,
systemName: "plus.circle.fill",
external: true
)
.onSelect {
) {
UIApplication.shared.open(.swiftfinGithubIssues)
}
.symbolRenderingMode(.monochrome)
@ -61,8 +59,7 @@ struct AboutAppView: View {
L10n.settings,
systemName: "gearshape.fill",
external: true
)
.onSelect {
) {
guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
UIApplication.shared.open(url)
}

View File

@ -23,36 +23,30 @@ struct AdminDashboardView: View {
description: L10n.dashboardDescription
)
ChevronButton(L10n.sessions)
.onSelect {
ChevronButton(L10n.sessions) {
router.route(to: \.activeSessions)
}
Section(L10n.activity) {
ChevronButton(L10n.devices)
.onSelect {
ChevronButton(L10n.devices) {
router.route(to: \.devices)
}
ChevronButton(L10n.users)
.onSelect {
ChevronButton(L10n.users) {
router.route(to: \.users)
}
}
Section(L10n.advanced) {
ChevronButton(L10n.apiKeys)
.onSelect {
ChevronButton(L10n.apiKeys) {
router.route(to: \.apiKeys)
}
ChevronButton(L10n.logs)
.onSelect {
ChevronButton(L10n.logs) {
router.route(to: \.serverLogs)
}
ChevronButton(L10n.tasks)
.onSelect {
ChevronButton(L10n.tasks) {
router.route(to: \.tasks)
}
}

View File

@ -29,7 +29,7 @@ extension AddTaskTriggerView {
// MARK: - Body
var body: some View {
ChevronAlertButton(
ChevronButton(
L10n.every,
subtitle: ServerTicks(
taskTriggerInfo.intervalTicks

View File

@ -30,7 +30,7 @@ extension AddTaskTriggerView {
var body: some View {
Section {
ChevronAlertButton(
ChevronButton(
L10n.timeLimit,
subtitle: subtitleString,
description: L10n.taskTriggerTimeLimit

View File

@ -65,7 +65,7 @@ struct ServerUserDetailsView: View {
}
Section {
ChevronAlertButton(
ChevronButton(
L10n.username,
subtitle: viewModel.user.name
) {
@ -82,43 +82,35 @@ struct ServerUserDetailsView: View {
}
}
if let userId = viewModel.user.id {
ChevronButton(L10n.password)
.onSelect {
ChevronButton(L10n.password) {
router.route(to: \.resetUserPassword, userId)
}
}
ChevronButton(L10n.permissions)
.onSelect {
ChevronButton(L10n.permissions) {
router.route(to: \.userPermissions, viewModel)
}
}
Section(L10n.access) {
ChevronButton(L10n.devices)
.onSelect {
ChevronButton(L10n.devices) {
router.route(to: \.userDeviceAccess, viewModel)
}
ChevronButton(L10n.liveTV)
.onSelect {
ChevronButton(L10n.liveTV) {
router.route(to: \.userLiveTVAccess, viewModel)
}
ChevronButton(L10n.media)
.onSelect {
ChevronButton(L10n.media) {
router.route(to: \.userMediaAccess, viewModel)
}
}
Section(L10n.parentalControls) {
ChevronButton(L10n.ratings)
.onSelect {
ChevronButton(L10n.ratings) {
router.route(to: \.userParentalRatings, viewModel)
}
ChevronButton(L10n.accessSchedules)
.onSelect {
ChevronButton(L10n.accessSchedules) {
router.route(to: \.userEditAccessSchedules, viewModel)
}
ChevronButton(L10n.accessTags)
.onSelect {
ChevronButton(L10n.accessTags) {
router.route(to: \.userEditAccessTags, viewModel)
}
}

View File

@ -35,7 +35,7 @@ extension ServerUserPermissionsView {
)
if policy.remoteClientBitrateLimit != MaxBitratePolicy.unlimited.rawValue {
ChevronAlertButton(
ChevronButton(
L10n.customBitrate,
subtitle: Text(policy.remoteClientBitrateLimit ?? 0, format: .bitRate),
description: L10n.enterCustomBitrate

View File

@ -74,7 +74,7 @@ extension ServerUserPermissionsView {
@ViewBuilder
private func MaxFailedLoginsButton() -> some View {
ChevronAlertButton(
ChevronButton(
L10n.customFailedLogins,
subtitle: Text(policy.loginAttemptsBeforeLockout ?? 1, format: .number),
description: L10n.enterCustomFailedLogins
@ -127,7 +127,7 @@ extension ServerUserPermissionsView {
@ViewBuilder
private func MaxSessionsButton() -> some View {
ChevronAlertButton(
ChevronButton(
L10n.customSessions,
subtitle: Text(policy.maxActiveSessions ?? 1, format: .number),
description: L10n.enterCustomMaxSessions

View File

@ -37,15 +37,13 @@ struct AppSettingsView: View {
var body: some View {
Form {
ChevronButton(L10n.about)
.onSelect {
ChevronButton(L10n.about) {
router.route(to: \.about, viewModel)
}
Section(L10n.accessibility) {
ChevronButton(L10n.appIcon)
.onSelect {
ChevronButton(L10n.appIcon) {
router.route(to: \.appIconSelector, viewModel)
}
@ -85,8 +83,7 @@ struct AppSettingsView: View {
SignOutIntervalSection()
ChevronButton(L10n.logs)
.onSelect {
ChevronButton(L10n.logs) {
router.route(to: \.log)
}
}

View File

@ -99,36 +99,29 @@ struct ItemEditorView: View {
private var editView: some View {
Section(L10n.edit) {
if [.boxSet, .movie, .person, .series].contains(viewModel.item.type) {
ChevronButton(L10n.identify)
.onSelect {
ChevronButton(L10n.identify) {
router.route(to: \.identifyItem, viewModel.item)
}
}
ChevronButton(L10n.images)
.onSelect {
ChevronButton(L10n.images) {
router.route(to: \.editImages, ItemImagesViewModel(item: viewModel.item))
}
ChevronButton(L10n.metadata)
.onSelect {
ChevronButton(L10n.metadata) {
router.route(to: \.editMetadata, viewModel.item)
}
}
Section {
ChevronButton(L10n.genres)
.onSelect {
ChevronButton(L10n.genres) {
router.route(to: \.editGenres, viewModel.item)
}
ChevronButton(L10n.people)
.onSelect {
ChevronButton(L10n.people) {
router.route(to: \.editPeople, viewModel.item)
}
ChevronButton(L10n.tags)
.onSelect {
ChevronButton(L10n.tags) {
router.route(to: \.editTags, viewModel.item)
}
ChevronButton(L10n.studios)
.onSelect {
ChevronButton(L10n.studios) {
router.route(to: \.editStudios, viewModel.item)
}
}

View File

@ -92,8 +92,7 @@ extension ItemImageDetailsView {
ChevronButton(
L10n.imageSource,
external: true
)
.onSelect {
) {
UIApplication.shared.open(url)
}
}

View File

@ -24,7 +24,7 @@ extension EditMetadataView {
// MARK: - Season Number
ChevronAlertButton(
ChevronButton(
L10n.season,
subtitle: item.parentIndexNumber?.description,
description: L10n.enterSeasonNumber
@ -39,7 +39,7 @@ extension EditMetadataView {
// MARK: - Episode Number
ChevronAlertButton(
ChevronButton(
L10n.episode,
subtitle: item.indexNumber?.description,
description: L10n.enterEpisodeNumber

View File

@ -24,7 +24,7 @@ extension EditMetadataView {
// MARK: - Critics Rating
ChevronAlertButton(
ChevronButton(
L10n.critics,
subtitle: item.criticRating.map { "\($0)" } ?? .emptyDash,
description: L10n.ratingDescription(L10n.critics)
@ -44,7 +44,7 @@ extension EditMetadataView {
// MARK: - Community Rating
ChevronAlertButton(
ChevronButton(
L10n.community,
subtitle: item.communityRating.map { "\($0)" } ?? .emptyDash,
description: L10n.ratingDescription(L10n.community)

View File

@ -99,7 +99,7 @@ extension EditMetadataView {
@ViewBuilder
private var runTimeView: some View {
ChevronAlertButton(
ChevronButton(
L10n.runtime,
subtitle: ServerTicks(item.runTimeTicks ?? 0)
.seconds.formatted(.hourMinute),

View File

@ -24,8 +24,7 @@ struct MediaSourceInfoView: View {
{
Section(L10n.video) {
ForEach(videoStreams, id: \.self) { stream in
ChevronButton(stream.displayTitle ?? .emptyDash)
.onSelect {
ChevronButton(stream.displayTitle ?? .emptyDash) {
router.route(to: \.mediaStreamInfo, stream)
}
}
@ -37,8 +36,7 @@ struct MediaSourceInfoView: View {
{
Section(L10n.audio) {
ForEach(audioStreams, id: \.self) { stream in
ChevronButton(stream.displayTitle ?? .emptyDash)
.onSelect {
ChevronButton(stream.displayTitle ?? .emptyDash) {
router.route(to: \.mediaStreamInfo, stream)
}
}
@ -50,8 +48,7 @@ struct MediaSourceInfoView: View {
{
Section(L10n.subtitle) {
ForEach(subtitleStreams, id: \.self) { stream in
ChevronButton(stream.displayTitle ?? .emptyDash)
.onSelect {
ChevronButton(stream.displayTitle ?? .emptyDash) {
router.route(to: \.mediaStreamInfo, stream)
}
}

View File

@ -27,7 +27,7 @@ extension CustomizeViewsSettings {
Toggle(L10n.nextUpRewatch, isOn: $resumeNextUp)
ChevronAlertButton(
ChevronButton(
L10n.nextUpDays,
subtitle: {
if maxNextUp > 0 {

View File

@ -35,8 +35,7 @@ extension CustomizeViewsSettings {
var body: some View {
Section(L10n.items) {
ChevronButton(L10n.mediaAttributes)
.onSelect {
ChevronButton(L10n.mediaAttributes) {
router.route(to: \.itemViewAttributes, $itemViewAttributes)
}

View File

@ -104,13 +104,11 @@ struct CustomizeViewsSettings: View {
)
}
ChevronButton(L10n.library)
.onSelect {
ChevronButton(L10n.library) {
router.route(to: \.itemFilterDrawerSelector, $libraryEnabledDrawerFilters)
}
ChevronButton(L10n.search)
.onSelect {
ChevronButton(L10n.search) {
router.route(to: \.itemFilterDrawerSelector, $searchEnabledDrawerFilters)
}
@ -127,8 +125,7 @@ struct CustomizeViewsSettings: View {
Section(L10n.posters) {
ChevronButton(L10n.indicators)
.onSelect {
ChevronButton(L10n.indicators) {
router.route(to: \.indicatorSettings)
}

View File

@ -70,8 +70,7 @@ struct PlaybackQualitySettingsView: View {
.animation(.none, value: compatibilityMode)
if compatibilityMode == .custom {
ChevronButton(L10n.profiles)
.onSelect {
ChevronButton(L10n.profiles) {
router.route(to: \.customDeviceProfileSettings)
}
}

View File

@ -38,14 +38,12 @@ struct SettingsView: View {
ChevronButton(
L10n.server,
subtitle: viewModel.userSession.server.name
)
.onSelect {
) {
router.route(to: \.serverConnection, viewModel.userSession.server)
}
if viewModel.userSession.user.permissions.isAdministrator {
ChevronButton(L10n.dashboard)
.onSelect {
ChevronButton(L10n.dashboard) {
router.route(to: \.adminDashboard)
}
}
@ -66,18 +64,15 @@ struct SettingsView: View {
selection: $videoPlayerType
)
ChevronButton(L10n.nativePlayer)
.onSelect {
ChevronButton(L10n.nativePlayer) {
router.route(to: \.nativePlayerSettings)
}
ChevronButton(L10n.videoPlayer)
.onSelect {
ChevronButton(L10n.videoPlayer) {
router.route(to: \.videoPlayerSettings)
}
ChevronButton(L10n.playbackQuality)
.onSelect {
ChevronButton(L10n.playbackQuality) {
router.route(to: \.playbackQualitySettings)
}
}
@ -85,8 +80,7 @@ struct SettingsView: View {
Section(L10n.accessibility) {
CaseIterablePicker(L10n.appearance, selection: $appearance)
ChevronButton(L10n.customize)
.onSelect {
ChevronButton(L10n.customize) {
router.route(to: \.customizeViewsSettings)
}
@ -105,15 +99,13 @@ struct SettingsView: View {
Text(L10n.viewsMayRequireRestart)
}
ChevronButton(L10n.logs)
.onSelect {
ChevronButton(L10n.logs) {
router.route(to: \.log)
}
#if DEBUG
ChevronButton("Debug")
.onSelect {
ChevronButton("Debug") {
router.route(to: \.debugSettings)
}

View File

@ -44,20 +44,17 @@ struct UserProfileSettingsView: View {
}
Section {
ChevronButton(L10n.quickConnect)
.onSelect {
ChevronButton(L10n.quickConnect) {
router.route(to: \.quickConnect)
}
ChevronButton(L10n.password)
.onSelect {
ChevronButton(L10n.password) {
router.route(to: \.resetUserPassword, viewModel.userSession.user.id)
}
}
Section {
ChevronButton(L10n.security)
.onSelect {
ChevronButton(L10n.security) {
router.route(to: \.localSecurity)
}
}

View File

@ -42,13 +42,11 @@ extension VideoPlayerSettingsView {
}
}
ChevronButton(L10n.barButtons)
.onSelect {
ChevronButton(L10n.barButtons) {
router.route(to: \.actionButtonSelector, $barActionButtons)
}
ChevronButton(L10n.menuButtons)
.onSelect {
ChevronButton(L10n.menuButtons) {
router.route(to: \.actionButtonSelector, $menuActionButtons)
}
}

View File

@ -23,8 +23,7 @@ extension VideoPlayerSettingsView {
var body: some View {
Section {
ChevronButton(L10n.subtitleFont, subtitle: subtitleFontName)
.onSelect {
ChevronButton(L10n.subtitleFont, subtitle: subtitleFontName) {
router.route(to: \.fontPicker, $subtitleFontName)
}

View File

@ -24,8 +24,7 @@ struct VideoPlayerSettingsView: View {
var body: some View {
Form {
ChevronButton(L10n.gestures)
.onSelect {
ChevronButton(L10n.gestures) {
router.route(to: \.gestureSettings)
}

View File

@ -34,8 +34,7 @@ struct PlaybackSettingsView: View {
Form {
Section {
ChevronButton(L10n.videoPlayer)
.onSelect {
ChevronButton(L10n.videoPlayer) {
router.route(to: \.videoPlayerSettings)
}
@ -67,8 +66,7 @@ struct PlaybackSettingsView: View {
if viewModel.videoStreams.isNotEmpty {
Section(L10n.video) {
ForEach(viewModel.videoStreams, id: \.displayTitle) { mediaStream in
ChevronButton(mediaStream.displayTitle ?? .emptyDash)
.onSelect {
ChevronButton(mediaStream.displayTitle ?? .emptyDash) {
router.route(to: \.mediaStreamInfo, mediaStream)
}
}
@ -78,8 +76,7 @@ struct PlaybackSettingsView: View {
if viewModel.audioStreams.isNotEmpty {
Section(L10n.audio) {
ForEach(viewModel.audioStreams, id: \.displayTitle) { mediaStream in
ChevronButton(mediaStream.displayTitle ?? .emptyDash)
.onSelect {
ChevronButton(mediaStream.displayTitle ?? .emptyDash) {
router.route(to: \.mediaStreamInfo, mediaStream)
}
}
@ -89,8 +86,7 @@ struct PlaybackSettingsView: View {
if viewModel.subtitleStreams.isNotEmpty {
Section(L10n.subtitle) {
ForEach(viewModel.subtitleStreams, id: \.displayTitle) { mediaStream in
ChevronButton(mediaStream.displayTitle ?? .emptyDash)
.onSelect {
ChevronButton(mediaStream.displayTitle ?? .emptyDash) {
router.route(to: \.mediaStreamInfo, mediaStream)
}
}