mirror of
https://github.com/yattee/yattee.git
synced 2026-07-21 14:52:03 +00:00
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.
324 lines
9.1 KiB
Swift
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
|