diff --git a/Yattee/Core/AppEnvironment.swift b/Yattee/Core/AppEnvironment.swift index ad56fcfa..359dd94d 100644 --- a/Yattee/Core/AppEnvironment.swift +++ b/Yattee/Core/AppEnvironment.swift @@ -56,6 +56,13 @@ final class AppEnvironment { let legacyMigrationService: LegacyDataMigrationService let sourcesSettings: SourcesSettings + /// Center-section settings of the active player controls preset, cached for + /// synchronous access. Menu bar commands read seek durations from here since + /// they cannot await the layout service actor. + private(set) var activeControlsCenterSettings: CenterSectionSettings = .default + + @ObservationIgnored private var controlsSettingsObservers: [NSObjectProtocol] = [] + // MARK: - Shared Instance /// The single, process-wide app environment. @@ -311,6 +318,18 @@ final class AppEnvironment { // Wire up player controls layout service to player service (for preset-based settings) player.setPlayerControlsLayoutService(layoutService) + // Cache active preset's center settings and keep them in sync + Task { await self.refreshActiveControlsSettings() } + for name: Notification.Name in [.playerControlsActivePresetDidChange, .playerControlsPresetsDidChange] { + controlsSettingsObservers.append( + NotificationCenter.default.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in + Task { @MainActor in + await self?.refreshActiveControlsSettings() + } + } + ) + } + // Set up circular dependencies after all properties are initialized bgRefreshManager.setAppEnvironment(self) @@ -350,6 +369,12 @@ final class AppEnvironment { // MARK: - Configuration + /// Refreshes the cached center-section settings from the active player controls preset. + func refreshActiveControlsSettings() async { + let layout = await playerControlsLayoutService.activeLayout() + activeControlsCenterSettings = layout.centerSettings + } + /// Updates the HTTP client's User-Agent configuration from current settings. /// Call this after changing User-Agent related settings. func updateUserAgent() { diff --git a/Yattee/Core/PlaybackCommands.swift b/Yattee/Core/PlaybackCommands.swift index a2fdf2b9..e8fa86b9 100644 --- a/Yattee/Core/PlaybackCommands.swift +++ b/Yattee/Core/PlaybackCommands.swift @@ -50,10 +50,10 @@ struct PlaybackCommands: Commands { Divider() // Seeking - seekBackward10Button - seekForward10Button - seekBackward30Button - seekForward30Button + seekBackwardButton + seekForwardButton + secondarySeekBackwardButton + secondarySeekForwardButton Divider() @@ -121,41 +121,59 @@ struct PlaybackCommands: Commands { // MARK: - Seeking - private var seekBackward10Button: some View { + /// Seek durations follow the active player controls preset, matching the + /// in-player arrow key shortcuts. + private var seekBackwardSeconds: Int { + appEnvironment.activeControlsCenterSettings.seekBackwardSeconds + } + + private var seekForwardSeconds: Int { + appEnvironment.activeControlsCenterSettings.seekForwardSeconds + } + + private var seekBackwardButton: some View { Button { - playerService.seekBackward(by: 10) + playerService.seekBackward(by: TimeInterval(seekBackwardSeconds)) } label: { - Text(String(localized: "menu.playback.seekBackward10")) + Text(String(localized: "menu.playback.seekBackward \(seekBackwardSeconds)")) } .keyboardShortcut(.leftArrow, modifiers: [.command]) .disabled(!hasActiveVideo) } - private var seekForward10Button: some View { + private var seekForwardButton: some View { Button { - playerService.seekForward(by: 10) + playerService.seekForward(by: TimeInterval(seekForwardSeconds)) } label: { - Text(String(localized: "menu.playback.seekForward10")) + Text(String(localized: "menu.playback.seekForward \(seekForwardSeconds)")) } .keyboardShortcut(.rightArrow, modifiers: [.command]) .disabled(!hasActiveVideo) } - private var seekBackward30Button: some View { + private var secondarySeekBackwardSeconds: Int { + appEnvironment.activeControlsCenterSettings.secondarySeekBackwardSeconds + } + + private var secondarySeekForwardSeconds: Int { + appEnvironment.activeControlsCenterSettings.secondarySeekForwardSeconds + } + + private var secondarySeekBackwardButton: some View { Button { - playerService.seekBackward(by: 30) + playerService.seekBackward(by: TimeInterval(secondarySeekBackwardSeconds)) } label: { - Text(String(localized: "menu.playback.seekBackward30")) + Text(String(localized: "menu.playback.seekBackward \(secondarySeekBackwardSeconds)")) } .keyboardShortcut(.leftArrow, modifiers: [.command, .shift]) .disabled(!hasActiveVideo) } - private var seekForward30Button: some View { + private var secondarySeekForwardButton: some View { Button { - playerService.seekForward(by: 30) + playerService.seekForward(by: TimeInterval(secondarySeekForwardSeconds)) } label: { - Text(String(localized: "menu.playback.seekForward30")) + Text(String(localized: "menu.playback.seekForward \(secondarySeekForwardSeconds)")) } .keyboardShortcut(.rightArrow, modifiers: [.command, .shift]) .disabled(!hasActiveVideo) diff --git a/Yattee/Localizable.xcstrings b/Yattee/Localizable.xcstrings index ca607170..6b68ae7a 100644 --- a/Yattee/Localizable.xcstrings +++ b/Yattee/Localizable.xcstrings @@ -5741,42 +5741,22 @@ } } }, - "menu.playback.seekBackward10" : { + "menu.playback.seekBackward %lld" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Seek Back 10s" + "value" : "Seek Back %llds" } } } }, - "menu.playback.seekBackward30" : { + "menu.playback.seekForward %lld" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Seek Back 30s" - } - } - } - }, - "menu.playback.seekForward10" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seek Forward 10s" - } - } - } - }, - "menu.playback.seekForward30" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Seek Forward 30s" + "value" : "Seek Forward %llds" } } } @@ -13260,6 +13240,28 @@ } } }, + "settings.playerControls.center.secondarySeekBackwardTime" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Secondary Seek Time" + } + } + } + }, + "settings.playerControls.center.secondarySeekForwardTime" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Secondary Seek Time" + } + } + } + }, "settings.playerControls.center.seekBackward" : { "localizations" : { "en" : { @@ -13286,7 +13288,7 @@ "en" : { "stringUnit" : { "state" : "new", - "value" : "Seek durations are used by the ← and → keyboard shortcuts and by seek buttons added to the player." + "value" : "Seek durations are used by the ← and → keyboard shortcuts and by seek buttons added to the player. Hold ⇧ Shift with ← or → to seek by the secondary durations." } } } diff --git a/Yattee/Models/PlayerControls/BuiltInPresets.swift b/Yattee/Models/PlayerControls/BuiltInPresets.swift index 8deac6f4..283e4caa 100644 --- a/Yattee/Models/PlayerControls/BuiltInPresets.swift +++ b/Yattee/Models/PlayerControls/BuiltInPresets.swift @@ -11,7 +11,7 @@ extension LayoutPreset { /// Bump this version whenever any built-in preset definition changes. /// On launch, the app compares this against the last-applied version /// and replaces stale built-in presets with fresh copies from code. - static let builtInPresetsVersion = 7 + static let builtInPresetsVersion = 8 // MARK: - Built-in Preset IDs @@ -164,13 +164,13 @@ extension LayoutPreset { ] // On macOS center settings only drive seek amounts (keyboard arrows - // and default seek buttons); 5s preserves the historical arrow-key step. + // and default seek buttons). let centerSettings = CenterSectionSettings( showPlayPause: true, showSeekBackward: true, showSeekForward: true, - seekBackwardSeconds: 5, - seekForwardSeconds: 5, + seekBackwardSeconds: 10, + seekForwardSeconds: 10, leftSlider: .disabled, rightSlider: .disabled ) @@ -308,8 +308,8 @@ extension LayoutPreset { showPlayPause: true, showSeekBackward: true, showSeekForward: true, - seekBackwardSeconds: 5, - seekForwardSeconds: 5, + seekBackwardSeconds: 10, + seekForwardSeconds: 10, leftSlider: .disabled, rightSlider: .disabled ) diff --git a/Yattee/Models/PlayerControls/CenterSectionSettings.swift b/Yattee/Models/PlayerControls/CenterSectionSettings.swift index 29b7aee7..c079a76e 100644 --- a/Yattee/Models/PlayerControls/CenterSectionSettings.swift +++ b/Yattee/Models/PlayerControls/CenterSectionSettings.swift @@ -25,6 +25,12 @@ struct CenterSectionSettings: Codable, Hashable, Sendable { /// Number of seconds for the seek forward button. var seekForwardSeconds: Int + /// Number of seconds for the secondary seek backward shortcut (⇧ + seek). + var secondarySeekBackwardSeconds: Int + + /// Number of seconds for the secondary seek forward shortcut (⇧ + seek). + var secondarySeekForwardSeconds: Int + /// Type of slider to show on the left edge of the player (iOS only). var leftSlider: SideSliderType @@ -40,6 +46,8 @@ struct CenterSectionSettings: Codable, Hashable, Sendable { /// - showSeekForward: Show seek forward button. Defaults to true. /// - seekBackwardSeconds: Seconds to seek backward. Defaults to 10. /// - seekForwardSeconds: Seconds to seek forward. Defaults to 10. + /// - secondarySeekBackwardSeconds: Seconds for the secondary (⇧) backward seek. Defaults to 30. + /// - secondarySeekForwardSeconds: Seconds for the secondary (⇧) forward seek. Defaults to 30. /// - leftSlider: Type of slider on left edge. Defaults to disabled. /// - rightSlider: Type of slider on right edge. Defaults to disabled. init( @@ -48,6 +56,8 @@ struct CenterSectionSettings: Codable, Hashable, Sendable { showSeekForward: Bool = true, seekBackwardSeconds: Int = 10, seekForwardSeconds: Int = 10, + secondarySeekBackwardSeconds: Int = 30, + secondarySeekForwardSeconds: Int = 30, leftSlider: SideSliderType = .disabled, rightSlider: SideSliderType = .disabled ) { @@ -56,6 +66,8 @@ struct CenterSectionSettings: Codable, Hashable, Sendable { self.showSeekForward = showSeekForward self.seekBackwardSeconds = max(1, seekBackwardSeconds) self.seekForwardSeconds = max(1, seekForwardSeconds) + self.secondarySeekBackwardSeconds = max(1, secondarySeekBackwardSeconds) + self.secondarySeekForwardSeconds = max(1, secondarySeekForwardSeconds) self.leftSlider = leftSlider self.rightSlider = rightSlider } @@ -68,6 +80,8 @@ struct CenterSectionSettings: Codable, Hashable, Sendable { case showSeekForward case seekBackwardSeconds case seekForwardSeconds + case secondarySeekBackwardSeconds + case secondarySeekForwardSeconds case leftSlider case rightSlider } @@ -80,6 +94,8 @@ struct CenterSectionSettings: Codable, Hashable, Sendable { seekBackwardSeconds = try container.decode(Int.self, forKey: .seekBackwardSeconds) seekForwardSeconds = try container.decode(Int.self, forKey: .seekForwardSeconds) // New properties with defaults for backward compatibility + secondarySeekBackwardSeconds = try container.decodeIfPresent(Int.self, forKey: .secondarySeekBackwardSeconds) ?? 30 + secondarySeekForwardSeconds = try container.decodeIfPresent(Int.self, forKey: .secondarySeekForwardSeconds) ?? 30 leftSlider = try container.decodeIfPresent(SideSliderType.self, forKey: .leftSlider) ?? .disabled rightSlider = try container.decodeIfPresent(SideSliderType.self, forKey: .rightSlider) ?? .disabled } diff --git a/Yattee/Views/Player/macOS/MacOSPlayerControlsView.swift b/Yattee/Views/Player/macOS/MacOSPlayerControlsView.swift index 418265b5..5faebef0 100644 --- a/Yattee/Views/Player/macOS/MacOSPlayerControlsView.swift +++ b/Yattee/Views/Player/macOS/MacOSPlayerControlsView.swift @@ -325,6 +325,12 @@ struct MacOSPlayerControlsView: View { } } + // Let ⌘-based key equivalents reach the menu bar (Playback menu shortcuts + // like ⌘←/⌘⇧← seek and ⌘⌥← previous video) instead of consuming them here. + if event.modifierFlags.contains(.command) { return event } + + let isShiftHeld = event.modifierFlags.contains(.shift) + // Handle keyboard shortcuts switch event.keyCode { case 49: // Space @@ -332,12 +338,20 @@ struct MacOSPlayerControlsView: View { return nil // Consume event case 123: // Left arrow - let seconds = TimeInterval(layout?.centerSettings.seekBackwardSeconds ?? 5) + let seconds = TimeInterval( + isShiftHeld + ? layout?.centerSettings.secondarySeekBackwardSeconds ?? 30 + : layout?.centerSettings.seekBackwardSeconds ?? 10 + ) Task { await onSeekBackward(seconds) } return nil case 124: // Right arrow - let seconds = TimeInterval(layout?.centerSettings.seekForwardSeconds ?? 5) + let seconds = TimeInterval( + isShiftHeld + ? layout?.centerSettings.secondarySeekForwardSeconds ?? 30 + : layout?.centerSettings.seekForwardSeconds ?? 10 + ) Task { await onSeekForward(seconds) } return nil diff --git a/Yattee/Views/Settings/PlayerControls/CenterControlsSettingsView.swift b/Yattee/Views/Settings/PlayerControls/CenterControlsSettingsView.swift index 38919600..7eff93a0 100644 --- a/Yattee/Views/Settings/PlayerControls/CenterControlsSettingsView.swift +++ b/Yattee/Views/Settings/PlayerControls/CenterControlsSettingsView.swift @@ -17,6 +17,8 @@ struct CenterControlsSettingsView: View { @State private var showSeekForward: Bool = true @State private var seekBackwardSeconds: Double = 10 @State private var seekForwardSeconds: Double = 10 + @State private var secondarySeekBackwardSeconds: Double = 30 + @State private var secondarySeekForwardSeconds: Double = 30 @State private var leftSlider: SideSliderType = .disabled @State private var rightSlider: SideSliderType = .disabled @@ -113,6 +115,39 @@ struct CenterControlsSettingsView: View { .tint(Int(seekBackwardSeconds) == seconds ? .accentColor : .secondary) } } + + #if os(macOS) + HStack { + Text(String( + localized: "settings.playerControls.center.secondarySeekBackwardTime", + defaultValue: "Secondary Seek Time" + )) + Spacer() + Text("\(Int(secondarySeekBackwardSeconds))s") + .foregroundStyle(.secondary) + } + + Slider( + value: $secondarySeekBackwardSeconds, + in: 1...90, + step: 1 + ) + .onChange(of: secondarySeekBackwardSeconds) { _, newValue in + viewModel.updateCenterSettingsSync { $0.secondarySeekBackwardSeconds = Int(newValue) } + } + + // Quick presets + HStack(spacing: 8) { + ForEach([15, 30, 45, 60, 90], id: \.self) { seconds in + Button("\(seconds)s") { + secondarySeekBackwardSeconds = Double(seconds) + } + .buttonStyle(.bordered) + .controlSize(.small) + .tint(Int(secondarySeekBackwardSeconds) == seconds ? .accentColor : .secondary) + } + } + #endif } } header: { Text(String(localized: "settings.playerControls.center.seekBackward")) @@ -162,6 +197,39 @@ struct CenterControlsSettingsView: View { .tint(Int(seekForwardSeconds) == seconds ? .accentColor : .secondary) } } + + #if os(macOS) + HStack { + Text(String( + localized: "settings.playerControls.center.secondarySeekForwardTime", + defaultValue: "Secondary Seek Time" + )) + Spacer() + Text("\(Int(secondarySeekForwardSeconds))s") + .foregroundStyle(.secondary) + } + + Slider( + value: $secondarySeekForwardSeconds, + in: 1...90, + step: 1 + ) + .onChange(of: secondarySeekForwardSeconds) { _, newValue in + viewModel.updateCenterSettingsSync { $0.secondarySeekForwardSeconds = Int(newValue) } + } + + // Quick presets + HStack(spacing: 8) { + ForEach([15, 30, 45, 60, 90], id: \.self) { seconds in + Button("\(seconds)s") { + secondarySeekForwardSeconds = Double(seconds) + } + .buttonStyle(.bordered) + .controlSize(.small) + .tint(Int(secondarySeekForwardSeconds) == seconds ? .accentColor : .secondary) + } + } + #endif } } header: { Text(String(localized: "settings.playerControls.center.seekForward")) @@ -169,7 +237,7 @@ struct CenterControlsSettingsView: View { #if os(macOS) Text(String( localized: "settings.playerControls.center.seekDurationsFooter", - defaultValue: "Seek durations are used by the ← and → keyboard shortcuts and by seek buttons added to the player." + defaultValue: "Seek durations are used by the ← and → keyboard shortcuts and by seek buttons added to the player. Hold ⇧ Shift with ← or → to seek by the secondary durations." )) #endif } @@ -231,6 +299,8 @@ struct CenterControlsSettingsView: View { showSeekForward: showSeekForward, seekBackwardSeconds: Int(seekBackwardSeconds), seekForwardSeconds: Int(seekForwardSeconds), + secondarySeekBackwardSeconds: Int(secondarySeekBackwardSeconds), + secondarySeekForwardSeconds: Int(secondarySeekForwardSeconds), leftSlider: leftSlider, rightSlider: rightSlider ) @@ -246,6 +316,8 @@ struct CenterControlsSettingsView: View { showSeekForward = settings.showSeekForward seekBackwardSeconds = Double(settings.seekBackwardSeconds) seekForwardSeconds = Double(settings.seekForwardSeconds) + secondarySeekBackwardSeconds = Double(settings.secondarySeekBackwardSeconds) + secondarySeekForwardSeconds = Double(settings.secondarySeekForwardSeconds) leftSlider = settings.leftSlider rightSlider = settings.rightSlider }