Files
yattee/Yattee/Views/Player/PlayerControlsActions.swift
Arkadiusz Fal 05bbd449f8 Drive macOS player control bars from presets
Render the macOS control bar button row and top bar from the active
preset's sections via a new MacOSControlsSectionRenderer, replacing the
hardcoded QuickTime-style rows in MacOSControlBar. Add a keepOnTop
button type and macOS-specific button availability lists, and adapt the
player controls settings editors (preset editor, section editor, button
configuration) for macOS.

Also apply the preset font style to the control bar time labels, and
update the built-in macOS Default preset order (queue before transport,
fullscreen at the end), bumping builtInPresetsVersion to 7.
2026-05-31 16:55:39 +02:00

324 lines
9.1 KiB
Swift

//
// PlayerControlsActions.swift
// Yattee
//
// Consolidated actions and state for player control buttons.
//
import SwiftUI
#if os(iOS) || os(macOS)
/// Consolidated actions and state for player control buttons.
/// Used by `ControlsSectionRenderer` (iOS) and `MacOSControlsSectionRenderer`
/// (macOS) to render buttons dynamically.
@MainActor
struct PlayerControlsActions {
// MARK: - State
/// The current player state
let playerState: PlayerState
/// Whether the layout is widescreen (landscape fullscreen)
let isWideScreenLayout: Bool
/// Whether fullscreen mode is active
let isFullscreen: Bool
/// Whether the video is widescreen aspect ratio
let isWidescreenVideo: Bool
#if os(iOS)
/// Whether orientation is locked
let isOrientationLocked: Bool
#endif
/// Whether the side panel is visible
let isPanelVisible: Bool
#if os(iOS)
/// Whether the side panel is pinned (iPad landscape)
let isPanelPinned: Bool
#endif
/// Which side the panel is on
let panelSide: FloatingPanelSide
#if os(iOS)
/// Whether running on iPad
let isIPad: Bool
#endif
/// Whether volume controls should show (mpv mode)
let showVolumeControls: Bool
/// Whether debug button should show
let showDebugButton: Bool
/// Whether close button should show
let showCloseButton: Bool
/// The currently playing video (for share, playlist, context menu)
let currentVideo: Video?
/// Available captions for the current video
let availableCaptions: [Caption]
/// Currently selected caption
let currentCaption: Caption?
/// Available streams for the current video
let availableStreams: [Stream]
/// Current video stream
let currentStream: Stream?
/// Current audio stream
let currentAudioStream: Stream?
#if os(iOS)
/// Current panscan value (0.0 = fit, 1.0 = fill)
let panscanValue: Double
/// Whether panscan change is currently allowed
let isPanscanAllowed: Bool
#endif
/// Whether auto-play next is enabled
let isAutoPlayNextEnabled: Bool
/// Yattee Server URL for channel avatar fallback
let yatteeServerURL: URL?
/// DeArrow branding provider for title replacement
let deArrowBrandingProvider: DeArrowBrandingProvider?
// MARK: - Actions
/// Close the player
var onClose: (() -> Void)?
/// Toggle debug overlay
var onToggleDebug: (() -> Void)?
/// Toggle Picture-in-Picture
var onTogglePiP: (() -> Void)?
/// Toggle fullscreen mode
var onToggleFullscreen: (() -> Void)?
/// Toggle details visibility (portrait fullscreen)
var onToggleDetailsVisibility: (() -> Void)?
#if os(iOS)
/// Toggle orientation lock
var onToggleOrientationLock: (() -> Void)?
#endif
/// Toggle side panel
var onTogglePanel: (() -> Void)?
#if os(iOS)
/// Toggle panscan between 0 and 1
var onTogglePanscan: (() -> Void)?
#endif
/// Toggle auto-play next in queue
var onToggleAutoPlayNext: (() -> Void)?
/// Show settings sheet
var onShowSettings: (() -> Void)?
/// Play next video in queue
var onPlayNext: (() async -> Void)?
/// Play previous video in queue
var onPlayPrevious: (() async -> Void)?
/// Toggle play/pause
var onPlayPause: (() -> Void)?
/// Seek forward by specified seconds
var onSeekForward: ((TimeInterval) async -> Void)?
/// Seek backward by specified seconds
var onSeekBackward: ((TimeInterval) async -> Void)?
/// Volume changed (0.0-1.0)
var onVolumeChanged: ((Float) -> Void)?
/// Toggle mute
var onMuteToggled: (() -> Void)?
/// Timer control callbacks for slider interactions
var onCancelHideTimer: (() -> Void)?
var onResetHideTimer: (() -> Void)?
/// Called when slider adjustment state changes (volume or brightness)
var onSliderAdjustmentChanged: ((Bool) -> Void)?
/// Change playback rate
var onRateChanged: ((PlaybackRate) -> Void)?
/// Select caption
var onCaptionSelected: ((Caption?) -> Void)?
/// Show playlist selector sheet
var onShowPlaylistSelector: (() -> Void)?
/// Show queue management sheet
var onShowQueue: (() -> Void)?
/// Show captions selector sheet
var onShowCaptionsSelector: (() -> Void)?
/// Show chapters selector sheet
var onShowChaptersSelector: (() -> Void)?
/// Show video track selector sheet
var onShowVideoTrackSelector: (() -> Void)?
/// Show audio track selector sheet
var onShowAudioTrackSelector: (() -> Void)?
/// Toggle controls lock state
var onControlsLockToggled: ((Bool) -> Void)?
// MARK: - Computed Properties
/// Whether controls are locked (all buttons disabled except settings)
var isControlsLocked: Bool {
playerState.isControlsLocked
}
/// Current playback rate display text (shows rate when not 1x)
var playbackRateDisplay: String? {
let rate = playerState.rate
if rate == .x1 {
return nil
}
return String(format: "%.2gx", rate.rawValue)
}
/// Whether share is available (has video with share URL)
var canShare: Bool {
currentVideo != nil
}
/// Whether add to playlist is available
var canAddToPlaylist: Bool {
guard let video = currentVideo else { return false }
return !video.isFromLocalFolder
}
/// Whether captions are available
var hasCaptions: Bool {
!availableCaptions.isEmpty
}
/// Whether chapters are available
var hasChapters: Bool {
!playerState.chapters.isEmpty
}
/// PiP icon based on current state
var pipIcon: String {
playerState.pipState == .active ? "pip.exit" : "pip.enter"
}
#if os(macOS)
/// Fullscreen icon based on current state (no rotation semantics on macOS).
var fullscreenIcon: String {
isFullscreen ? "arrow.down.right.and.arrow.up.left" : "arrow.up.left.and.arrow.down.right"
}
/// Whether the fullscreen button should be shown
var shouldShowFullscreenButton: Bool {
onToggleFullscreen != nil
}
#else
/// Fullscreen icon based on current state.
/// Uses rotation icons when tapping will cause device rotation,
/// otherwise uses standard fullscreen arrows.
var fullscreenIcon: String {
if willRotateOnFullscreenToggle {
if isFullscreen {
// Currently landscape fullscreen, will rotate to portrait
return "rectangle.portrait.rotate"
} else {
// Currently portrait, will rotate to landscape
return "rectangle.landscape.rotate"
}
} else {
// Standard fullscreen icons (iPad or portrait video details toggle)
return isFullscreen ? "arrow.down.right.and.arrow.up.left" : "arrow.up.left.and.arrow.down.right"
}
}
/// Whether tapping fullscreen will cause device rotation.
/// Mirrors the logic in ControlsSectionRenderer.handleFullscreenTap()
var willRotateOnFullscreenToggle: Bool {
// iPad never rotates via fullscreen button
guard !isIPad else { return false }
let isActualWidescreenLayout = isWideScreenLayout && onTogglePanel != nil
if isActualWidescreenLayout && isFullscreen && !isWidescreenVideo {
// iPhone in landscape with portrait video: rotates to portrait
return true
} else if !isWidescreenVideo {
// iPhone portrait video in portrait: no rotation (toggles details)
return false
} else {
// iPhone widescreen video: rotates
return true
}
}
/// Orientation lock icon based on current state
var orientationLockIcon: String {
isOrientationLocked ? "lock.rotation" : "lock.rotation.open"
}
/// Panel toggle icon (flipped based on panel side)
var panelToggleIcon: String {
"sidebar.trailing"
}
/// Panscan icon based on current state
var panscanIcon: String {
panscanValue > 0.5 ? "arrow.left.and.right.square.fill" : "arrow.left.and.right.square"
}
/// Whether the fullscreen button should be shown
var shouldShowFullscreenButton: Bool {
// iPad widescreen (true landscape): hidden (can't force rotation)
let isActualWidescreen = isWideScreenLayout && onTogglePanel != nil
let showFullscreenButton = !(isIPad && isActualWidescreen)
let hasFullscreenAction = onToggleFullscreen != nil || onToggleDetailsVisibility != nil
// Hide on iPad when panel is pinned and visible (tapping does nothing)
let isPinnedPanelActive = isIPad && isPanelPinned && isPanelVisible
return showFullscreenButton && hasFullscreenAction && !isPinnedPanelActive
}
#endif
/// Whether PiP button should be enabled
var isPiPAvailable: Bool {
playerState.isPiPPossible
}
/// Whether play next button should be enabled
var hasNextInQueue: Bool {
playerState.hasNext
}
/// Whether play previous button should be enabled
var hasPreviousInQueue: Bool {
playerState.hasPrevious
}
}
#endif