diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index b54bf0dc..38a8f113 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -162,6 +162,7 @@ final class PlayerModel: ObservableObject { @Default(.closePiPOnOpeningPlayer) var closePiPOnOpeningPlayer @Default(.resetWatchedStatusOnPlaying) var resetWatchedStatusOnPlaying @Default(.playerRate) var playerRate + @Default(.systemControlsSeekDuration) var systemControlsSeekDuration #if !os(macOS) @Default(.closePiPAndOpenPlayerOnEnteringForeground) var closePiPAndOpenPlayerOnEnteringForeground @@ -746,18 +747,19 @@ final class PlayerModel: ObservableObject { UIApplication.shared.beginReceivingRemoteControlEvents() #endif - let preferredIntervals = [NSNumber(10)] + let interval = TimeInterval(systemControlsSeekDuration) ?? 10 + let preferredIntervals = [NSNumber(value: interval)] skipForwardCommand.preferredIntervals = preferredIntervals skipBackwardCommand.preferredIntervals = preferredIntervals skipForwardCommand.addTarget { [weak self] _ in - self?.backend.seek(relative: .secondsInDefaultTimescale(10), seekType: .userInteracted) + self?.backend.seek(relative: .secondsInDefaultTimescale(interval), seekType: .userInteracted) return .success } skipBackwardCommand.addTarget { [weak self] _ in - self?.backend.seek(relative: .secondsInDefaultTimescale(-10), seekType: .userInteracted) + self?.backend.seek(relative: .secondsInDefaultTimescale(-interval), seekType: .userInteracted) return .success } diff --git a/Shared/Constants.swift b/Shared/Constants.swift index a85aee9f..0e7ce2d4 100644 --- a/Shared/Constants.swift +++ b/Shared/Constants.swift @@ -1,3 +1,4 @@ +import Defaults import Foundation import SwiftUI @@ -51,4 +52,25 @@ struct Constants { return "list.and.film" } } + + static func seekIcon(_ type: String, _ interval: TimeInterval) -> String { + let interval = Int(interval) + let allVersions = [10, 15, 30, 45, 60, 75, 90] + let iOS15 = [5] + let iconName = "go\(type).\(interval)" + + if #available(iOS 15, macOS 12, *) { + if iOS15.contains(interval) { + return iconName + } + } + + if allVersions.contains(interval) { + return iconName + } + + let sign = type == "forward" ? "plus" : "minus" + + return "go\(type).\(sign)" + } } diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index e340ce68..4d341b8a 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -188,6 +188,12 @@ extension Defaults.Keys { static let playerActionsButtonLabelStyle = Key("playerActionsButtonLabelStyle", default: .iconAndText) static let systemControlsCommands = Key("systemControlsCommands", default: .restartAndAdvanceToNext) + + static let buttonBackwardSeekDuration = Key("buttonBackwardSeekDuration", default: "10") + static let buttonForwardSeekDuration = Key("buttonForwardSeekDuration", default: "10") + static let gestureBackwardSeekDuration = Key("gestureBackwardSeekDuration", default: "10") + static let gestureForwardSeekDuration = Key("gestureForwardSeekDuration", default: "10") + static let systemControlsSeekDuration = Key("systemControlsBackwardSeekDuration", default: "10") static let actionButtonShareEnabled = Key("actionButtonShareEnabled", default: true) static let actionButtonAddToPlaylistEnabled = Key("actionButtonAddToPlaylistEnabled", default: true) static let actionButtonSubscribeEnabled = Key("actionButtonSubscribeEnabled", default: false) diff --git a/Shared/Player/Controls/PlayerControls.swift b/Shared/Player/Controls/PlayerControls.swift index 3bda8576..ad8543d2 100644 --- a/Shared/Player/Controls/PlayerControls.swift +++ b/Shared/Player/Controls/PlayerControls.swift @@ -29,6 +29,8 @@ struct PlayerControls: View { @Default(.playerControlsLayout) private var regularPlayerControlsLayout @Default(.fullScreenPlayerControlsLayout) private var fullScreenPlayerControlsLayout @Default(.openWatchNextOnClose) private var openWatchNextOnClose + @Default(.buttonBackwardSeekDuration) private var buttonBackwardSeekDuration + @Default(.buttonForwardSeekDuration) private var buttonForwardSeekDuration private let controlsOverlayModel = ControlOverlaysModel.shared @@ -398,8 +400,14 @@ struct PlayerControls: View { size = playerControlsLayout.bigButtonSize #endif - return button("Seek Backward", systemImage: "gobackward.10", fontSize: fontSize, size: size, cornerRadius: 5, background: false, foregroundColor: foregroundColor) { - player.backend.seek(relative: .secondsInDefaultTimescale(-10), seekType: .userInteracted) + let interval = TimeInterval(buttonBackwardSeekDuration) ?? 10 + + return button( + "Seek Backward", + systemImage: Constants.seekIcon("backward", interval), + fontSize: fontSize, size: size, cornerRadius: 5, background: false, foregroundColor: foregroundColor + ) { + player.backend.seek(relative: .secondsInDefaultTimescale(-interval), seekType: .userInteracted) } .disabled(player.liveStreamInAVPlayer) #if os(tvOS) @@ -420,8 +428,14 @@ struct PlayerControls: View { size = playerControlsLayout.bigButtonSize #endif - return button("Seek Forward", systemImage: "goforward.10", fontSize: fontSize, size: size, cornerRadius: 5, background: false, foregroundColor: foregroundColor) { - player.backend.seek(relative: .secondsInDefaultTimescale(10), seekType: .userInteracted) + let interval = TimeInterval(buttonForwardSeekDuration) ?? 10 + + return button( + "Seek Forward", + systemImage: Constants.seekIcon("forward", interval), + fontSize: fontSize, size: size, cornerRadius: 5, background: false, foregroundColor: foregroundColor + ) { + player.backend.seek(relative: .secondsInDefaultTimescale(interval), seekType: .userInteracted) } .disabled(player.liveStreamInAVPlayer) #if os(tvOS) diff --git a/Shared/Player/PlayerGestures.swift b/Shared/Player/PlayerGestures.swift index 6a3d73c9..f194bb48 100644 --- a/Shared/Player/PlayerGestures.swift +++ b/Shared/Player/PlayerGestures.swift @@ -1,9 +1,13 @@ +import Defaults import SwiftUI struct PlayerGestures: View { private var player = PlayerModel.shared @ObservedObject private var model = PlayerControlsModel.shared + @Default(.gestureBackwardSeekDuration) private var gestureBackwardSeekDuration + @Default(.gestureForwardSeekDuration) private var gestureForwardSeekDuration + var body: some View { HStack(spacing: 0) { gestureRectangle @@ -11,7 +15,8 @@ struct PlayerGestures: View { tapSensitivity: 0.2, singleTapAction: { singleTapAction() }, doubleTapAction: { - player.backend.seek(relative: .secondsInDefaultTimescale(-10), seekType: .userInteracted) + let interval = TimeInterval(gestureBackwardSeekDuration) ?? 10 + player.backend.seek(relative: .secondsInDefaultTimescale(-interval), seekType: .userInteracted) }, anyTapAction: { model.update() @@ -32,7 +37,8 @@ struct PlayerGestures: View { tapSensitivity: 0.2, singleTapAction: { singleTapAction() }, doubleTapAction: { - player.backend.seek(relative: .secondsInDefaultTimescale(10), seekType: .userInteracted) + let interval = TimeInterval(gestureForwardSeekDuration) ?? 10 + player.backend.seek(relative: .secondsInDefaultTimescale(interval), seekType: .userInteracted) } ) } diff --git a/Shared/Player/VideoPlayerView.swift b/Shared/Player/VideoPlayerView.swift index 8b1eb631..05cd6493 100644 --- a/Shared/Player/VideoPlayerView.swift +++ b/Shared/Player/VideoPlayerView.swift @@ -66,6 +66,8 @@ struct VideoPlayerView: View { @Default(.seekGestureSpeed) var seekGestureSpeed @Default(.seekGestureSensitivity) var seekGestureSensitivity @Default(.playerSidebar) var playerSidebar + @Default(.gestureBackwardSeekDuration) private var gestureBackwardSeekDuration + @Default(.gestureForwardSeekDuration) private var gestureForwardSeekDuration @ObservedObject internal var controlsOverlayModel = ControlOverlaysModel.shared @@ -371,10 +373,12 @@ struct VideoPlayerView: View { guard !player.controls.presentingControls else { return } if direction == .left { - player.backend.seek(relative: .secondsInDefaultTimescale(-10), seekType: .userInteracted) + let interval = TimeInterval(gestureBackwardSeekDuration) ?? 10 + player.backend.seek(relative: .secondsInDefaultTimescale(-interval), seekType: .userInteracted) } if direction == .right { - player.backend.seek(relative: .secondsInDefaultTimescale(10), seekType: .userInteracted) + let interval = TimeInterval(gestureForwardSeekDuration) ?? 10 + player.backend.seek(relative: .secondsInDefaultTimescale(interval), seekType: .userInteracted) } } .onPlayPauseCommand { diff --git a/Shared/Settings/PlayerSettings.swift b/Shared/Settings/PlayerSettings.swift index 55e965dc..bdf273c0 100644 --- a/Shared/Settings/PlayerSettings.swift +++ b/Shared/Settings/PlayerSettings.swift @@ -33,6 +33,12 @@ struct PlayerSettings: View { @Default(.openWatchNextOnFinishedWatching) private var openWatchNextOnFinishedWatching @Default(.openWatchNextOnFinishedWatchingDelay) private var openWatchNextOnFinishedWatchingDelay + @Default(.buttonBackwardSeekDuration) private var buttonBackwardSeekDuration + @Default(.buttonForwardSeekDuration) private var buttonForwardSeekDuration + @Default(.gestureBackwardSeekDuration) private var gestureBackwardSeekDuration + @Default(.gestureForwardSeekDuration) private var gestureForwardSeekDuration + @Default(.systemControlsSeekDuration) private var systemControlsSeekDuration + @Default(.actionButtonShareEnabled) private var actionButtonShareEnabled @Default(.actionButtonSubscribeEnabled) private var actionButtonSubscribeEnabled @Default(.actionButtonNextEnabled) private var actionButtonNextEnabled @@ -41,6 +47,7 @@ struct PlayerSettings: View { @Default(.actionButtonSettingsEnabled) private var actionButtonSettingsEnabled @Default(.actionButtonHideEnabled) private var actionButtonHideEnabled @Default(.actionButtonNextQueueCountEnabled) private var actionButtonNextQueueCountEnabled + @ObservedObject private var accounts = AccountsModel.shared private var player = PlayerModel.shared @@ -83,6 +90,10 @@ struct PlayerSettings: View { systemControlsCommandsPicker } + Section(header: SettingsHeader(text: "Seeking"), footer: seekingGestureSection) { + seekingSection + } + #if !os(tvOS) Section(header: SettingsHeader(text: "Actions Buttons")) { actionButtonToggles @@ -225,6 +236,43 @@ struct PlayerSettings: View { .multilineTextAlignment(.trailing) } + @ViewBuilder private var seekingSection: some View { + seekingDurationSetting("System controls", $systemControlsSeekDuration) + .foregroundColor(systemControlsCommands == .restartAndAdvanceToNext ? .secondary : .primary) + .disabled(systemControlsCommands == .restartAndAdvanceToNext) + seekingDurationSetting("Controls button: backwards", $buttonBackwardSeekDuration) + seekingDurationSetting("Controls button: forwards", $buttonForwardSeekDuration) + seekingDurationSetting("Gesture: backwards", $gestureBackwardSeekDuration) + seekingDurationSetting("Gesture: fowards", $gestureForwardSeekDuration) + } + + private var seekingGestureSection: some View { + #if os(iOS) + Text("Gesture settings control skipping interval for double tap gesture on left/right side of the player. Changing system controls settings requires restart.") + #elseif os(macOS) + Text("Gesture settings control skipping interval for double click on left/right side of the player. Changing system controls settings requires restart.") + #else + Text("Gesture settings control skipping interval for remote arrow buttons (for 2nd generation Siri Remote or newer). Changing system controls settings requires restart.") + #endif + } + + private func seekingDurationSetting(_ name: String, _ value: Binding) -> some View { + HStack { + Text(name) + .frame(minWidth: 140, alignment: .leading) + Spacer() + TextField("Duration", text: value) + + .frame(maxWidth: 50, alignment: .trailing) + .multilineTextAlignment(.trailing) + + .labelsHidden() + #if !os(macOS) + .keyboardType(.numberPad) + #endif + } + } + @ViewBuilder private var actionButtonToggles: some View { actionButtonToggle("Share", $actionButtonShareEnabled) actionButtonToggle("Add to Playlist", $actionButtonAddToPlaylistEnabled)