// // PlayerControls.swift // // Copyright 2026 Brendan Szymanski // // 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 . // // 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, position: Binding, duration: Binding, 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) } }