Files
yattee/Yattee/Core/PlaybackCommands.swift
2026-02-08 18:33:56 +01:00

325 lines
8.8 KiB
Swift

//
// PlaybackCommands.swift
// Yattee
//
// Menu bar commands for playback control.
//
import SwiftUI
#if !os(tvOS)
/// Playback-related menu bar commands.
/// Works on both macOS and iPadOS 26+.
struct PlaybackCommands: Commands {
let appEnvironment: AppEnvironment
private var playerService: PlayerService {
appEnvironment.playerService
}
private var navigationCoordinator: NavigationCoordinator {
appEnvironment.navigationCoordinator
}
private var state: PlayerState {
playerService.state
}
private var settingsManager: SettingsManager {
appEnvironment.settingsManager
}
private var hasActiveVideo: Bool {
state.currentVideo != nil
}
private var isPlayerExpanded: Bool {
navigationCoordinator.isPlayerExpanded
}
var body: some Commands {
CommandMenu(String(localized: "menu.playback")) {
// Player visibility (existing)
playerToggleButton
Divider()
// Core playback
playPauseButton
Divider()
// Seeking
seekBackward10Button
seekForward10Button
seekBackward30Button
seekForward30Button
Divider()
// Navigation
previousVideoButton
nextVideoButton
Divider()
// Speed
slowerButton
fasterButton
resetSpeedButton
Divider()
// Volume
volumeUpButton
volumeDownButton
muteButton
Divider()
// Display modes
pipButton
Divider()
closeVideoButton
}
}
// MARK: - Player Visibility
private var playerToggleButton: some View {
Button {
togglePlayerExpanded()
} label: {
Text(isPlayerExpanded
? String(localized: "menu.playback.hidePlayer")
: String(localized: "menu.playback.showPlayer"))
}
.keyboardShortcut("p", modifiers: [.command, .shift])
.disabled(!hasActiveVideo)
}
private func togglePlayerExpanded() {
if isPlayerExpanded {
navigationCoordinator.isPlayerExpanded = false
} else {
navigationCoordinator.expandPlayer()
}
}
// MARK: - Core Playback
private var playPauseButton: some View {
Button {
playerService.togglePlayPause()
} label: {
Text(String(localized: "menu.playback.playPause"))
}
.keyboardShortcut("k", modifiers: [.command])
.disabled(!hasActiveVideo)
}
// MARK: - Seeking
private var seekBackward10Button: some View {
Button {
playerService.seekBackward(by: 10)
} label: {
Text(String(localized: "menu.playback.seekBackward10"))
}
.keyboardShortcut(.leftArrow, modifiers: [.command])
.disabled(!hasActiveVideo)
}
private var seekForward10Button: some View {
Button {
playerService.seekForward(by: 10)
} label: {
Text(String(localized: "menu.playback.seekForward10"))
}
.keyboardShortcut(.rightArrow, modifiers: [.command])
.disabled(!hasActiveVideo)
}
private var seekBackward30Button: some View {
Button {
playerService.seekBackward(by: 30)
} label: {
Text(String(localized: "menu.playback.seekBackward30"))
}
.keyboardShortcut(.leftArrow, modifiers: [.command, .shift])
.disabled(!hasActiveVideo)
}
private var seekForward30Button: some View {
Button {
playerService.seekForward(by: 30)
} label: {
Text(String(localized: "menu.playback.seekForward30"))
}
.keyboardShortcut(.rightArrow, modifiers: [.command, .shift])
.disabled(!hasActiveVideo)
}
// MARK: - Navigation
private var previousVideoButton: some View {
Button {
Task {
await playerService.playPrevious()
}
} label: {
Text(String(localized: "menu.playback.previousVideo"))
}
.keyboardShortcut(.leftArrow, modifiers: [.command, .option])
.disabled(!hasActiveVideo || !state.hasPrevious)
}
private var nextVideoButton: some View {
Button {
Task {
await playerService.playNext()
}
} label: {
Text(String(localized: "menu.playback.nextVideo"))
}
.keyboardShortcut(.rightArrow, modifiers: [.command, .option])
.disabled(!hasActiveVideo || !state.hasNext)
}
// MARK: - Speed
private var slowerButton: some View {
Button {
cycleSpeedDown()
} label: {
Text(String(localized: "menu.playback.slower"))
}
.keyboardShortcut("[", modifiers: [.command])
.disabled(!hasActiveVideo)
}
private var fasterButton: some View {
Button {
cycleSpeedUp()
} label: {
Text(String(localized: "menu.playback.faster"))
}
.keyboardShortcut("]", modifiers: [.command])
.disabled(!hasActiveVideo)
}
private var resetSpeedButton: some View {
Button {
state.rate = .x1
playerService.currentBackend?.rate = 1.0
} label: {
Text(String(localized: "menu.playback.resetSpeed"))
}
.keyboardShortcut("0", modifiers: [.command])
.disabled(!hasActiveVideo)
}
private func cycleSpeedDown() {
let rates = PlaybackRate.allCases
guard let currentIndex = rates.firstIndex(of: state.rate) else { return }
if currentIndex > 0 {
let newRate = rates[currentIndex - 1]
state.rate = newRate
playerService.currentBackend?.rate = Float(newRate.rawValue)
}
}
private func cycleSpeedUp() {
let rates = PlaybackRate.allCases
guard let currentIndex = rates.firstIndex(of: state.rate) else { return }
if currentIndex < rates.count - 1 {
let newRate = rates[currentIndex + 1]
state.rate = newRate
playerService.currentBackend?.rate = Float(newRate.rawValue)
}
}
// MARK: - Volume
private var volumeUpButton: some View {
Button {
let newVolume = min(1.0, state.volume + 0.1)
state.volume = newVolume
playerService.currentBackend?.volume = newVolume
} label: {
Text(String(localized: "menu.playback.volumeUp"))
}
.keyboardShortcut(.upArrow, modifiers: [.command])
.disabled(!hasActiveVideo)
}
private var volumeDownButton: some View {
Button {
let newVolume = max(0.0, state.volume - 0.1)
state.volume = newVolume
playerService.currentBackend?.volume = newVolume
} label: {
Text(String(localized: "menu.playback.volumeDown"))
}
.keyboardShortcut(.downArrow, modifiers: [.command])
.disabled(!hasActiveVideo)
}
private var muteButton: some View {
Button {
state.isMuted.toggle()
playerService.currentBackend?.isMuted = state.isMuted
} label: {
Text(String(localized: "menu.playback.mute"))
}
.keyboardShortcut("m", modifiers: [.command, .shift])
.disabled(!hasActiveVideo)
}
// MARK: - Display Modes
private var pipButton: some View {
Button {
if let mpvBackend = playerService.currentBackend as? MPVBackend {
mpvBackend.togglePiP()
}
} label: {
Text(String(localized: "menu.playback.pip"))
}
.keyboardShortcut("i", modifiers: [.command, .shift])
.disabled(!hasActiveVideo || !state.isPiPPossible)
}
// MARK: - Close video button
private var closeVideoButton: some View {
Button {
closeVideo()
} label: {
Text(String(localized: "menu.playback.closeVideo"))
}
.keyboardShortcut(".", modifiers: [.command])
.disabled(!hasActiveVideo)
}
private func closeVideo() {
// Mark as closing to hide tab accessory before dismissal
state.isClosingVideo = true
// Clear the queue when closing video
appEnvironment.queueManager.clearQueue()
// Reset panel state when closing player
settingsManager.landscapeDetailsPanelVisible = false
settingsManager.landscapeDetailsPanelPinned = false
// Stop player FIRST before dismissing window
playerService.stop()
// Then dismiss player window (after backend is stopped)
navigationCoordinator.isPlayerExpanded = false
}
}
#endif