botui/ui/suite/player/player.html
Rodrigo Rodriguez (Pragmatismo) 80c91f6304 Redesign home page with beautiful layout, add People/Contacts, rename Tools to Compliance
- Complete home page redesign with large icons, full descriptions, recent documents
- Add People (Contacts) menu item and page with contacts management
- Move Paper right after Chat in menu order
- Rename Tools to Compliance with shield icon
- Settings moved to end of menu
- Logo click now shows home page
- Add Project, Canvas, Goals, Player, Workspace, Video, Learn to menu
- New CSS for home page with modern card layout
2026-01-09 20:56:59 -03:00

1003 lines
26 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;
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>