luminate-old/Sources/LuminatePlayer/PlayerControls.swift

135 lines
4.2 KiB
Swift

//
// PlayerControls.swift
//
// Copyright 2026 Brendan Szymanski <hello@bscubed.dev>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: GPL-3.0-or-later
//
import Adwaita
import LuminateCore
import LuminateUI
public struct PlayerControls: View {
@Binding var isPlaying: Bool
@Binding var position: Double
@Binding var duration: Double
var playbackState: PlayerPlaybackState?
public var onClose: () -> Void
public var onSeekBack: () -> Void
public var onSeekForward: () -> Void
public var onSeekAbsolute: (Double) -> Void
public var onFullscreen: () -> Void
public var onSubtitleAudio: () -> Void
init(
isPlaying: Binding<Bool>,
position: Binding<Double>,
duration: Binding<Double>,
playbackState: PlayerPlaybackState? = nil,
onClose: @escaping () -> Void,
onSeekBack: @escaping () -> Void,
onSeekForward: @escaping () -> Void,
onSeekAbsolute: @escaping (Double) -> Void,
onFullscreen: @escaping () -> Void,
onSubtitleAudio: @escaping () -> Void
) {
self._isPlaying = isPlaying
self._position = position
self._duration = duration
self.playbackState = playbackState
self.onClose = onClose
self.onSeekBack = onSeekBack
self.onSeekForward = onSeekForward
self.onSeekAbsolute = onSeekAbsolute
self.onFullscreen = onFullscreen
self.onSubtitleAudio = onSubtitleAudio
}
public var view: Body {
Box {
HStack(spacing: 6) {
Button(icon: .custom(name: "skip-backwards-10-symbolic")) {
onSeekBack()
}
.style("circular")
Button(icon: .default(icon: isPlaying ? .mediaPlaybackPause : .mediaPlaybackStart)) {
isPlaying.toggle()
}
.style("circular")
Button(icon: .custom(name: "skip-forward-10-symbolic")) {
onSeekForward()
}
.style("circular")
Separator()
.style("spacer")
Text(formatTime(position))
.caption()
SeekBar(
value: $position,
range: 0...(duration > 0 ? duration : 1),
playbackState: playbackState,
onSeek: { newPos in
onSeekAbsolute(newPos)
}
)
.hexpand(true)
Text("-\(formatTime(max(0, duration - position)))")
.caption()
Separator()
.style("spacer")
Button(icon: .custom(name: "language-symbolic")) {
onSubtitleAudio()
}
.style("flat")
Button(icon: .default(icon: .audioVolumeHigh)) {
// Volume popover future
}
.style("flat")
.insensitive()
Button(icon: .default(icon: .viewFullscreen)) {
onFullscreen()
}
.style("flat")
}
.padding()
}
.style("card")
.style("view")
}
private func formatTime(_ seconds: Double) -> String {
guard seconds.isFinite, seconds >= 0 else { return "0:00" }
let h = Int(seconds) / 3600
let m = (Int(seconds) / 60) % 60
let s = Int(seconds) % 60
if h > 0 {
return String(format: "%d:%02d:%02d", h, m, s)
}
return String(format: "%d:%02d", m, s)
}
}