mirror of
https://github.com/yattee/yattee.git
synced 2026-02-20 09:49:46 +00:00
299 lines
8.5 KiB
Swift
299 lines
8.5 KiB
Swift
//
|
|
// PlayerControlsActions.swift
|
|
// Yattee
|
|
//
|
|
// Consolidated actions and state for player control buttons.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
#if os(iOS)
|
|
|
|
/// Consolidated actions and state for player control buttons.
|
|
/// Used by `ControlsSectionRenderer` 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
|
|
|
|
/// Whether orientation is locked
|
|
let isOrientationLocked: Bool
|
|
|
|
/// Whether the side panel is visible
|
|
let isPanelVisible: Bool
|
|
|
|
/// Whether the side panel is pinned (iPad landscape)
|
|
let isPanelPinned: Bool
|
|
|
|
/// Which side the panel is on
|
|
let panelSide: FloatingPanelSide
|
|
|
|
/// Whether running on iPad
|
|
let isIPad: Bool
|
|
|
|
/// 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?
|
|
|
|
/// Current panscan value (0.0 = fit, 1.0 = fill)
|
|
let panscanValue: Double
|
|
|
|
/// Whether panscan change is currently allowed
|
|
let isPanscanAllowed: Bool
|
|
|
|
/// 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)?
|
|
|
|
/// Toggle orientation lock
|
|
var onToggleOrientationLock: (() -> Void)?
|
|
|
|
/// Toggle side panel
|
|
var onTogglePanel: (() -> Void)?
|
|
|
|
/// Toggle panscan between 0 and 1
|
|
var onTogglePanscan: (() -> Void)?
|
|
|
|
/// 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"
|
|
}
|
|
|
|
/// 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
|
|
}
|
|
|
|
/// 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
|