Generic Button to Input from an Alert (#1273)
Some checks are pending
Build 🔨 / Build 🔨 (Swiftfin tvOS) (push) Waiting to run
Build 🔨 / Build 🔨 (Swiftfin) (push) Waiting to run

* Creation of an Alert Input Button based on the Chevron Button.

* Only one button required for both iOS and tvOS

* wip

---------

Co-authored-by: Ethan Pippin <ethanpippin2343@gmail.com>
This commit is contained in:
Joe 2024-10-15 00:42:42 -06:00 committed by GitHub
parent 43811aa50d
commit 2bda693143
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 340 additions and 249 deletions

View File

@ -0,0 +1,96 @@
//
// 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) 2024 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 = 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

@ -0,0 +1,161 @@
//
// 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) 2024 Jellyfin & Jellyfin Contributors
//
import SwiftUI
struct ChevronButton<Icon: View>: View {
private let icon: Icon
private let isExternal: Bool
private let title: Text
private let subtitle: Text?
private var onSelect: () -> Void
var body: some View {
Button(action: onSelect) {
HStack {
icon
.font(.body.weight(.bold))
title
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: {}
)
}
}
extension ChevronButton {
func onSelect(perform action: @escaping () -> Void) -> Self {
copy(modifying: \.onSelect, with: action)
}
}

View File

@ -77,3 +77,31 @@ extension ParseableFormatStyle where Self == DayIntervalParseableFormatStyle {
.init(range: range)
}
}
extension FormatStyle where Self == TimeIntervalFormatStyle {
static func interval(
style: Date.ComponentsFormatStyle.Style,
fields: Set<Date.ComponentsFormatStyle.Field>
) -> TimeIntervalFormatStyle {
TimeIntervalFormatStyle(style: style, fields: fields)
}
}
struct TimeIntervalFormatStyle: FormatStyle {
let style: Date.ComponentsFormatStyle.Style
let fields: Set<Date.ComponentsFormatStyle.Field>
func format(_ value: TimeInterval) -> String {
let value = abs(value)
let t = Date.now
return Date.ComponentsFormatStyle(
style: style,
locale: .current,
calendar: .current,
fields: fields
).format(t ..< t.addingTimeInterval(value))
}
}

View File

@ -230,6 +230,8 @@ internal enum L10n {
internal static let dashboard = L10n.tr("Localizable", "dashboard", fallback: "Dashboard")
/// Perform administrative tasks for your Jellyfin server.
internal static let dashboardDescription = L10n.tr("Localizable", "dashboardDescription", fallback: "Perform administrative tasks for your Jellyfin server.")
/// Days
internal static let days = L10n.tr("Localizable", "days", fallback: "Days")
/// Default Scheme
internal static let defaultScheme = L10n.tr("Localizable", "defaultScheme", fallback: "Default Scheme")
/// Delete
@ -434,8 +436,8 @@ internal enum L10n {
internal static let nextUp = L10n.tr("Localizable", "nextUp", fallback: "Next Up")
/// Days in Next Up
internal static let nextUpDays = L10n.tr("Localizable", "nextUpDays", fallback: "Days in Next Up")
/// Set the maximum amount of days a show should stay in the 'Next Up' list without watching it.
internal static let nextUpDaysDescription = L10n.tr("Localizable", "nextUpDaysDescription", fallback: "Set the maximum amount of days a show should stay in the 'Next Up' list without watching it.")
/// Set the maximum amount of days a show should stay in the 'Next Up' list without watching it. Set the value to 0 to disable.
internal static let nextUpDaysDescription = L10n.tr("Localizable", "nextUpDaysDescription", fallback: "Set the maximum amount of days a show should stay in the 'Next Up' list without watching it. Set the value to 0 to disable.")
/// Rewatching in Next Up
internal static let nextUpRewatch = L10n.tr("Localizable", "nextUpRewatch", fallback: "Rewatching in Next Up")
/// No Cast devices found..
@ -638,6 +640,8 @@ internal enum L10n {
internal static let running = L10n.tr("Localizable", "running", fallback: "Running...")
/// Runtime
internal static let runtime = L10n.tr("Localizable", "runtime", fallback: "Runtime")
/// Save
internal static let save = L10n.tr("Localizable", "save", fallback: "Save")
/// Scan All Libraries
internal static let scanAllLibraries = L10n.tr("Localizable", "scanAllLibraries", fallback: "Scan All Libraries")
/// Scheduled Tasks

View File

@ -1,85 +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) 2024 Jellyfin & Jellyfin Contributors
//
import SwiftUI
struct ChevronButton: View {
private let isExternal: Bool
private let title: Text
private let subtitle: Text?
private var leadingView: () -> any View
private var onSelect: () -> Void
var body: some View {
Button {
onSelect()
} label: {
HStack {
leadingView()
.eraseToAnyView()
title
.foregroundColor(.primary)
Spacer()
if let subtitle {
subtitle
.foregroundColor(.secondary)
}
Image(systemName: isExternal ? "arrow.up.forward" : "chevron.right")
.font(.body.weight(.regular))
.foregroundColor(.secondary)
}
}
}
}
extension ChevronButton {
init(
_ title: String,
subtitle: String? = nil,
external: Bool = false
) {
self.init(
isExternal: external,
title: Text(title),
subtitle: {
if let subtitle {
Text(subtitle)
} else {
nil
}
}(),
leadingView: { EmptyView() },
onSelect: {}
)
}
init(_ title: String, external: Bool = false, subtitle: @autoclosure () -> Text) {
self.init(
isExternal: external,
title: Text(title),
subtitle: subtitle(),
leadingView: { EmptyView() },
onSelect: {}
)
}
func leadingView(@ViewBuilder _ content: @escaping () -> any View) -> Self {
copy(modifying: \.leadingView, with: content)
}
func onSelect(_ action: @escaping () -> Void) -> Self {
copy(modifying: \.onSelect, with: action)
}
}

View File

@ -10,6 +10,7 @@ import Defaults
import SwiftUI
extension CustomizeViewsSettings {
struct HomeSection: View {
@Default(.Customization.Home.showRecentlyAdded)
@ -29,34 +30,23 @@ extension CustomizeViewsSettings {
Toggle(L10n.nextUpRewatch, isOn: $resumeNextUp)
ChevronButton(
ChevronAlertButton(
L10n.nextUpDays,
subtitle: {
if maxNextUp > 0 {
return Text(
Date.now.addingTimeInterval(-maxNextUp) ..< Date.now,
format: .components(style: .narrow, fields: [.year, .month, .week, .day])
)
return Text(maxNextUp, format: .interval(style: .narrow, fields: [.day]))
} else {
return Text(L10n.disabled)
}
}()
)
.onSelect {
isPresentingNextUpDays = true
}
.alert(L10n.nextUpDays, isPresented: $isPresentingNextUpDays) {
// TODO: Validate whether this says Done or a Number
}(),
description: L10n.nextUpDaysDescription
) {
TextField(
L10n.nextUpDays,
L10n.days,
value: $maxNextUp,
format: .dayInterval(range: 0 ... 1000)
)
.keyboardType(.numberPad)
} message: {
L10n.nextUpDaysDescription.text
}
}
}

View File

@ -34,6 +34,7 @@
4E2AC4D42C6C4C1200DD600D /* OrderedSectionSelectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2AC4D32C6C4C1200DD600D /* OrderedSectionSelectorView.swift */; };
4E2AC4D62C6C4CDC00DD600D /* PlaybackQualitySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2AC4D52C6C4CDC00DD600D /* PlaybackQualitySettingsView.swift */; };
4E2AC4D92C6C4D9400DD600D /* PlaybackQualitySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E2AC4D72C6C4D8D00DD600D /* PlaybackQualitySettingsView.swift */; };
4E4A53222CBE0A1C003BD24D /* ChevronAlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB7B33A2CBDE63F004A342E /* ChevronAlertButton.swift */; };
4E5E48E52AB59806003F1B48 /* CustomizeViewsSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */; };
4E63B9FA2C8A5BEF00C25378 /* UserDashboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E63B9F42C8A5BEF00C25378 /* UserDashboardView.swift */; };
4E63B9FC2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E63B9FB2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift */; };
@ -56,6 +57,7 @@
4EB1A8CA2C9A766200F43898 /* ActiveSessionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1A8C92C9A765800F43898 /* ActiveSessionsView.swift */; };
4EB1A8CC2C9B1BA200F43898 /* ServerTaskButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1A8CB2C9B1B9700F43898 /* ServerTaskButton.swift */; };
4EB1A8CE2C9B2D0800F43898 /* ActiveSessionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB1A8CD2C9B2D0100F43898 /* ActiveSessionRow.swift */; };
4EB7B33B2CBDE645004A342E /* ChevronAlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB7B33A2CBDE63F004A342E /* ChevronAlertButton.swift */; };
4EBE06462C7E9509004A6C03 /* PlaybackCompatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBE06452C7E9509004A6C03 /* PlaybackCompatibility.swift */; };
4EBE06472C7E9509004A6C03 /* PlaybackCompatibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBE06452C7E9509004A6C03 /* PlaybackCompatibility.swift */; };
4EBE064D2C7EB6D3004A6C03 /* VideoPlayerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EBE064C2C7EB6D3004A6C03 /* VideoPlayerType.swift */; };
@ -389,7 +391,6 @@
E12CC1CB28D1333400678D5D /* CinematicResumeItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12CC1CA28D1333400678D5D /* CinematicResumeItemView.swift */; };
E12CC1CD28D135C700678D5D /* NextUpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12CC1CC28D135C700678D5D /* NextUpView.swift */; };
E12E30F1296383810022FAC9 /* SplitFormWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12E30F0296383810022FAC9 /* SplitFormWindowView.swift */; };
E12E30F329638B140022FAC9 /* ChevronButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12E30F229638B140022FAC9 /* ChevronButton.swift */; };
E12E30F5296392EC0022FAC9 /* EnumPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12E30F4296392EC0022FAC9 /* EnumPickerView.swift */; };
E12F038C28F8B0B100976CC3 /* EdgeInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12F038B28F8B0B100976CC3 /* EdgeInsets.swift */; };
E132D3C82BD200C10058A2DF /* CollectionVGrid in Frameworks */ = {isa = PBXBuildFile; productRef = E132D3C72BD200C10058A2DF /* CollectionVGrid */; };
@ -641,6 +642,7 @@
E17FB55B28C1266400311DFE /* GenresHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17FB55A28C1266400311DFE /* GenresHStack.swift */; };
E1803EA12BFBD6CF0039F90E /* Hashable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1803EA02BFBD6CF0039F90E /* Hashable.swift */; };
E1803EA22BFBD6CF0039F90E /* Hashable.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1803EA02BFBD6CF0039F90E /* Hashable.swift */; };
E18121062CBE428000682985 /* ChevronButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A1528728FD229500600579 /* ChevronButton.swift */; };
E18295E429CAC6F100F91ED0 /* BasicNavigationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E154967D296CCB6C00C4EF88 /* BasicNavigationCoordinator.swift */; };
E18443CB2A037773002DDDC8 /* UDPBroadcast in Frameworks */ = {isa = PBXBuildFile; productRef = E18443CA2A037773002DDDC8 /* UDPBroadcast */; };
E185920628CDAA6400326F80 /* CastAndCrewHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E185920528CDAA6400326F80 /* CastAndCrewHStack.swift */; };
@ -1053,6 +1055,7 @@
4EB1A8C92C9A765800F43898 /* ActiveSessionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionsView.swift; sourceTree = "<group>"; };
4EB1A8CB2C9B1B9700F43898 /* ServerTaskButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTaskButton.swift; sourceTree = "<group>"; };
4EB1A8CD2C9B2D0100F43898 /* ActiveSessionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionRow.swift; sourceTree = "<group>"; };
4EB7B33A2CBDE63F004A342E /* ChevronAlertButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChevronAlertButton.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>"; };
4EBE06502C7ED0E1004A6C03 /* DeviceProfile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceProfile.swift; sourceTree = "<group>"; };
@ -1307,7 +1310,6 @@
E12CC1CA28D1333400678D5D /* CinematicResumeItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicResumeItemView.swift; sourceTree = "<group>"; };
E12CC1CC28D135C700678D5D /* NextUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextUpView.swift; sourceTree = "<group>"; };
E12E30F0296383810022FAC9 /* SplitFormWindowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitFormWindowView.swift; sourceTree = "<group>"; };
E12E30F229638B140022FAC9 /* ChevronButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChevronButton.swift; sourceTree = "<group>"; };
E12E30F4296392EC0022FAC9 /* EnumPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnumPickerView.swift; sourceTree = "<group>"; };
E12F038B28F8B0B100976CC3 /* EdgeInsets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EdgeInsets.swift; sourceTree = "<group>"; };
E13316FD2ADE42B6009BF865 /* OnSizeChangedModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnSizeChangedModifier.swift; sourceTree = "<group>"; };
@ -2195,7 +2197,6 @@
536D3D77267BB9650004248C /* Components */ = {
isa = PBXGroup;
children = (
E12E30F229638B140022FAC9 /* ChevronButton.swift */,
E1DC9818296DD1CD00982F06 /* CinematicBackgroundView.swift */,
E1A42E4928CA6CCD00A14DCB /* CinematicItemSelector.swift */,
E1C92618288756BD002A7A66 /* DotHStack.swift */,
@ -2430,7 +2431,6 @@
isa = PBXGroup;
children = (
E1D8429429346C6400D1041A /* BasicStepper.swift */,
E1A1528728FD229500600579 /* ChevronButton.swift */,
E133328C2953AE4B00EE76AB /* CircularProgressView.swift */,
E1A3E4CE2BB7E02B005C59F8 /* DelayedProgressView.swift */,
E18E01A7288746AF0022598C /* DotHStack.swift */,
@ -3594,6 +3594,8 @@
E104DC952B9E7E29008F506D /* AssertionFailureView.swift */,
E18E0203288749200022598C /* BlurView.swift */,
E145EB212BDCCA43003BF6F3 /* BulletedList.swift */,
4EB7B33A2CBDE63F004A342E /* ChevronAlertButton.swift */,
E1A1528728FD229500600579 /* ChevronButton.swift */,
E1153DCB2BBB633B00424D36 /* FastSVGView.swift */,
531AC8BE26750DE20091C7EB /* ImageView.swift */,
4E16FD562C01A32700110147 /* LetterPickerOrientation.swift */,
@ -4338,6 +4340,7 @@
E1575E74293E77B5001665B1 /* PanDirectionGestureRecognizer.swift in Sources */,
E1575E85293E7A00001665B1 /* DarkAppIcon.swift in Sources */,
E1763A762BF3FF01004DF6AB /* AppLoadingView.swift in Sources */,
E18121062CBE428000682985 /* ChevronButton.swift in Sources */,
E102315A2BCF8AF8009D71FC /* ProgramButtonContent.swift in Sources */,
E17639F82BF2E25B004DF6AB /* Keychain.swift in Sources */,
C45C36552A8B1F2C003DAE46 /* LiveVideoPlayerManager.swift in Sources */,
@ -4381,7 +4384,6 @@
4E9A24E92C82B79D0023DA83 /* EditCustomDeviceProfileCoordinator.swift in Sources */,
E1C9261C288756BD002A7A66 /* PosterHStack.swift in Sources */,
E1722DB229491C3900CC0239 /* ImageBlurHashes.swift in Sources */,
E12E30F329638B140022FAC9 /* ChevronButton.swift in Sources */,
4E9A24ED2C82BAFB0023DA83 /* EditCustomDeviceProfileView.swift in Sources */,
E12CC1BC28D11E1000678D5D /* RecentlyAddedViewModel.swift in Sources */,
E1575E9E293E7B1E001665B1 /* Equatable.swift in Sources */,
@ -4468,6 +4470,7 @@
E1763A722BF3F67C004DF6AB /* SwiftfinStore+Mappings.swift in Sources */,
E1937A3C288E54AD00CB80AA /* BaseItemDto+Images.swift in Sources */,
E18A17F0298C68B700C22F62 /* Overlay.swift in Sources */,
4E4A53222CBE0A1C003BD24D /* ChevronAlertButton.swift in Sources */,
E1A42E4A28CA6CCD00A14DCB /* CinematicItemSelector.swift in Sources */,
4E2AC4CF2C6C4A0600DD600D /* PlaybackQualitySettingsCoordinator.swift in Sources */,
E1D37F492B9C648E00343D2B /* MaxHeightText.swift in Sources */,
@ -5044,6 +5047,7 @@
E1721FAE28FB801C00762992 /* SmallPlaybackButtons.swift in Sources */,
E1A7B1662B9ADAD300152546 /* ItemTypeLibraryViewModel.swift in Sources */,
E1ED7FD62CA8A7FD00ACB6E3 /* EditScheduledTaskView.swift in Sources */,
4EB7B33B2CBDE645004A342E /* ChevronAlertButton.swift in Sources */,
E1545BD82BDC55C300D9578F /* ResetUserPasswordView.swift in Sources */,
E1E750682A33E9B400B2C1EE /* OverviewCard.swift in Sources */,
E1CCF13128AC07EC006CAC9E /* PosterHStack.swift in Sources */,

View File

@ -1,85 +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) 2024 Jellyfin & Jellyfin Contributors
//
import SwiftUI
struct ChevronButton: View {
private let isExternal: Bool
private let title: Text
private let subtitle: Text?
private var leadingView: () -> any View
private var onSelect: () -> Void
var body: some View {
Button {
onSelect()
} label: {
HStack {
leadingView()
.eraseToAnyView()
title
.foregroundColor(.primary)
Spacer()
if let subtitle {
subtitle
.foregroundColor(.secondary)
}
Image(systemName: isExternal ? "arrow.up.forward" : "chevron.right")
.font(.body.weight(.regular))
.foregroundColor(.secondary)
}
}
}
}
extension ChevronButton {
init(
_ title: String,
subtitle: String? = nil,
external: Bool = false
) {
self.init(
isExternal: external,
title: Text(title),
subtitle: {
if let subtitle {
Text(subtitle)
} else {
nil
}
}(),
leadingView: { EmptyView() },
onSelect: {}
)
}
init(_ title: String, external: Bool = false, subtitle: @autoclosure () -> Text) {
self.init(
isExternal: external,
title: Text(title),
subtitle: subtitle(),
leadingView: { EmptyView() },
onSelect: {}
)
}
func leadingView(@ViewBuilder _ content: @escaping () -> any View) -> Self {
copy(modifying: \.leadingView, with: content)
}
func onSelect(_ action: @escaping () -> Void) -> Self {
copy(modifying: \.onSelect, with: action)
}
}

View File

@ -38,44 +38,34 @@ struct AboutAppView: View {
trailing: "\(UIApplication.appVersion ?? .emptyDash) (\(UIApplication.bundleVersion ?? .emptyDash))"
)
ChevronButton(L10n.sourceCode, external: true)
.leadingView {
Image(.logoGithub)
.resizable()
.aspectRatio(1, contentMode: .fit)
.frame(width: 24, height: 24)
.foregroundColor(.primary)
}
.onSelect {
UIApplication.shared.open(.swiftfinGithub)
}
ChevronButton(
L10n.sourceCode,
image: Image(.logoGithub),
external: true
)
.onSelect {
UIApplication.shared.open(.swiftfinGithub)
}
ChevronButton(L10n.bugsAndFeatures, external: true)
.leadingView {
Image(systemName: "plus.circle.fill")
.resizable()
.backport
.fontWeight(.bold)
.aspectRatio(1, contentMode: .fit)
.frame(width: 24, height: 24)
.foregroundColor(.primary)
}
.onSelect {
UIApplication.shared.open(.swiftfinGithubIssues)
}
ChevronButton(
L10n.bugsAndFeatures,
systemName: "plus.circle.fill",
external: true
)
.onSelect {
UIApplication.shared.open(.swiftfinGithubIssues)
}
.symbolRenderingMode(.monochrome)
ChevronButton(L10n.settings, external: true)
.leadingView {
Image(systemName: "gearshape.fill")
.resizable()
.aspectRatio(1, contentMode: .fit)
.frame(width: 24, height: 24)
.foregroundColor(.primary)
}
.onSelect {
guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
UIApplication.shared.open(url)
}
ChevronButton(
L10n.settings,
systemName: "gearshape.fill",
external: true
)
.onSelect {
guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
UIApplication.shared.open(url)
}
}
}
}

View File

@ -10,6 +10,7 @@ import Defaults
import SwiftUI
extension CustomizeViewsSettings {
struct HomeSection: View {
@Default(.Customization.Home.showRecentlyAdded)
@ -19,9 +20,6 @@ extension CustomizeViewsSettings {
@Default(.Customization.Home.resumeNextUp)
private var resumeNextUp
@State
private var isPresentingNextUpDays = false
var body: some View {
Section(L10n.home) {
@ -29,33 +27,23 @@ extension CustomizeViewsSettings {
Toggle(L10n.nextUpRewatch, isOn: $resumeNextUp)
ChevronButton(
ChevronAlertButton(
L10n.nextUpDays,
subtitle: {
if maxNextUp > 0 {
return Text(
Date.now.addingTimeInterval(-maxNextUp) ..< Date.now,
format: .components(style: .narrow, fields: [.year, .month, .week, .day])
)
return Text(maxNextUp, format: .interval(style: .narrow, fields: [.day]))
} else {
return Text(L10n.disabled)
}
}()
)
.onSelect {
isPresentingNextUpDays = true
}
.alert(L10n.nextUpDays, isPresented: $isPresentingNextUpDays) {
}(),
description: L10n.nextUpDaysDescription
) {
TextField(
L10n.nextUpDays,
L10n.days,
value: $maxNextUp,
format: .dayInterval(range: 0 ... 1000)
)
.keyboardType(.numberPad)
} message: {
L10n.nextUpDaysDescription.text
}
}
}