mirror of
https://github.com/yattee/yattee.git
synced 2026-02-19 17:29:45 +00:00
325 lines
8.8 KiB
Swift
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
|