mirror of
https://github.com/yattee/yattee.git
synced 2026-10-23 13:12:53 +00:00
iPhone landscape now uses the floating panel only, always on the right; the pin and side-toggle buttons on the grabber are gone and the pinned setting resolves to false on phone. iPad and macOS default to the pinned side-by-side layout, keep the pin button to switch to floating, and pick the panel side in the preset's panel settings instead of an on-screen arrow. The pinned state persists as a layout preference: closing the player or the hidden default visibility no longer resets it. Removes PanelAlignmentButton, the global floatingDetailsPanelSide setting and its key, FloatingPanelSide.opposite, and the legacy wideLayoutPanelAlignment preset field.
342 lines
9.7 KiB
Swift
342 lines
9.7 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
|
|
seekBackwardButton
|
|
seekForwardButton
|
|
secondarySeekBackwardButton
|
|
secondarySeekForwardButton
|
|
|
|
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
|
|
|
|
/// 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: TimeInterval(seekBackwardSeconds))
|
|
} label: {
|
|
Text(String(localized: "menu.playback.seekBackward \(seekBackwardSeconds)"))
|
|
}
|
|
.keyboardShortcut(.leftArrow, modifiers: [.command])
|
|
.disabled(!hasActiveVideo)
|
|
}
|
|
|
|
private var seekForwardButton: some View {
|
|
Button {
|
|
playerService.seekForward(by: TimeInterval(seekForwardSeconds))
|
|
} label: {
|
|
Text(String(localized: "menu.playback.seekForward \(seekForwardSeconds)"))
|
|
}
|
|
.keyboardShortcut(.rightArrow, modifiers: [.command])
|
|
.disabled(!hasActiveVideo)
|
|
}
|
|
|
|
private var secondarySeekBackwardSeconds: Int {
|
|
appEnvironment.activeControlsCenterSettings.secondarySeekBackwardSeconds
|
|
}
|
|
|
|
private var secondarySeekForwardSeconds: Int {
|
|
appEnvironment.activeControlsCenterSettings.secondarySeekForwardSeconds
|
|
}
|
|
|
|
private var secondarySeekBackwardButton: some View {
|
|
Button {
|
|
playerService.seekBackward(by: TimeInterval(secondarySeekBackwardSeconds))
|
|
} label: {
|
|
Text(String(localized: "menu.playback.seekBackward \(secondarySeekBackwardSeconds)"))
|
|
}
|
|
.keyboardShortcut(.leftArrow, modifiers: [.command, .shift])
|
|
.disabled(!hasActiveVideo)
|
|
}
|
|
|
|
private var secondarySeekForwardButton: some View {
|
|
Button {
|
|
playerService.seekForward(by: TimeInterval(secondarySeekForwardSeconds))
|
|
} label: {
|
|
Text(String(localized: "menu.playback.seekForward \(secondarySeekForwardSeconds)"))
|
|
}
|
|
.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 || state.currentStream?.isAudioOnly == true)
|
|
}
|
|
|
|
// 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 visibility when closing player
|
|
settingsManager.landscapeDetailsPanelVisible = false
|
|
|
|
// Stop player FIRST before dismissing window
|
|
playerService.stop()
|
|
|
|
// Then dismiss player window (after backend is stopped)
|
|
navigationCoordinator.isPlayerExpanded = false
|
|
}
|
|
}
|
|
#endif
|