diff --git a/Shared/Components/ChevronAlertButton.swift b/Shared/Components/ChevronAlertButton.swift new file mode 100644 index 00000000..2844f878 --- /dev/null +++ b/Shared/Components/ChevronAlertButton.swift @@ -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: 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 + ) + } +} diff --git a/Shared/Components/ChevronButton.swift b/Shared/Components/ChevronButton.swift new file mode 100644 index 00000000..46be21aa --- /dev/null +++ b/Shared/Components/ChevronButton.swift @@ -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: 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) + } +} diff --git a/Shared/Extensions/FormatStyle.swift b/Shared/Extensions/FormatStyle.swift index b0bbabb6..a88d4a8b 100644 --- a/Shared/Extensions/FormatStyle.swift +++ b/Shared/Extensions/FormatStyle.swift @@ -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 + ) -> TimeIntervalFormatStyle { + TimeIntervalFormatStyle(style: style, fields: fields) + } +} + +struct TimeIntervalFormatStyle: FormatStyle { + + let style: Date.ComponentsFormatStyle.Style + let fields: Set + + 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)) + } +} diff --git a/Shared/Strings/Strings.swift b/Shared/Strings/Strings.swift index 6098c600..c7101a92 100644 --- a/Shared/Strings/Strings.swift +++ b/Shared/Strings/Strings.swift @@ -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 diff --git a/Swiftfin tvOS/Components/ChevronButton.swift b/Swiftfin tvOS/Components/ChevronButton.swift deleted file mode 100644 index ce2d19d5..00000000 --- a/Swiftfin tvOS/Components/ChevronButton.swift +++ /dev/null @@ -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) - } -} diff --git a/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/Sections/HomeSection.swift b/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/Sections/HomeSection.swift index e7d76ee7..f3db9a58 100644 --- a/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/Sections/HomeSection.swift +++ b/Swiftfin tvOS/Views/SettingsView/CustomizeViewsSettings/Components/Sections/HomeSection.swift @@ -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 } } } diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 6dce97af..ea6ddfa7 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -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 = ""; }; 4EB1A8CB2C9B1B9700F43898 /* ServerTaskButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTaskButton.swift; sourceTree = ""; }; 4EB1A8CD2C9B2D0100F43898 /* ActiveSessionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionRow.swift; sourceTree = ""; }; + 4EB7B33A2CBDE63F004A342E /* ChevronAlertButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChevronAlertButton.swift; sourceTree = ""; }; 4EBE06452C7E9509004A6C03 /* PlaybackCompatibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackCompatibility.swift; sourceTree = ""; }; 4EBE064C2C7EB6D3004A6C03 /* VideoPlayerType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerType.swift; sourceTree = ""; }; 4EBE06502C7ED0E1004A6C03 /* DeviceProfile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceProfile.swift; sourceTree = ""; }; @@ -1307,7 +1310,6 @@ E12CC1CA28D1333400678D5D /* CinematicResumeItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicResumeItemView.swift; sourceTree = ""; }; E12CC1CC28D135C700678D5D /* NextUpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NextUpView.swift; sourceTree = ""; }; E12E30F0296383810022FAC9 /* SplitFormWindowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitFormWindowView.swift; sourceTree = ""; }; - E12E30F229638B140022FAC9 /* ChevronButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChevronButton.swift; sourceTree = ""; }; E12E30F4296392EC0022FAC9 /* EnumPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnumPickerView.swift; sourceTree = ""; }; E12F038B28F8B0B100976CC3 /* EdgeInsets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EdgeInsets.swift; sourceTree = ""; }; E13316FD2ADE42B6009BF865 /* OnSizeChangedModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnSizeChangedModifier.swift; sourceTree = ""; }; @@ -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 */, diff --git a/Swiftfin/Components/ChevronButton.swift b/Swiftfin/Components/ChevronButton.swift deleted file mode 100644 index ce2d19d5..00000000 --- a/Swiftfin/Components/ChevronButton.swift +++ /dev/null @@ -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) - } -} diff --git a/Swiftfin/Views/AboutAppView.swift b/Swiftfin/Views/AboutAppView.swift index 0626e319..5ca359ac 100644 --- a/Swiftfin/Views/AboutAppView.swift +++ b/Swiftfin/Views/AboutAppView.swift @@ -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) + } } } } diff --git a/Swiftfin/Views/SettingsView/CustomizeViewsSettings/Components/Sections/HomeSection.swift b/Swiftfin/Views/SettingsView/CustomizeViewsSettings/Components/Sections/HomeSection.swift index bb7da2d0..8c1b5bdf 100644 --- a/Swiftfin/Views/SettingsView/CustomizeViewsSettings/Components/Sections/HomeSection.swift +++ b/Swiftfin/Views/SettingsView/CustomizeViewsSettings/Components/Sections/HomeSection.swift @@ -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 } } } diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index d2d1b6b1..7233f3b4 100644 Binary files a/Translations/en.lproj/Localizable.strings and b/Translations/en.lproj/Localizable.strings differ