botui/ui/suite/chat/projector.html

1399 lines
36 KiB
HTML

<!-- Content Projector/Player Component -->
<!-- Displays video, images, PDFs, presentations, code, and more -->
<div id="projector-overlay" class="projector-overlay hidden" onclick="closeProjectorOnOverlay(event)">
<div class="projector-container" onclick="event.stopPropagation()">
<!-- Header -->
<div class="projector-header">
<div class="projector-title-section">
<span class="projector-icon" id="projector-icon">🎬</span>
<span class="projector-title" id="projector-title">Content Viewer</span>
</div>
<div class="projector-actions">
<button class="projector-btn" onclick="toggleFullscreen()" title="Fullscreen">
<span id="fullscreen-icon"></span>
</button>
<button class="projector-btn" onclick="downloadContent()" title="Download">
⬇️
</button>
<button class="projector-btn" onclick="shareContent()" title="Share">
🔗
</button>
<button class="projector-btn close-btn" onclick="closeProjector()" title="Close">
</button>
</div>
</div>
<!-- Content Area -->
<div class="projector-content" id="projector-content">
<!-- Dynamic content loaded here -->
<div class="projector-loading" id="projector-loading">
<div class="loading-spinner"></div>
<span>Loading content...</span>
</div>
</div>
<!-- Controls -->
<div class="projector-controls" id="projector-controls">
<!-- Video/Audio Controls -->
<div class="media-controls hidden" id="media-controls">
<button class="control-btn" onclick="mediaSeekBack()" title="Back 10s">
</button>
<button class="control-btn play-btn" onclick="togglePlayPause()" id="play-pause-btn" title="Play/Pause">
▶️
</button>
<button class="control-btn" onclick="mediaSeekForward()" title="Forward 10s">
</button>
<div class="progress-container">
<input type="range" class="progress-bar" id="progress-bar"
min="0" max="100" value="0"
oninput="seekTo(this.value)">
<span class="time-display" id="time-display">0:00 / 0:00</span>
</div>
<button class="control-btn" onclick="toggleMute()" id="mute-btn" title="Mute">
🔊
</button>
<input type="range" class="volume-slider" id="volume-slider"
min="0" max="100" value="100"
oninput="setVolume(this.value)">
<button class="control-btn" onclick="toggleLoop()" id="loop-btn" title="Loop">
🔁
</button>
<select class="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>
<!-- Presentation/PDF Controls -->
<div class="slide-controls hidden" id="slide-controls">
<button class="control-btn" onclick="prevSlide()" title="Previous">
◀️
</button>
<span class="slide-info" id="slide-info">Slide 1 of 1</span>
<button class="control-btn" onclick="nextSlide()" title="Next">
▶️
</button>
<div class="slide-nav">
<input type="number" id="slide-input" min="1" value="1"
onchange="goToSlide(this.value)">
</div>
<button class="control-btn" onclick="zoomIn()" title="Zoom In">
🔍+
</button>
<button class="control-btn" onclick="zoomOut()" title="Zoom Out">
🔍-
</button>
<span class="zoom-level" id="zoom-level">100%</span>
</div>
<!-- Image Controls -->
<div class="image-controls hidden" id="image-controls">
<button class="control-btn" onclick="prevImage()" id="prev-image-btn" title="Previous">
◀️
</button>
<span class="image-info" id="image-info">1 of 1</span>
<button class="control-btn" onclick="nextImage()" id="next-image-btn" title="Next">
▶️
</button>
<button class="control-btn" onclick="zoomIn()" title="Zoom In">
🔍+
</button>
<button class="control-btn" onclick="zoomOut()" title="Zoom Out">
🔍-
</button>
<button class="control-btn" onclick="rotateImage()" title="Rotate">
🔄
</button>
<button class="control-btn" onclick="fitToScreen()" title="Fit to Screen">
📐
</button>
</div>
<!-- Code Controls -->
<div class="code-controls hidden" id="code-controls">
<span class="code-info" id="code-info">code.rs</span>
<button class="control-btn" onclick="toggleLineNumbers()" title="Line Numbers">
#️⃣
</button>
<button class="control-btn" onclick="toggleWordWrap()" title="Word Wrap">
↩️
</button>
<select class="theme-select" id="theme-select" onchange="setCodeTheme(this.value)">
<option value="dark">Dark</option>
<option value="light">Light</option>
<option value="monokai">Monokai</option>
<option value="github">GitHub</option>
</select>
<button class="control-btn" onclick="copyCode()" title="Copy">
📋
</button>
</div>
</div>
</div>
</div>
<style>
/* Projector Overlay */
.projector-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.9);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
animation: fadeIn 0.2s ease;
}
.projector-overlay.hidden {
display: none;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* Container */
.projector-container {
width: 95%;
max-width: 1400px;
height: 90%;
max-height: 900px;
background: #1a1a1a;
border-radius: 12px;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}
.projector-container.fullscreen {
width: 100%;
max-width: none;
height: 100%;
max-height: none;
border-radius: 0;
}
/* Header */
.projector-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
background: #252525;
border-bottom: 1px solid #333;
}
.projector-title-section {
display: flex;
align-items: center;
gap: 10px;
}
.projector-icon {
font-size: 20px;
}
.projector-title {
color: #fff;
font-size: 16px;
font-weight: 500;
max-width: 400px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.projector-actions {
display: flex;
gap: 8px;
}
.projector-btn {
background: transparent;
border: none;
color: #aaa;
font-size: 18px;
padding: 8px;
cursor: pointer;
border-radius: 6px;
transition: all 0.2s ease;
}
.projector-btn:hover {
background: #333;
color: #fff;
}
.projector-btn.close-btn:hover {
background: #e74c3c;
color: #fff;
}
/* Content Area */
.projector-content {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
position: relative;
background: #000;
}
/* Loading */
.projector-loading {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
color: #888;
}
.projector-loading.hidden {
display: none;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid #333;
border-top-color: #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Video Player */
.projector-video {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
}
/* Audio Player */
.projector-audio {
width: 80%;
max-width: 600px;
}
.audio-visualizer {
width: 100%;
height: 200px;
background: linear-gradient(180deg, #1a1a2e 0%, #16213e 100%);
border-radius: 8px;
margin-bottom: 20px;
}
/* Image Viewer */
.projector-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
transition: transform 0.3s ease;
}
/* PDF Viewer */
.projector-pdf {
width: 100%;
height: 100%;
border: none;
}
/* Code Viewer */
.projector-code {
width: 100%;
height: 100%;
overflow: auto;
background: #1e1e1e;
padding: 20px;
}
.projector-code pre {
margin: 0;
font-family: 'Fira Code', 'Monaco', 'Consolas', monospace;
font-size: 14px;
line-height: 1.5;
color: #d4d4d4;
}
.projector-code.line-numbers pre {
counter-reset: line;
}
.projector-code.line-numbers pre .line::before {
counter-increment: line;
content: counter(line);
display: inline-block;
width: 40px;
padding-right: 20px;
color: #666;
text-align: right;
}
/* Presentation Viewer */
.projector-presentation {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.slide-container {
background: #fff;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
aspect-ratio: 16/9;
max-width: 90%;
max-height: 90%;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.slide-content {
width: 100%;
height: 100%;
object-fit: contain;
}
/* Iframe Viewer */
.projector-iframe {
width: 100%;
height: 100%;
border: none;
background: #fff;
}
/* Markdown Viewer */
.projector-markdown {
width: 100%;
height: 100%;
overflow: auto;
padding: 40px;
background: #fff;
color: #333;
}
.projector-markdown h1,
.projector-markdown h2,
.projector-markdown h3 {
color: #1a1a1a;
margin-top: 24px;
margin-bottom: 12px;
}
.projector-markdown code {
background: #f0f0f0;
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
}
.projector-markdown pre code {
display: block;
padding: 16px;
overflow-x: auto;
}
/* Controls */
.projector-controls {
padding: 12px 20px;
background: #252525;
border-top: 1px solid #333;
}
.media-controls,
.slide-controls,
.image-controls,
.code-controls {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.media-controls.hidden,
.slide-controls.hidden,
.image-controls.hidden,
.code-controls.hidden {
display: none;
}
.control-btn {
background: #333;
border: none;
color: #fff;
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
transition: all 0.2s ease;
}
.control-btn:hover {
background: #444;
}
.control-btn.active {
background: #667eea;
}
.play-btn {
font-size: 20px;
padding: 8px 16px;
}
/* Progress Bar */
.progress-container {
flex: 1;
display: flex;
align-items: center;
gap: 12px;
min-width: 200px;
}
.progress-bar {
flex: 1;
height: 6px;
-webkit-appearance: none;
appearance: none;
background: #444;
border-radius: 3px;
cursor: pointer;
}
.progress-bar::-webkit-slider-thumb {
-webkit-appearance: none;
width: 14px;
height: 14px;
background: #667eea;
border-radius: 50%;
cursor: pointer;
}
.progress-bar::-moz-range-thumb {
width: 14px;
height: 14px;
background: #667eea;
border-radius: 50%;
cursor: pointer;
border: none;
}
.time-display {
color: #888;
font-size: 12px;
min-width: 100px;
text-align: right;
}
/* Volume */
.volume-slider {
width: 80px;
height: 4px;
-webkit-appearance: none;
appearance: none;
background: #444;
border-radius: 2px;
cursor: pointer;
}
.volume-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
background: #fff;
border-radius: 50%;
cursor: pointer;
}
/* Speed/Theme Select */
.speed-select,
.theme-select {
background: #333;
color: #fff;
border: none;
padding: 6px 10px;
border-radius: 6px;
cursor: pointer;
}
/* Slide Info */
.slide-info,
.image-info,
.code-info,
.zoom-level {
color: #888;
font-size: 14px;
}
.slide-nav input {
width: 50px;
background: #333;
border: none;
color: #fff;
padding: 6px;
border-radius: 4px;
text-align: center;
}
/* Error State */
.projector-error {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
color: #e74c3c;
}
.projector-error-icon {
font-size: 48px;
}
.projector-error-message {
font-size: 16px;
color: #888;
}
/* Responsive */
@media (max-width: 768px) {
.projector-container {
width: 100%;
height: 100%;
max-width: none;
max-height: none;
border-radius: 0;
}
.projector-header {
padding: 10px 16px;
}
.projector-title {
max-width: 150px;
font-size: 14px;
}
.projector-controls {
padding: 10px 16px;
}
.progress-container {
min-width: 120px;
}
.time-display {
display: none;
}
.volume-slider {
display: none;
}
}
</style>
<script>
// Projector State
let projectorState = {
isOpen: false,
contentType: null,
source: null,
options: {},
currentSlide: 1,
totalSlides: 1,
currentImage: 0,
totalImages: 1,
zoom: 100,
rotation: 0,
isPlaying: false,
isLooping: false,
isMuted: false,
lineNumbers: true,
wordWrap: false
};
// Get media element
function getMediaElement() {
return document.querySelector('.projector-video, .projector-audio');
}
// Open Projector
function openProjector(data) {
const overlay = document.getElementById('projector-overlay');
const content = document.getElementById('projector-content');
const loading = document.getElementById('projector-loading');
const title = document.getElementById('projector-title');
const icon = document.getElementById('projector-icon');
// Reset state
projectorState = {
...projectorState,
isOpen: true,
contentType: data.content_type,
source: data.source_url,
options: data.options || {}
};
// Set title
title.textContent = data.title || 'Content Viewer';
// Set icon based on content type
const icons = {
'Video': '🎬',
'Audio': '🎵',
'Image': '🖼️',
'Pdf': '📄',
'Presentation': '📊',
'Code': '💻',
'Spreadsheet': '📈',
'Markdown': '📝',
'Html': '🌐',
'Document': '📃'
};
icon.textContent = icons[data.content_type] || '📁';
// Show loading
loading.classList.remove('hidden');
hideAllControls();
// Show overlay
overlay.classList.remove('hidden');
// Load content based on type
loadContent(data);
}
// Load Content
function loadContent(data) {
const content = document.getElementById('projector-content');
const loading = document.getElementById('projector-loading');
setTimeout(() => {
loading.classList.add('hidden');
switch (data.content_type) {
case 'Video':
loadVideo(content, data);
break;
case 'Audio':
loadAudio(content, data);
break;
case 'Image':
loadImage(content, data);
break;
case 'Pdf':
loadPdf(content, data);
break;
case 'Presentation':
loadPresentation(content, data);
break;
case 'Code':
loadCode(content, data);
break;
case 'Markdown':
loadMarkdown(content, data);
break;
case 'Iframe':
case 'Html':
loadIframe(content, data);
break;
default:
loadGeneric(content, data);
}
}, 300);
}
// Load Video
function loadVideo(container, data) {
const loading = document.getElementById('projector-loading');
const video = document.createElement('video');
video.className = 'projector-video';
video.src = data.source_url;
video.controls = false;
video.autoplay = data.options?.autoplay || false;
video.loop = data.options?.loop_content || false;
video.muted = data.options?.muted || false;
video.addEventListener('loadedmetadata', () => {
loading.classList.add('hidden');
updateTimeDisplay();
});
video.addEventListener('timeupdate', () => {
updateProgress();
updateTimeDisplay();
});
video.addEventListener('play', () => {
projectorState.isPlaying = true;
document.getElementById('play-pause-btn').textContent = '⏸️';
});
video.addEventListener('pause', () => {
projectorState.isPlaying = false;
document.getElementById('play-pause-btn').textContent = '▶️';
});
video.addEventListener('ended', () => {
if (!projectorState.isLooping) {
projectorState.isPlaying = false;
document.getElementById('play-pause-btn').textContent = '▶️';
}
});
// Clear and add video
clearContent(container);
container.appendChild(video);
// Show media controls
showControls('media');
}
// Load Audio
function loadAudio(container, data) {
const wrapper = document.createElement('div');
wrapper.style.textAlign = 'center';
wrapper.style.padding = '40px';
// Visualizer placeholder
const visualizer = document.createElement('canvas');
visualizer.className = 'audio-visualizer';
visualizer.id = 'audio-visualizer';
wrapper.appendChild(visualizer);
const audio = document.createElement('audio');
audio.className = 'projector-audio';
audio.src = data.source_url;
audio.autoplay = data.options?.autoplay || false;
audio.loop = data.options?.loop_content || false;
audio.addEventListener('loadedmetadata', () => updateTimeDisplay());
audio.addEventListener('timeupdate', () => {
updateProgress();
updateTimeDisplay();
});
audio.addEventListener('play', () => {
projectorState.isPlaying = true;
document.getElementById('play-pause-btn').textContent = '⏸️';
});
audio.addEventListener('pause', () => {
projectorState.isPlaying = false;
document.getElementById('play-pause-btn').textContent = '▶️';
});
wrapper.appendChild(audio);
clearContent(container);
container.appendChild(wrapper);
showControls('media');
}
// Load Image
function loadImage(container, data) {
const img = document.createElement('img');
img.className = 'projector-image';
img.src = data.source_url;
img.alt = data.title || 'Image';
img.id = 'projector-img';
img.addEventListener('load', () => {
document.getElementById('projector-loading').classList.add('hidden');
});
img.addEventListener('error', () => {
showError('Failed to load image');
});
clearContent(container);
container.appendChild(img);
// Hide nav if single image
document.getElementById('prev-image-btn').style.display =
projectorState.totalImages > 1 ? 'block' : 'none';
document.getElementById('next-image-btn').style.display =
projectorState.totalImages > 1 ? 'block' : 'none';
showControls('image');
updateImageInfo();
}
// Load PDF
function loadPdf(container, data) {
const iframe = document.createElement('iframe');
iframe.className = 'projector-pdf';
iframe.src = `/static/pdfjs/web/viewer.html?file=${encodeURIComponent(data.source_url)}`;
clearContent(container);
container.appendChild(iframe);
showControls('slide');
}
// Load Presentation
function loadPresentation(container, data) {
const wrapper = document.createElement('div');
wrapper.className = 'projector-presentation';
const slideContainer = document.createElement('div');
slideContainer.className = 'slide-container';
slideContainer.id = 'slide-container';
// For now, show as images (each slide converted to image)
const slideImg = document.createElement('img');
slideImg.className = 'slide-content';
slideImg.id = 'slide-content';
slideImg.src = `${data.source_url}?slide=1`;
slideContainer.appendChild(slideImg);
wrapper.appendChild(slideContainer);
clearContent(container);
container.appendChild(wrapper);
showControls('slide');
updateSlideInfo();
}
// Load Code
function loadCode(container, data) {
const wrapper = document.createElement('div');
wrapper.className = 'projector-code';
wrapper.id = 'code-container';
if (projectorState.lineNumbers) {
wrapper.classList.add('line-numbers');
}
const pre = document.createElement('pre');
const code = document.createElement('code');
// Fetch code content
fetch(data.source_url)
.then(res => res.text())
.then(text => {
// Split into lines for line numbers
const lines = text.split('\n').map(line =>
`<span class="line">${escapeHtml(line)}</span>`
).join('\n');
code.innerHTML = lines;
// Apply syntax highlighting if Prism is available
if (window.Prism) {
Prism.highlightElement(code);
}
})
.catch(() => {
code.textContent = 'Failed to load code';
});
pre.appendChild(code);
wrapper.appendChild(pre);
clearContent(container);
container.appendChild(wrapper);
// Update code info
const filename = data.source_url.split('/').pop();
document.getElementById('code-info').textContent = filename;
showControls('code');
}
// Load Markdown
function loadMarkdown(container, data) {
const wrapper = document.createElement('div');
wrapper.className = 'projector-markdown';
fetch(data.source_url)
.then(res => res.text())
.then(text => {
// Simple markdown parsing (use marked.js in production)
wrapper.innerHTML = parseMarkdown(text);
})
.catch(() => {
wrapper.innerHTML = '<p>Failed to load markdown</p>';
});
clearContent(container);
container.appendChild(wrapper);
hideAllControls();
}
// Load Iframe
function loadIframe(container, data) {
const iframe = document.createElement('iframe');
iframe.className = 'projector-iframe';
iframe.src = data.source_url;
iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture';
iframe.allowFullscreen = true;
clearContent(container);
container.appendChild(iframe);
hideAllControls();
}
// Load Generic
function loadGeneric(container, data) {
const wrapper = document.createElement('div');
wrapper.style.textAlign = 'center';
wrapper.style.padding = '40px';
wrapper.style.color = '#888';
wrapper.innerHTML = `
<div style="font-size: 64px; margin-bottom: 20px;">📁</div>
<div style="font-size: 18px; margin-bottom: 10px;">Cannot preview this file type</div>
<a href="${data.source_url}" download style="color: #667eea; text-decoration: none;">
⬇️ Download File
</a>
`;
clearContent(container);
container.appendChild(wrapper);
hideAllControls();
}
// Show Error
function showError(message) {
const content = document.getElementById('projector-content');
content.innerHTML = `
<div class="projector-error">
<span class="projector-error-icon">❌</span>
<span class="projector-error-message">${message}</span>
</div>
`;
}
// Clear Content
function clearContent(container) {
const loading = document.getElementById('projector-loading');
container.innerHTML = '';
container.appendChild(loading);
}
// Show/Hide Controls
function showControls(type) {
hideAllControls();
const controls = document.getElementById(`${type}-controls`);
if (controls) {
controls.classList.remove('hidden');
}
}
function hideAllControls() {
document.getElementById('media-controls').classList.add('hidden');
document.getElementById('slide-controls').classList.add('hidden');
document.getElementById('image-controls').classList.add('hidden');
document.getElementById('code-controls').classList.add('hidden');
}
// Close Projector
function closeProjector() {
const overlay = document.getElementById('projector-overlay');
overlay.classList.add('hidden');
projectorState.isOpen = false;
// Stop any playing media
const media = getMediaElement();
if (media) {
media.pause();
media.src = '';
}
// Clear content
const content = document.getElementById('projector-content');
const loading = document.getElementById('projector-loading');
content.innerHTML = '';
content.appendChild(loading);
}
function closeProjectorOnOverlay(event) {
if (event.target.id === 'projector-overlay') {
closeProjector();
}
}
// Media Controls
function togglePlayPause() {
const media = getMediaElement();
if (media) {
if (media.paused) {
media.play();
} else {
media.pause();
}
}
}
function mediaSeekBack() {
const media = getMediaElement();
if (media) {
media.currentTime = Math.max(0, media.currentTime - 10);
}
}
function mediaSeekForward() {
const media = getMediaElement();
if (media) {
media.currentTime = Math.min(media.duration, media.currentTime + 10);
}
}
function seekTo(percent) {
const media = getMediaElement();
if (media && media.duration) {
media.currentTime = (percent / 100) * media.duration;
}
}
function setVolume(value) {
const media = getMediaElement();
if (media) {
media.volume = value / 100;
projectorState.isMuted = value === 0;
document.getElementById('mute-btn').textContent = value === 0 ? '🔇' : '🔊';
}
}
function toggleMute() {
const media = getMediaElement();
if (media) {
media.muted = !media.muted;
projectorState.isMuted = media.muted;
document.getElementById('mute-btn').textContent = media.muted ? '🔇' : '🔊';
}
}
function toggleLoop() {
const media = getMediaElement();
if (media) {
media.loop = !media.loop;
projectorState.isLooping = media.loop;
document.getElementById('loop-btn').classList.toggle('active', media.loop);
}
}
function setPlaybackSpeed(speed) {
const media = getMediaElement();
if (media) {
media.playbackRate = parseFloat(speed);
}
}
function updateProgress() {
const media = getMediaElement();
if (media && media.duration) {
const progress = (media.currentTime / media.duration) * 100;
document.getElementById('progress-bar').value = progress;
}
}
function updateTimeDisplay() {
const media = getMediaElement();
if (media) {
const current = formatTime(media.currentTime);
const duration = formatTime(media.duration || 0);
document.getElementById('time-display').textContent = `${current} / ${duration}`;
}
}
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')}`;
}
// Slide/Page Controls
function prevSlide() {
if (projectorState.currentSlide > 1) {
projectorState.currentSlide--;
updateSlide();
}
}
function nextSlide() {
if (projectorState.currentSlide < projectorState.totalSlides) {
projectorState.currentSlide++;
updateSlide();
}
}
function goToSlide(num) {
const slide = parseInt(num);
if (slide >= 1 && slide <= projectorState.totalSlides) {
projectorState.currentSlide = slide;
updateSlide();
}
}
function updateSlide() {
const slideContent = document.getElementById('slide-content');
if (slideContent) {
slideContent.src = `${projectorState.source}?slide=${projectorState.currentSlide}`;
}
updateSlideInfo();
}
function updateSlideInfo() {
document.getElementById('slide-info').textContent =
`Slide ${projectorState.currentSlide} of ${projectorState.totalSlides}`;
document.getElementById('slide-input').value = projectorState.currentSlide;
}
// Image Controls
function prevImage() {
if (projectorState.currentImage > 0) {
projectorState.currentImage--;
updateImage();
}
}
function nextImage() {
if (projectorState.currentImage < projectorState.totalImages - 1) {
projectorState.currentImage++;
updateImage();
}
}
function updateImage() {
// Implementation for image galleries
updateImageInfo();
}
function updateImageInfo() {
document.getElementById('image-info').textContent =
`${projectorState.currentImage + 1} of ${projectorState.totalImages}`;
}
function rotateImage() {
projectorState.rotation = (projectorState.rotation + 90) % 360;
const img = document.getElementById('projector-img');
if (img) {
img.style.transform = `rotate(${projectorState.rotation}deg) scale(${projectorState.zoom / 100})`;
}
}
function fitToScreen() {
projectorState.zoom = 100;
projectorState.rotation = 0;
const img = document.getElementById('projector-img');
if (img) {
img.style.transform = 'none';
}
document.getElementById('zoom-level').textContent = '100%';
}
// Zoom Controls
function zoomIn() {
projectorState.zoom = Math.min(300, projectorState.zoom + 25);
applyZoom();
}
function zoomOut() {
projectorState.zoom = Math.max(25, projectorState.zoom - 25);
applyZoom();
}
function applyZoom() {
const img = document.getElementById('projector-img');
const slideContainer = document.getElementById('slide-container');
if (img) {
img.style.transform = `rotate(${projectorState.rotation}deg) scale(${projectorState.zoom / 100})`;
}
if (slideContainer) {
slideContainer.style.transform = `scale(${projectorState.zoom / 100})`;
}
document.getElementById('zoom-level').textContent = `${projectorState.zoom}%`;
}
// Code Controls
function toggleLineNumbers() {
projectorState.lineNumbers = !projectorState.lineNumbers;
const container = document.getElementById('code-container');
if (container) {
container.classList.toggle('line-numbers', projectorState.lineNumbers);
}
}
function toggleWordWrap() {
projectorState.wordWrap = !projectorState.wordWrap;
const container = document.getElementById('code-container');
if (container) {
container.style.whiteSpace = projectorState.wordWrap ? 'pre-wrap' : 'pre';
}
}
function setCodeTheme(theme) {
const container = document.getElementById('code-container');
if (container) {
container.className = `projector-code ${projectorState.lineNumbers ? 'line-numbers' : ''} theme-${theme}`;
}
}
function copyCode() {
const code = document.querySelector('.projector-code code');
if (code) {
navigator.clipboard.writeText(code.textContent).then(() => {
// Show feedback
const btn = document.querySelector('.code-controls .control-btn:last-child');
const originalText = btn.textContent;
btn.textContent = '✅';
setTimeout(() => btn.textContent = originalText, 2000);
});
}
}
// Fullscreen
function toggleFullscreen() {
const container = document.querySelector('.projector-container');
const icon = document.getElementById('fullscreen-icon');
if (!document.fullscreenElement) {
container.requestFullscreen().then(() => {
container.classList.add('fullscreen');
icon.textContent = '⛶';
}).catch(() => {});
} else {
document.exitFullscreen().then(() => {
container.classList.remove('fullscreen');
icon.textContent = '⛶';
}).catch(() => {});
}
}
// Download
function downloadContent() {
const link = document.createElement('a');
link.href = projectorState.source;
link.download = '';
link.click();
}
// Share
function shareContent() {
if (navigator.share) {
navigator.share({
title: document.getElementById('projector-title').textContent,
url: projectorState.source
}).catch(() => {});
} else {
navigator.clipboard.writeText(window.location.origin + projectorState.source).then(() => {
alert('Link copied to clipboard!');
});
}
}
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (!projectorState.isOpen) return;
switch (e.key) {
case 'Escape':
closeProjector();
break;
case ' ':
e.preventDefault();
togglePlayPause();
break;
case 'ArrowLeft':
if (projectorState.contentType === 'Video' || projectorState.contentType === 'Audio') {
mediaSeekBack();
} else {
prevSlide();
}
break;
case 'ArrowRight':
if (projectorState.contentType === 'Video' || projectorState.contentType === 'Audio') {
mediaSeekForward();
} else {
nextSlide();
}
break;
case 'f':
toggleFullscreen();
break;
case 'm':
toggleMute();
break;
case '+':
case '=':
zoomIn();
break;
case '-':
zoomOut();
break;
}
});
// Helper Functions
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function parseMarkdown(text) {
// Simple markdown parsing - use marked.js for full support
return text
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
.replace(/\*\*(.*)\*\*/gim, '<strong>$1</strong>')
.replace(/\*(.*)\*/gim, '<em>$1</em>')
.replace(/`([^`]+)`/gim, '<code>$1</code>')
.replace(/\n/gim, '<br>');
}
// Listen for play messages from WebSocket
if (window.htmx) {
htmx.on('htmx:wsMessage', function(event) {
try {
const data = JSON.parse(event.detail.message);
if (data.type === 'play') {
openProjector(data.data);
} else if (data.type === 'player_command') {
switch (data.command) {
case 'stop':
closeProjector();
break;
case 'pause':
const media = getMediaElement();
if (media) media.pause();
break;
case 'resume':
const mediaR = getMediaElement();
if (mediaR) mediaR.play();
break;
}
}
} catch (e) {
// Not a projector message
}
});
}
</script>