mirror of
https://github.com/yattee/yattee.git
synced 2026-02-20 01:39:46 +00:00
Yattee v2 rewrite
This commit is contained in:
324
Yattee/Core/PlaybackCommands.swift
Normal file
324
Yattee/Core/PlaybackCommands.swift
Normal file
@@ -0,0 +1,324 @@
|
||||
//
|
||||
// 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
|
||||
Reference in New Issue
Block a user