botui/ui/suite/player/player.html
Rodrigo Rodriguez (Pragmatismo) 69654f37d6 refactor(ui): extract inline CSS/JS to external files
Phase 2 of CSS/JS extraction - replace inline styles and scripts with
external file references for better maintainability and caching.

Files updated:
- home.html -> css/home.css, js/home.js
- tasks/tasks.html -> tasks/tasks.css, tasks/tasks.js
- admin/index.html -> admin/admin.css, admin/admin.js
- analytics/analytics.html -> analytics/analytics.css, analytics/analytics.js
- mail/mail.html -> mail/mail.css, mail/mail.js
- monitoring/monitoring.html -> monitoring/monitoring.css, monitoring/monitoring.js
- attendant/index.html -> attendant/attendant.css, attendant/attendant.js

All JS wrapped in IIFE pattern to prevent global namespace pollution.
Functions called from HTML onclick handlers exposed via window object.
HTMX reload handlers included for proper reinitialization.

Per PROMPT.md: no CDN links, HTMX-first approach, local assets only.
2026-01-10 20:12:48 -03:00

1172 lines
32 KiB
HTML

<!-- =============================================================================
PLAYER APP - Media Viewer for Documents, Audio, Video, and Presentations
Respects Theme Manager - No hardcoded theme
============================================================================= -->
<div class="player-app">
<!-- Sidebar - File Browser -->
<aside class="player-sidebar">
<div class="sidebar-header">
<h2 data-i18n="player-title">Player</h2>
</div>
<div class="sidebar-search">
<input
type="search"
placeholder="Search files..."
hx-get="/api/drive/list"
hx-trigger="keyup changed delay:300ms"
hx-target="#file-list"
hx-swap="innerHTML"
name="q"
/>
</div>
<nav class="sidebar-nav">
<div class="nav-section">
<h3>Recent</h3>
<div
id="recent-files"
hx-get="/api/drive/list?filter=recent&types=media"
hx-trigger="load"
hx-swap="innerHTML"
>
<div class="loading-placeholder">Loading...</div>
</div>
</div>
<div class="nav-section">
<h3>Files</h3>
<div class="file-type-filters">
<button
class="filter-btn active"
data-type="all"
onclick="filterFiles('all')"
>
All
</button>
<button
class="filter-btn"
data-type="video"
onclick="filterFiles('video')"
>
🎬
</button>
<button
class="filter-btn"
data-type="audio"
onclick="filterFiles('audio')"
>
🎵
</button>
<button
class="filter-btn"
data-type="document"
onclick="filterFiles('document')"
>
📄
</button>
<button
class="filter-btn"
data-type="image"
onclick="filterFiles('image')"
>
🖼️
</button>
</div>
<div
id="file-list"
hx-get="/api/drive/list?types=media"
hx-trigger="load, fileUploaded from:body"
hx-swap="innerHTML"
>
<div class="loading-placeholder">Loading files...</div>
</div>
</div>
</nav>
</aside>
<!-- Main Content - Player Area -->
<main class="player-main">
<!-- Player Header -->
<header class="player-header">
<div class="file-info">
<h1 id="file-name">No file selected</h1>
<div class="file-meta">
<span id="file-type">--</span>
<span id="file-size">--</span>
<span id="file-duration">--</span>
</div>
</div>
<div class="player-actions">
<button
class="btn-icon"
id="btn-download"
title="Download"
disabled
onclick="downloadFile()"
>
<span>📥</span>
</button>
<button
class="btn-icon"
id="btn-share"
title="Share"
disabled
onclick="shareFile()"
>
<span>🔗</span>
</button>
<button
class="btn-icon"
id="btn-fullscreen"
title="Fullscreen"
disabled
onclick="toggleFullscreen()"
>
<span></span>
</button>
</div>
</header>
<!-- Player Container -->
<div class="player-container" id="player-container">
<!-- Video Player -->
<div id="video-player" class="media-player hidden">
<video id="video-element" controls>
<source src="" type="video/mp4" />
Your browser does not support the video tag.
</video>
</div>
<!-- Audio Player -->
<div id="audio-player" class="media-player hidden">
<div class="audio-visualizer" id="audio-visualizer">
<div class="album-art">🎵</div>
</div>
<audio id="audio-element" controls>
<source src="" type="audio/mpeg" />
Your browser does not support the audio tag.
</audio>
</div>
<!-- Document Viewer -->
<div id="document-viewer" class="media-player hidden">
<iframe id="document-frame" src="" frameborder="0"></iframe>
</div>
<!-- Image Viewer -->
<div id="image-viewer" class="media-player hidden">
<img id="image-element" src="" alt="" />
</div>
<!-- Presentation Viewer -->
<div id="presentation-viewer" class="media-player hidden">
<div class="slide-container">
<div id="slide-content"></div>
</div>
<div class="slide-navigation">
<button class="nav-btn" onclick="prevSlide()"></button>
<span id="slide-counter">1 / 1</span>
<button class="nav-btn" onclick="nextSlide()"></button>
</div>
</div>
<!-- Empty State -->
<div id="empty-state" class="empty-state">
<div class="empty-state-icon">🎬</div>
<h2>No Media Selected</h2>
<p>Select a file from the sidebar to start viewing</p>
<div class="supported-formats">
<h4>Supported Formats</h4>
<div class="format-grid">
<div class="format-item">
<span class="format-icon">🎬</span>
<span class="format-label">MP4, WebM, OGV</span>
</div>
<div class="format-item">
<span class="format-icon">🎵</span>
<span class="format-label">MP3, WAV, OGG</span>
</div>
<div class="format-item">
<span class="format-icon">📄</span>
<span class="format-label">PDF, TXT, MD</span>
</div>
<div class="format-item">
<span class="format-icon">🖼️</span>
<span class="format-label">PNG, JPG, GIF</span>
</div>
</div>
</div>
</div>
</div>
<!-- Playback Controls (for video/audio) -->
<div class="playback-controls hidden" id="playback-controls">
<div class="progress-bar">
<div class="progress-track">
<div class="progress-fill" id="progress-fill"></div>
<div class="progress-handle" id="progress-handle"></div>
</div>
<div class="time-display">
<span id="current-time">0:00</span>
<span id="total-time">0:00</span>
</div>
</div>
<div class="control-buttons">
<button
class="control-btn"
onclick="skipBackward()"
data-i18n-title="player-previous"
>
<span></span>
</button>
<button
class="control-btn play-pause"
id="play-pause-btn"
onclick="togglePlayPause()"
data-i18n-title="player-play"
>
<span id="play-icon"></span>
</button>
<button
class="control-btn"
onclick="skipForward()"
data-i18n-title="player-next"
>
<span></span>
</button>
<div class="volume-control">
<button
class="control-btn"
onclick="toggleMute()"
data-i18n-title="player-volume"
>
<span id="volume-icon">🔊</span>
</button>
<input
type="range"
id="volume-slider"
min="0"
max="100"
value="100"
onchange="setVolume(this.value)"
/>
</div>
<div class="playback-speed">
<select
id="speed-select"
onchange="setPlaybackSpeed(this.value)"
>
<option value="0.5">0.5x</option>
<option value="0.75">0.75x</option>
<option value="1" selected>1x</option>
<option value="1.25">1.25x</option>
<option value="1.5">1.5x</option>
<option value="2">2x</option>
</select>
</div>
</div>
</div>
</main>
<!-- Playlist Panel -->
<aside class="playlist-panel collapsed" id="playlist-panel">
<button class="panel-toggle" onclick="togglePlaylistPanel()">
<span>📋</span>
</button>
<div class="panel-content">
<h3>Playlist</h3>
<div id="playlist-items">
<p class="empty-message">No playlist created</p>
</div>
<button class="btn-secondary btn-sm" onclick="createPlaylist()">
Create Playlist
</button>
</div>
</aside>
</div>
<style>
.player-app {
display: flex;
height: 100%;
background: var(--bg-primary);
color: var(--text-primary);
}
.player-sidebar {
width: 280px;
min-width: 280px;
background: var(--bg-secondary);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
overflow: hidden;
}
.sidebar-header {
padding: 1rem;
border-bottom: 1px solid var(--border-color);
}
.sidebar-header h2 {
font-size: 1rem;
font-weight: 600;
margin: 0;
}
.sidebar-search {
padding: 0.75rem 1rem;
}
.sidebar-search input {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--bg-primary);
color: var(--text-primary);
font-size: 0.875rem;
}
.sidebar-nav {
flex: 1;
overflow-y: auto;
padding: 0.5rem 0;
}
.nav-section {
margin-bottom: 1rem;
}
.nav-section h3 {
font-size: 0.75rem;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
padding: 0.5rem 1rem;
margin: 0;
}
.file-type-filters {
display: flex;
gap: 0.25rem;
padding: 0 1rem 0.5rem;
}
.filter-btn {
padding: 0.375rem 0.5rem;
border: 1px solid var(--border-color);
background: var(--bg-primary);
color: var(--text-secondary);
font-size: 0.75rem;
cursor: pointer;
border-radius: 4px;
}
.filter-btn.active {
background: var(--accent-color);
color: white;
border-color: var(--accent-color);
}
.player-main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.player-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border-color);
background: var(--bg-secondary);
}
.file-info h1 {
font-size: 1.125rem;
font-weight: 600;
margin: 0 0 0.25rem 0;
}
.file-meta {
display: flex;
gap: 1rem;
font-size: 0.875rem;
color: var(--text-muted);
}
.player-actions {
display: flex;
gap: 0.5rem;
}
.player-container {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: #000;
position: relative;
overflow: hidden;
}
.media-player {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.media-player.hidden {
display: none;
}
#video-element {
max-width: 100%;
max-height: 100%;
}
#audio-player {
flex-direction: column;
background: var(--bg-secondary);
}
.audio-visualizer {
width: 200px;
height: 200px;
background: var(--bg-primary);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 2rem;
}
.album-art {
font-size: 4rem;
}
#audio-element {
width: 80%;
max-width: 500px;
}
#document-frame {
width: 100%;
height: 100%;
background: white;
}
#image-element {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.slide-container {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: white;
color: #000;
padding: 2rem;
}
.slide-navigation {
position: absolute;
bottom: 1rem;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 1rem;
background: rgba(0, 0, 0, 0.7);
padding: 0.5rem 1rem;
border-radius: 9999px;
color: white;
}
.nav-btn {
width: 32px;
height: 32px;
border: none;
background: transparent;
color: white;
font-size: 1rem;
cursor: pointer;
border-radius: 50%;
}
.nav-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
color: var(--text-muted);
background: var(--bg-primary);
width: 100%;
height: 100%;
}
.empty-state-icon {
font-size: 4rem;
margin-bottom: 1rem;
}
.empty-state h2 {
color: var(--text-primary);
margin: 0 0 0.5rem 0;
}
.supported-formats {
margin-top: 2rem;
padding: 1.5rem;
background: var(--bg-secondary);
border-radius: 8px;
}
.supported-formats h4 {
margin: 0 0 1rem 0;
color: var(--text-primary);
}
.format-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.format-item {
display: flex;
align-items: center;
gap: 0.5rem;
}
.format-icon {
font-size: 1.25rem;
}
.format-label {
font-size: 0.875rem;
}
.playback-controls {
padding: 1rem 1.5rem;
background: var(--bg-secondary);
border-top: 1px solid var(--border-color);
}
.playback-controls.hidden {
display: none;
}
.progress-bar {
margin-bottom: 0.75rem;
}
.progress-track {
height: 6px;
background: var(--border-color);
border-radius: 3px;
position: relative;
cursor: pointer;
}
.progress-fill {
height: 100%;
background: var(--accent-color);
border-radius: 3px;
width: 0%;
}
.progress-handle {
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
width: 14px;
height: 14px;
background: var(--accent-color);
border-radius: 50%;
cursor: pointer;
left: 0%;
}
.time-display {
display: flex;
justify-content: space-between;
font-size: 0.75rem;
color: var(--text-muted);
margin-top: 0.25rem;
}
.control-buttons {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
}
.control-btn {
width: 40px;
height: 40px;
border: none;
background: transparent;
color: var(--text-primary);
font-size: 1.25rem;
cursor: pointer;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.control-btn:hover {
background: var(--bg-hover);
}
.control-btn.play-pause {
width: 48px;
height: 48px;
background: var(--accent-color);
color: white;
}
.control-btn.play-pause:hover {
background: var(--accent-hover);
}
.volume-control {
display: flex;
align-items: center;
gap: 0.5rem;
}
#volume-slider {
width: 80px;
height: 4px;
cursor: pointer;
}
.playback-speed select {
padding: 0.25rem 0.5rem;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--bg-primary);
color: var(--text-primary);
font-size: 0.75rem;
}
.playlist-panel {
width: 280px;
background: var(--bg-secondary);
border-left: 1px solid var(--border-color);
transition: width 0.2s;
}
.playlist-panel.collapsed {
width: 48px;
}
.playlist-panel.collapsed .panel-content {
display: none;
}
.panel-toggle {
width: 100%;
padding: 0.75rem;
border: none;
background: transparent;
cursor: pointer;
font-size: 1.25rem;
}
.panel-content {
padding: 1rem;
}
.panel-content h3 {
font-size: 0.875rem;
font-weight: 600;
margin: 0 0 1rem 0;
}
.btn-icon {
width: 36px;
height: 36px;
border: none;
background: var(--bg-primary);
color: var(--text-secondary);
cursor: pointer;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
}
.btn-icon:hover:not(:disabled) {
background: var(--bg-hover);
color: var(--text-primary);
}
.btn-icon:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-secondary {
padding: 0.5rem 1rem;
background: var(--bg-primary);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 0.875rem;
cursor: pointer;
width: 100%;
}
.btn-sm {
padding: 0.375rem 0.75rem;
font-size: 0.75rem;
}
.loading-placeholder {
color: var(--text-muted);
font-size: 0.875rem;
padding: 1rem;
text-align: center;
}
.empty-message {
color: var(--text-muted);
font-size: 0.875rem;
text-align: center;
}
.file-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.5rem 1rem;
cursor: pointer;
border-radius: 4px;
}
.file-item:hover {
background: var(--bg-hover);
}
.file-item.active {
background: var(--accent-color);
color: white;
}
.file-item-icon {
font-size: 1.25rem;
}
.file-item-name {
flex: 1;
font-size: 0.875rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.file-item-duration {
font-size: 0.75rem;
color: var(--text-muted);
}
@media (max-width: 1024px) {
.playlist-panel {
display: none;
}
}
@media (max-width: 768px) {
.player-sidebar {
position: absolute;
left: -280px;
height: 100%;
z-index: 50;
transition: left 0.2s;
}
.player-sidebar.open {
left: 0;
}
.format-grid {
grid-template-columns: 1fr;
}
}
</style>
<script>
let currentFile = null;
let currentPlayer = null;
let currentSlide = 0;
let totalSlides = 1;
let driveSource = null;
document.addEventListener("DOMContentLoaded", loadFromUrlParams);
async function loadFromUrlParams() {
const urlParams = new URLSearchParams(window.location.search);
const hash = window.location.hash;
let bucket = urlParams.get("bucket");
let path = urlParams.get("path");
if (hash) {
const hashQueryIndex = hash.indexOf("?");
if (hashQueryIndex !== -1) {
const hashParams = new URLSearchParams(
hash.substring(hashQueryIndex + 1),
);
bucket = bucket || hashParams.get("bucket");
path = path || hashParams.get("path");
}
}
if (bucket && path) {
await loadFromDrive(bucket, path);
}
}
async function loadFromDrive(bucket, path) {
const fileName = path.split("/").pop() || "media";
const ext = fileName.split(".").pop().toLowerCase();
driveSource = { bucket, path };
const audioExts = [
"mp3",
"wav",
"ogg",
"oga",
"flac",
"aac",
"m4a",
"wma",
"aiff",
"aif",
];
const type = audioExts.includes(ext) ? "audio" : "video";
hideAllPlayers();
currentFile = { path, type, name: fileName };
document.getElementById("file-name").textContent = fileName;
document.getElementById("btn-download").disabled = false;
document.getElementById("btn-share").disabled = false;
document.getElementById("btn-fullscreen").disabled = false;
try {
const response = await fetch("/api/files/download", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ bucket, path }),
});
if (!response.ok) {
throw new Error(`Failed to load file: ${response.status}`);
}
const blob = await response.blob();
const url = URL.createObjectURL(blob);
if (type === "audio") {
showAudioPlayer(url);
} else {
showVideoPlayer(url);
}
} catch (err) {
console.error("Failed to load file from drive:", err);
document.getElementById("empty-state").classList.remove("hidden");
}
}
function loadFile(path, type, name) {
hideAllPlayers();
currentFile = { path, type, name };
document.getElementById("file-name").textContent = name;
document.getElementById("btn-download").disabled = false;
document.getElementById("btn-share").disabled = false;
document.getElementById("btn-fullscreen").disabled = false;
const streamUrl = `/api/player/default/stream/${path}`;
switch (type) {
case "video":
showVideoPlayer(streamUrl);
break;
case "audio":
showAudioPlayer(streamUrl);
break;
case "document":
case "pdf":
showDocumentViewer(streamUrl);
break;
case "image":
showImageViewer(streamUrl);
break;
case "presentation":
showPresentationViewer(streamUrl);
break;
default:
showDocumentViewer(streamUrl);
}
}
function hideAllPlayers() {
document.getElementById("video-player").classList.add("hidden");
document.getElementById("audio-player").classList.add("hidden");
document.getElementById("document-viewer").classList.add("hidden");
document.getElementById("image-viewer").classList.add("hidden");
document.getElementById("presentation-viewer").classList.add("hidden");
document.getElementById("empty-state").classList.add("hidden");
document.getElementById("playback-controls").classList.add("hidden");
}
function showVideoPlayer(url) {
const player = document.getElementById("video-player");
const video = document.getElementById("video-element");
video.src = url;
player.classList.remove("hidden");
document.getElementById("playback-controls").classList.remove("hidden");
currentPlayer = video;
setupMediaEvents(video);
}
function showAudioPlayer(url) {
const player = document.getElementById("audio-player");
const audio = document.getElementById("audio-element");
audio.src = url;
player.classList.remove("hidden");
document.getElementById("playback-controls").classList.remove("hidden");
currentPlayer = audio;
setupMediaEvents(audio);
}
function showDocumentViewer(url) {
const viewer = document.getElementById("document-viewer");
const frame = document.getElementById("document-frame");
frame.src = url;
viewer.classList.remove("hidden");
}
function showImageViewer(url) {
const viewer = document.getElementById("image-viewer");
const img = document.getElementById("image-element");
img.src = url;
viewer.classList.remove("hidden");
}
function showPresentationViewer(url) {
const viewer = document.getElementById("presentation-viewer");
viewer.classList.remove("hidden");
currentSlide = 0;
updateSlideCounter();
}
function setupMediaEvents(media) {
media.addEventListener("timeupdate", updateProgress);
media.addEventListener("loadedmetadata", function () {
document.getElementById("total-time").textContent = formatTime(
media.duration,
);
document.getElementById("file-duration").textContent = formatTime(
media.duration,
);
});
media.addEventListener("play", function () {
document.getElementById("play-icon").textContent = "⏸";
});
media.addEventListener("pause", function () {
document.getElementById("play-icon").textContent = "▶";
});
}
function updateProgress() {
if (!currentPlayer) return;
const progress =
(currentPlayer.currentTime / currentPlayer.duration) * 100;
document.getElementById("progress-fill").style.width = progress + "%";
document.getElementById("progress-handle").style.left = progress + "%";
document.getElementById("current-time").textContent = formatTime(
currentPlayer.currentTime,
);
}
function formatTime(seconds) {
if (isNaN(seconds)) return "0:00";
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, "0")}`;
}
function togglePlayPause() {
if (!currentPlayer) return;
if (currentPlayer.paused) {
currentPlayer.play();
} else {
currentPlayer.pause();
}
}
function skipBackward() {
if (!currentPlayer) return;
currentPlayer.currentTime = Math.max(0, currentPlayer.currentTime - 10);
}
function skipForward() {
if (!currentPlayer) return;
currentPlayer.currentTime = Math.min(
currentPlayer.duration,
currentPlayer.currentTime + 10,
);
}
function toggleMute() {
if (!currentPlayer) return;
currentPlayer.muted = !currentPlayer.muted;
document.getElementById("volume-icon").textContent = currentPlayer.muted
? "🔇"
: "🔊";
}
function setVolume(value) {
if (!currentPlayer) return;
currentPlayer.volume = value / 100;
document.getElementById("volume-icon").textContent =
value == 0 ? "🔇" : "🔊";
}
function setPlaybackSpeed(speed) {
if (!currentPlayer) return;
currentPlayer.playbackRate = parseFloat(speed);
}
function prevSlide() {
if (currentSlide > 0) {
currentSlide--;
updateSlideCounter();
}
}
function nextSlide() {
if (currentSlide < totalSlides - 1) {
currentSlide++;
updateSlideCounter();
}
}
function updateSlideCounter() {
document.getElementById("slide-counter").textContent =
`${currentSlide + 1} / ${totalSlides}`;
}
function toggleFullscreen() {
const container = document.getElementById("player-container");
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
container.requestFullscreen();
}
}
function downloadFile() {
if (!currentFile) return;
const link = document.createElement("a");
link.href = `/api/player/default/stream/${currentFile.path}`;
link.download = currentFile.name;
link.click();
}
function shareFile() {
if (!currentFile) return;
const url = `${window.location.origin}/player?file=${encodeURIComponent(currentFile.path)}`;
navigator.clipboard.writeText(url);
alert("Link copied to clipboard!");
}
function filterFiles(type) {
document.querySelectorAll(".filter-btn").forEach((btn) => {
btn.classList.toggle("active", btn.dataset.type === type);
});
const typeParam = type === "all" ? "media" : type;
htmx.ajax("GET", `/api/drive/list?types=${typeParam}`, {
target: "#file-list",
swap: "innerHTML",
});
}
function togglePlaylistPanel() {
const panel = document.getElementById("playlist-panel");
panel.classList.toggle("collapsed");
}
function createPlaylist() {
alert("Playlist feature coming soon!");
}
document.addEventListener("keydown", function (e) {
if (!currentPlayer) return;
switch (e.key) {
case " ":
e.preventDefault();
togglePlayPause();
break;
case "ArrowLeft":
skipBackward();
break;
case "ArrowRight":
skipForward();
break;
case "ArrowUp":
e.preventDefault();
setVolume(Math.min(100, currentPlayer.volume * 100 + 10));
break;
case "ArrowDown":
e.preventDefault();
setVolume(Math.max(0, currentPlayer.volume * 100 - 10));
break;
case "f":
toggleFullscreen();
break;
case "m":
toggleMute();
break;
case "Escape":
if (document.fullscreenElement) {
document.exitFullscreen();
}
break;
}
});
document
.querySelector(".progress-track")
.addEventListener("click", function (e) {
if (!currentPlayer) return;
const rect = this.getBoundingClientRect();
const percent = (e.clientX - rect.left) / rect.width;
currentPlayer.currentTime = percent * currentPlayer.duration;
});
</script>