Add 'Enable Rewatching' and ' Max days' to Next Up (#1258)

* Rebasing https://github.com/jellyfin/Swiftfin/pull/1212 on Main. Also, less baggage and random crap.

* Change 0 to Disabled. Better mirror iOS and tvOS Alerts for MaxNextUpDays.

* Review Changes:

Don't use the property wrappers in non-view contexts. While they technically can still work, use the subscript instead at the usage sites.
Use the dayInterval(0 ... 1000) format instead, then we don't need maxNextUpDays.

* Remove unused strings, and unused variables

* Add a tvOS TODO to double check the Done/Number button on the alert.
This commit is contained in:
Joe 2024-10-07 15:02:18 -06:00 committed by GitHub
parent 973a9ea3a4
commit 1405d2695c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 297 additions and 31 deletions

View File

@ -52,3 +52,28 @@ extension FormatStyle where Self == RunTimeFormatStyle {
static var runtime: RunTimeFormatStyle { RunTimeFormatStyle() }
}
/// Represent intervals as 24 hour, 60 minute, 60 second days
struct DayIntervalParseableFormatStyle: ParseableFormatStyle {
let range: ClosedRange<Int>
var parseStrategy: DayIntervalParseStrategy = .init()
func format(_ value: TimeInterval) -> String {
"\(clamp(Int(value / 86400), min: range.lowerBound, max: range.upperBound))"
}
}
struct DayIntervalParseStrategy: ParseStrategy {
func parse(_ value: String) throws -> TimeInterval {
(TimeInterval(value) ?? 0) * 86400
}
}
extension ParseableFormatStyle where Self == DayIntervalParseableFormatStyle {
static func dayInterval(range: ClosedRange<Int>) -> DayIntervalParseableFormatStyle {
.init(range: range)
}
}

View File

@ -117,7 +117,6 @@ extension Defaults.Keys {
static let showPosterLabels: Key<Bool> = UserKey("showPosterLabels", default: true)
static let nextUpPosterType: Key<PosterDisplayType> = UserKey("nextUpPosterType", default: .portrait)
static let recentlyAddedPosterType: Key<PosterDisplayType> = UserKey("recentlyAddedPosterType", default: .portrait)
static let showRecentlyAdded: Key<Bool> = UserKey("showRecentlyAdded", default: true)
static let latestInLibraryPosterType: Key<PosterDisplayType> = UserKey("latestInLibraryPosterType", default: .portrait)
static let shouldShowMissingSeasons: Key<Bool> = UserKey("shouldShowMissingSeasons", default: true)
static let shouldShowMissingEpisodes: Key<Bool> = UserKey("shouldShowMissingEpisodes", default: true)
@ -165,6 +164,15 @@ extension Defaults.Keys {
static let rememberSort: Key<Bool> = UserKey("libraryRememberSort", default: false)
}
enum Home {
static let showRecentlyAdded: Key<Bool> = UserKey("showRecentlyAdded", default: true)
static let resumeNextUp: Key<Bool> = UserKey("homeResumeNextUp", default: true)
static let maxNextUp: Key<TimeInterval> = UserKey(
"homeMaxNextUp",
default: 366 * 86400
)
}
enum Search {
static let enabledDrawerFilters: Key<[ItemFilterType]> = UserKey(

View File

@ -258,6 +258,8 @@ internal enum L10n {
internal static let dismiss = L10n.tr("Localizable", "dismiss", fallback: "Dismiss")
/// Display order
internal static let displayOrder = L10n.tr("Localizable", "displayOrder", fallback: "Display order")
/// Done
internal static let done = L10n.tr("Localizable", "done", fallback: "Done")
/// Downloads
internal static let downloads = L10n.tr("Localizable", "downloads", fallback: "Downloads")
/// Edit
@ -428,6 +430,12 @@ internal enum L10n {
internal static let nextItem = L10n.tr("Localizable", "nextItem", fallback: "Next Item")
/// Next Up
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.")
/// Rewatching in Next Up
internal static let nextUpRewatch = L10n.tr("Localizable", "nextUpRewatch", fallback: "Rewatching in Next Up")
/// No Cast devices found..
internal static let noCastdevicesfound = L10n.tr("Localizable", "noCastdevicesfound", fallback: "No Cast devices found..")
/// No Codec

View File

@ -7,11 +7,15 @@
//
import Combine
import Defaults
import Foundation
import JellyfinAPI
final class NextUpLibraryViewModel: PagingLibraryViewModel<BaseItemDto> {
let maxNextUp = Defaults[.Customization.Home.maxNextUp]
let resumeNextUp = Defaults[.Customization.Home.resumeNextUp]
init() {
super.init(parent: TitledLibraryParent(displayTitle: L10n.nextUp, id: "nextUp"))
}
@ -31,6 +35,10 @@ final class NextUpLibraryViewModel: PagingLibraryViewModel<BaseItemDto> {
parameters.enableUserData = true
parameters.fields = .MinimumFields
parameters.limit = pageSize
if maxNextUp > 0 {
parameters.nextUpDateCutoff = Date.now.addingTimeInterval(-maxNextUp)
}
parameters.enableRewatching = resumeNextUp
parameters.startIndex = page
parameters.userID = userSession.user.id

View File

@ -10,8 +10,9 @@ import SwiftUI
struct ChevronButton: View {
private let title: String
private let subtitle: String?
private let isExternal: Bool
private let title: Text
private let subtitle: Text?
private var leadingView: () -> any View
private var onSelect: () -> Void
@ -24,17 +25,17 @@ struct ChevronButton: View {
leadingView()
.eraseToAnyView()
Text(title)
title
.foregroundColor(.primary)
Spacer()
if let subtitle {
Text(subtitle)
subtitle
.foregroundColor(.secondary)
}
Image(systemName: "chevron.right")
Image(systemName: isExternal ? "arrow.up.forward" : "chevron.right")
.font(.body.weight(.regular))
.foregroundColor(.secondary)
}
@ -44,10 +45,31 @@ struct ChevronButton: View {
extension ChevronButton {
init(_ title: String, subtitle: String? = nil) {
init(
_ title: String,
subtitle: String? = nil,
external: Bool = false
) {
self.init(
title: title,
subtitle: subtitle,
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: {}
)

View File

@ -19,7 +19,7 @@ struct HomeView: View {
@StateObject
private var viewModel = HomeViewModel()
@Default(.Customization.showRecentlyAdded)
@Default(.Customization.Home.showRecentlyAdded)
private var showRecentlyAdded
@ViewBuilder

View File

@ -0,0 +1,64 @@
//
// 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 Defaults
import SwiftUI
extension CustomizeViewsSettings {
struct HomeSection: View {
@Default(.Customization.Home.showRecentlyAdded)
private var showRecentlyAdded
@Default(.Customization.Home.maxNextUp)
private var maxNextUp
@Default(.Customization.Home.resumeNextUp)
private var resumeNextUp
@State
private var isPresentingNextUpDays = false
var body: some View {
Section(L10n.home) {
Toggle(L10n.showRecentlyAdded, isOn: $showRecentlyAdded)
Toggle(L10n.nextUpRewatch, isOn: $resumeNextUp)
ChevronButton(
L10n.nextUpDays,
subtitle: {
if maxNextUp > 0 {
return Text(
Date.now.addingTimeInterval(-maxNextUp) ..< Date.now,
format: .components(style: .narrow, fields: [.year, .month, .week, .day])
)
} else {
return Text(L10n.disabled)
}
}()
)
.onSelect {
isPresentingNextUpDays = true
}
.alert(L10n.nextUpDays, isPresented: $isPresentingNextUpDays) {
// TODO: Validate whether this says Done or a Number
TextField(
L10n.nextUpDays,
value: $maxNextUp,
format: .dayInterval(range: 0 ... 1000)
)
.keyboardType(.numberPad)
} message: {
L10n.nextUpDaysDescription.text
}
}
}
}
}

View File

@ -37,8 +37,6 @@ struct CustomizeViewsSettings: View {
private var libraryRandomImage
@Default(.Customization.Library.showFavorites)
private var showFavorites
@Default(.Customization.showRecentlyAdded)
private var showRecentlyAdded
@EnvironmentObject
private var router: CustomizeSettingsCoordinator.Router
@ -89,9 +87,9 @@ struct CustomizeViewsSettings: View {
Toggle(L10n.randomImage, isOn: $libraryRandomImage)
Toggle(L10n.showFavorites, isOn: $showFavorites)
Toggle(L10n.showRecentlyAdded, isOn: $showRecentlyAdded)
}
HomeSection()
}
.withDescriptionTopPadding()
.navigationTitle(L10n.customize)

View File

@ -37,6 +37,8 @@
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 */; };
4E699BB92CB33FC2007CBD5D /* HomeSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E699BB82CB33FB5007CBD5D /* HomeSection.swift */; };
4E699BC02CB3477D007CBD5D /* HomeSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E699BBF2CB34775007CBD5D /* HomeSection.swift */; };
4E6C27082C8BD0AD00FD2185 /* ActiveSessionDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E6C27072C8BD0AD00FD2185 /* ActiveSessionDetailView.swift */; };
4E71D6892C80910900A0174D /* EditCustomDeviceProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E71D6882C80910900A0174D /* EditCustomDeviceProfileView.swift */; };
4E73E2A62C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E73E2A52C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift */; };
@ -1033,6 +1035,8 @@
4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeViewsSettings.swift; sourceTree = "<group>"; };
4E63B9F42C8A5BEF00C25378 /* UserDashboardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDashboardView.swift; sourceTree = "<group>"; };
4E63B9FB2C8A5C3E00C25378 /* ActiveSessionsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActiveSessionsViewModel.swift; sourceTree = "<group>"; };
4E699BB82CB33FB5007CBD5D /* HomeSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeSection.swift; sourceTree = "<group>"; };
4E699BBF2CB34775007CBD5D /* HomeSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeSection.swift; sourceTree = "<group>"; };
4E6C27072C8BD0AD00FD2185 /* ActiveSessionDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionDetailView.swift; sourceTree = "<group>"; };
4E71D6882C80910900A0174D /* EditCustomDeviceProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditCustomDeviceProfileView.swift; sourceTree = "<group>"; };
4E73E2A52C41CFD3002D2A78 /* PlaybackBitrateTestSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackBitrateTestSize.swift; sourceTree = "<group>"; };
@ -1864,6 +1868,56 @@
path = UserDashboardView;
sourceTree = "<group>";
};
4E699BB52CB33F4B007CBD5D /* CustomizeViewsSettings */ = {
isa = PBXGroup;
children = (
4E699BB62CB33FA8007CBD5D /* Components */,
4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */,
);
path = CustomizeViewsSettings;
sourceTree = "<group>";
};
4E699BB62CB33FA8007CBD5D /* Components */ = {
isa = PBXGroup;
children = (
4E699BB72CB33FB0007CBD5D /* Sections */,
);
path = Components;
sourceTree = "<group>";
};
4E699BB72CB33FB0007CBD5D /* Sections */ = {
isa = PBXGroup;
children = (
4E699BB82CB33FB5007CBD5D /* HomeSection.swift */,
);
path = Sections;
sourceTree = "<group>";
};
4E699BBC2CB34740007CBD5D /* CustomizeViewsSettings */ = {
isa = PBXGroup;
children = (
4E699BBD2CB34746007CBD5D /* Components */,
E1CEFBF627914E6400F60429 /* CustomizeViewsSettings.swift */,
);
path = CustomizeViewsSettings;
sourceTree = "<group>";
};
4E699BBD2CB34746007CBD5D /* Components */ = {
isa = PBXGroup;
children = (
4E699BBE2CB3474C007CBD5D /* Sections */,
);
path = Components;
sourceTree = "<group>";
};
4E699BBE2CB3474C007CBD5D /* Sections */ = {
isa = PBXGroup;
children = (
4E699BBF2CB34775007CBD5D /* HomeSection.swift */,
);
path = Sections;
sourceTree = "<group>";
};
4E6C27062C8BD09200FD2185 /* ActiveSessionDetailView */ = {
isa = PBXGroup;
children = (
@ -3760,7 +3814,7 @@
isa = PBXGroup;
children = (
4EC1C86A2C80900B00E2879E /* CustomDeviceProfileSettingsView */,
4E5E48E42AB59806003F1B48 /* CustomizeViewsSettings.swift */,
4E699BB52CB33F4B007CBD5D /* CustomizeViewsSettings */,
E175AFF2299AC117004DCF52 /* DebugSettingsView.swift */,
E1E5D54B2783E27200692DFE /* ExperimentalSettingsView.swift */,
E16AF11B292C98A7001422A8 /* GestureSettingsView.swift */,
@ -3779,7 +3833,7 @@
isa = PBXGroup;
children = (
4E9A24E32C82B4700023DA83 /* CustomDeviceProfileSettingsView */,
E1CEFBF627914E6400F60429 /* CustomizeViewsSettings.swift */,
4E699BBC2CB34740007CBD5D /* CustomizeViewsSettings */,
E1E5D5502783E67700692DFE /* ExperimentalSettingsView.swift */,
E104C872296E0D0A00C1C3F9 /* IndicatorSettingsView.swift */,
4E2AC4D52C6C4CDC00DD600D /* PlaybackQualitySettingsView.swift */,
@ -4233,6 +4287,7 @@
E102314B2BCF8A6D009D71FC /* ProgramsViewModel.swift in Sources */,
E1DD55382B6EE533007501C0 /* Task.swift in Sources */,
E1575EA1293E7B1E001665B1 /* String.swift in Sources */,
4E699BC02CB3477D007CBD5D /* HomeSection.swift in Sources */,
E1E6C45429B1304E0064123F /* ChaptersActionButton.swift in Sources */,
E1763A292BF3046A004DF6AB /* AddUserButton.swift in Sources */,
E1E6C44229AECCD50064123F /* ActionButtons.swift in Sources */,
@ -4611,6 +4666,7 @@
E102314A2BCF8A6D009D71FC /* ProgramsViewModel.swift in Sources */,
E1721FAA28FB7CAC00762992 /* CompactTimeStamp.swift in Sources */,
E1803EA12BFBD6CF0039F90E /* Hashable.swift in Sources */,
4E699BB92CB33FC2007CBD5D /* HomeSection.swift in Sources */,
62C29E9F26D1016600C1D2E7 /* iOSMainCoordinator.swift in Sources */,
E12CC1B128D1008F00678D5D /* NextUpView.swift in Sources */,
E11895AF2893840F0042947B /* NavigationBarOffsetView.swift in Sources */,

View File

@ -11,8 +11,8 @@ import SwiftUI
struct ChevronButton: View {
private let isExternal: Bool
private let title: String
private let subtitle: String?
private let title: Text
private let subtitle: Text?
private var leadingView: () -> any View
private var onSelect: () -> Void
@ -25,13 +25,13 @@ struct ChevronButton: View {
leadingView()
.eraseToAnyView()
Text(title)
title
.foregroundColor(.primary)
Spacer()
if let subtitle {
Text(subtitle)
subtitle
.foregroundColor(.secondary)
}
@ -45,11 +45,31 @@ struct ChevronButton: View {
extension ChevronButton {
init(_ title: String, subtitle: String? = nil, external: Bool = false) {
init(
_ title: String,
subtitle: String? = nil,
external: Bool = false
) {
self.init(
isExternal: external,
title: title,
subtitle: subtitle,
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: {}
)

View File

@ -18,7 +18,7 @@ struct HomeView: View {
@Default(.Customization.nextUpPosterType)
private var nextUpPosterType
@Default(.Customization.showRecentlyAdded)
@Default(.Customization.Home.showRecentlyAdded)
private var showRecentlyAdded
@Default(.Customization.recentlyAddedPosterType)
private var recentlyAddedPosterType

View File

@ -0,0 +1,63 @@
//
// 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 Defaults
import SwiftUI
extension CustomizeViewsSettings {
struct HomeSection: View {
@Default(.Customization.Home.showRecentlyAdded)
private var showRecentlyAdded
@Default(.Customization.Home.maxNextUp)
private var maxNextUp
@Default(.Customization.Home.resumeNextUp)
private var resumeNextUp
@State
private var isPresentingNextUpDays = false
var body: some View {
Section(L10n.home) {
Toggle(L10n.showRecentlyAdded, isOn: $showRecentlyAdded)
Toggle(L10n.nextUpRewatch, isOn: $resumeNextUp)
ChevronButton(
L10n.nextUpDays,
subtitle: {
if maxNextUp > 0 {
return Text(
Date.now.addingTimeInterval(-maxNextUp) ..< Date.now,
format: .components(style: .narrow, fields: [.year, .month, .week, .day])
)
} else {
return Text(L10n.disabled)
}
}()
)
.onSelect {
isPresentingNextUpDays = true
}
.alert(L10n.nextUpDays, isPresented: $isPresentingNextUpDays) {
TextField(
L10n.nextUpDays,
value: $maxNextUp,
format: .dayInterval(range: 0 ... 1000)
)
.keyboardType(.numberPad)
} message: {
L10n.nextUpDaysDescription.text
}
}
}
}
}

View File

@ -37,8 +37,6 @@ struct CustomizeViewsSettings: View {
@Default(.Customization.nextUpPosterType)
private var nextUpPosterType
@Default(.Customization.recentlyAddedPosterType)
private var recentlyAddedPosterType
@Default(.Customization.showRecentlyAdded)
private var showRecentlyAdded
@Default(.Customization.latestInLibraryPosterType)
private var latestInLibraryPosterType
@ -138,8 +136,6 @@ struct CustomizeViewsSettings: View {
CaseIterablePicker(L10n.next, selection: $nextUpPosterType)
CaseIterablePicker(L10n.recentlyAdded, selection: $recentlyAddedPosterType)
CaseIterablePicker(L10n.latestWithString(L10n.library), selection: $latestInLibraryPosterType)
CaseIterablePicker(L10n.recommended, selection: $similarPosterType)
@ -162,9 +158,7 @@ struct CustomizeViewsSettings: View {
}
}
Section("Home") {
Toggle("Show recently added", isOn: $showRecentlyAdded)
}
HomeSection()
Section {
Toggle("Remember layout", isOn: $rememberLibraryLayout)