ismail c5f76b3855
Some checks are pending
Build and Push Docker Image / build (push) Waiting to run
updates
2026-05-11 14:45:30 +03:00

464 lines
17 KiB
JavaScript

class PXSlideViewer {
constructor(container, options = {}) {
this.container = container;
this.slides = [];
this.currentIndex = 0;
this.isPlaying = false;
this.speakerNotesOpen = false;
this.thumbnailOpen = false;
this.transitionType = options.transition || 'fade';
this.autoplayDelay = options.autoplayDelay || 8000;
this._options = options;
this._init();
}
_init() {
var opts = this._options || {};
this.slides = Array.from(this.container.querySelectorAll('.viewer-slide'));
if (this.slides.length === 0 && opts.slides && opts.slides.length) {
this.slides = opts.slides;
}
if (this.slides.length === 0) return;
this._setupHTML();
this._setupStyles();
this._setupEvents();
this.goTo(0);
}
_setupHTML() {
this.container.classList.add('px-viewer');
this.container.innerHTML = '';
const stage = document.createElement('div');
stage.className = 'px-viewer__stage';
this.stageEl = stage;
this.slides.forEach((slide, idx) => {
const wrapper = document.createElement('div');
wrapper.className = 'px-viewer__slide';
wrapper.dataset.index = idx;
wrapper.innerHTML = slide.innerHTML || slide.html || '';
stage.appendChild(wrapper);
});
this.slideEls = stage.querySelectorAll('.px-viewer__slide');
const controls = document.createElement('div');
controls.className = 'px-viewer__controls';
controls.innerHTML = `
<div class="px-viewer__progress">
<div class="px-viewer__progress-fill"></div>
</div>
<div class="px-viewer__bar">
<div class="px-viewer__bar-left">
<button class="px-viewer__btn" data-action="thumbnails" title="Thumbnails">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
</button>
<button class="px-viewer__btn" data-action="notes" title="Speaker Notes">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
</button>
</div>
<div class="px-viewer__nav">
<button class="px-viewer__btn" data-action="prev" title="Previous (←)">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>
</button>
<span class="px-viewer__counter">1 / ${this.slides.length}</span>
<button class="px-viewer__btn" data-action="next" title="Next (→)">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
</button>
</div>
<div class="px-viewer__bar-right">
<button class="px-viewer__btn" data-action="fullscreen" title="Fullscreen (F)">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>
</button>
<button class="px-viewer__btn" data-action="exit" title="Exit (Esc)">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
</div>
`;
this.progressFill = controls.querySelector('.px-viewer__progress-fill');
this.counterEl = controls.querySelector('.px-viewer__counter');
const notesPanel = document.createElement('div');
notesPanel.className = 'px-viewer__notes';
notesPanel.innerHTML = '<div class="px-viewer__notes-content"></div>';
this.notesEl = notesPanel.querySelector('.px-viewer__notes-content');
const thumbPanel = document.createElement('div');
thumbPanel.className = 'px-viewer__thumbs';
this.thumbsEl = thumbPanel;
this.slides.forEach((slide, idx) => {
const thumb = document.createElement('div');
thumb.className = 'px-viewer__thumb';
thumb.dataset.index = idx;
const title = slide.title || `Slide ${idx + 1}`;
const layout = slide.layout || '';
thumb.innerHTML = `
<div class="px-viewer__thumb-number">${idx + 1}</div>
<div class="px-viewer__thumb-info">
<div class="px-viewer__thumb-title">${title}</div>
<div class="px-viewer__thumb-layout">${layout.replace(/_/g, ' ')}</div>
</div>
`;
thumb.addEventListener('click', () => this.goTo(idx));
thumbPanel.appendChild(thumb);
});
this.container.appendChild(stage);
this.container.appendChild(notesPanel);
this.container.appendChild(thumbPanel);
this.container.appendChild(controls);
}
_setupStyles() {
if (document.getElementById('px-viewer-styles')) return;
const style = document.createElement('style');
style.id = 'px-viewer-styles';
style.textContent = `
.px-viewer {
position: fixed;
inset: 0;
background: #0a0a0a;
z-index: 9999;
display: flex;
flex-direction: column;
font-family: 'Inter', sans-serif;
}
.px-viewer__stage {
flex: 1;
position: relative;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.px-viewer__slide {
position: absolute;
top: 0;
left: 0;
width: 1920px;
height: 1080px;
transform-origin: top left;
opacity: 0;
pointer-events: none;
transition: opacity 0.5s ease, transform 0.5s ease;
}
.px-viewer__slide.active {
opacity: 1;
pointer-events: auto;
z-index: 1;
}
.px-viewer__slide.slide-enter {
opacity: 0;
transform: scale(0.97);
}
.px-viewer__slide.active.slide-enter {
opacity: 1;
transform: scale(1);
}
.px-viewer__progress {
height: 3px;
background: rgba(255,255,255,0.1);
position: relative;
}
.px-viewer__progress-fill {
height: 100%;
background: linear-gradient(to right, #005696, #0EA5E9);
transition: width 0.4s ease;
border-radius: 0 2px 2px 0;
}
.px-viewer__bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 20px;
background: rgba(15,15,15,0.95);
backdrop-filter: blur(10px);
}
.px-viewer__bar-left, .px-viewer__bar-right {
display: flex;
gap: 4px;
}
.px-viewer__nav {
display: flex;
align-items: center;
gap: 12px;
}
.px-viewer__btn {
background: none;
border: none;
color: rgba(255,255,255,0.6);
cursor: pointer;
padding: 8px;
border-radius: 8px;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.px-viewer__btn:hover {
color: #fff;
background: rgba(255,255,255,0.1);
}
.px-viewer__counter {
color: rgba(255,255,255,0.5);
font-size: 13px;
font-variant-numeric: tabular-nums;
min-width: 60px;
text-align: center;
user-select: none;
}
.px-viewer__controls {
position: relative;
z-index: 10;
}
.px-viewer__notes {
position: fixed;
bottom: 60px;
right: 0;
width: 400px;
max-height: 50vh;
background: rgba(15,15,15,0.95);
backdrop-filter: blur(10px);
border-left: 1px solid rgba(255,255,255,0.1);
border-top: 1px solid rgba(255,255,255,0.1);
border-radius: 16px 0 0 0;
padding: 24px;
color: rgba(255,255,255,0.8);
font-size: 14px;
line-height: 1.7;
overflow-y: auto;
transform: translateX(100%);
transition: transform 0.3s ease;
z-index: 20;
}
.px-viewer__notes.open {
transform: translateX(0);
}
.px-viewer__notes-content {
white-space: pre-wrap;
}
.px-viewer__thumbs {
position: fixed;
left: 0;
top: 0;
bottom: 60px;
width: 280px;
background: rgba(15,15,15,0.95);
backdrop-filter: blur(10px);
border-right: 1px solid rgba(255,255,255,0.1);
padding: 20px 16px;
overflow-y: auto;
transform: translateX(-100%);
transition: transform 0.3s ease;
z-index: 20;
display: flex;
flex-direction: column;
gap: 8px;
}
.px-viewer__thumbs.open {
transform: translateX(0);
}
.px-viewer__thumb {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border-radius: 10px;
cursor: pointer;
transition: background 0.2s;
border: 2px solid transparent;
}
.px-viewer__thumb:hover {
background: rgba(255,255,255,0.05);
}
.px-viewer__thumb.active {
background: rgba(14,165,233,0.15);
border-color: rgba(14,165,233,0.4);
}
.px-viewer__thumb-number {
width: 32px;
height: 32px;
border-radius: 8px;
background: rgba(255,255,255,0.1);
color: rgba(255,255,255,0.5);
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 600;
flex-shrink: 0;
}
.px-viewer__thumb.active .px-viewer__thumb-number {
background: rgba(14,165,233,0.3);
color: #0EA5E9;
}
.px-viewer__thumb-title {
font-size: 13px;
font-weight: 500;
color: rgba(255,255,255,0.7);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.px-viewer__thumb-layout {
font-size: 11px;
color: rgba(255,255,255,0.35);
text-transform: capitalize;
}
`;
document.head.appendChild(style);
}
_setupEvents() {
document.addEventListener('keydown', (e) => {
if (e.target.matches('input, textarea, [contenteditable]')) return;
switch (e.key) {
case 'ArrowRight':
case ' ':
case 'PageDown':
e.preventDefault();
this.next();
break;
case 'ArrowLeft':
case 'PageUp':
e.preventDefault();
this.prev();
break;
case 'Home':
e.preventDefault();
this.goTo(0);
break;
case 'End':
e.preventDefault();
this.goTo(this.slides.length - 1);
break;
case 'Escape':
e.preventDefault();
this.destroy();
break;
case 'f':
case 'F':
e.preventDefault();
this.toggleFullscreen();
break;
case 'n':
case 'N':
e.preventDefault();
this.toggleNotes();
break;
case 't':
case 'T':
e.preventDefault();
this.toggleThumbnails();
break;
}
});
this.container.addEventListener('click', (e) => {
const btn = e.target.closest('[data-action]');
if (!btn) return;
const action = btn.dataset.action;
switch (action) {
case 'prev': this.prev(); break;
case 'next': this.next(); break;
case 'fullscreen': this.toggleFullscreen(); break;
case 'exit': this.destroy(); break;
case 'notes': this.toggleNotes(); break;
case 'thumbnails': this.toggleThumbnails(); break;
}
});
window.addEventListener('resize', () => this._scale());
}
_scale() {
const slide = this.stageEl.querySelector('.px-viewer__slide.active');
if (!slide) return;
const vw = this.stageEl.clientWidth;
const vh = this.stageEl.clientHeight;
const scale = Math.min(vw / 1920, vh / 1080);
const offsetX = (vw - 1920 * scale) / 2;
const offsetY = (vh - 1080 * scale) / 2;
this.slideEls.forEach(el => {
el.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale})`;
});
}
goTo(index) {
if (index < 0 || index >= this.slides.length) return;
this.slideEls.forEach((el, i) => {
el.classList.remove('active', 'slide-enter');
if (i === index) {
el.classList.add('active', 'slide-enter');
}
});
this.currentIndex = index;
this.counterEl.textContent = `${index + 1} / ${this.slides.length}`;
this.progressFill.style.width = `${((index + 1) / this.slides.length) * 100}%`;
this.notesEl.textContent = this.slides[index]?.notes || 'No speaker notes for this slide.';
this.thumbsEl.querySelectorAll('.px-viewer__thumb').forEach((t, i) => {
t.classList.toggle('active', i === index);
});
this._scale();
}
next() {
if (this.currentIndex < this.slides.length - 1) {
this.goTo(this.currentIndex + 1);
}
}
prev() {
if (this.currentIndex > 0) {
this.goTo(this.currentIndex - 1);
}
}
toggleFullscreen() {
if (!document.fullscreenElement) {
this.container.requestFullscreen();
} else {
document.exitFullscreen();
}
}
toggleNotes() {
this.speakerNotesOpen = !this.speakerNotesOpen;
this.container.querySelector('.px-viewer__notes').classList.toggle('open', this.speakerNotesOpen);
}
toggleThumbnails() {
this.thumbnailOpen = !this.thumbnailOpen;
this.container.querySelector('.px-viewer__thumbs').classList.toggle('open', this.thumbnailOpen);
if (this.thumbnailOpen) {
setTimeout(() => this._scale(), 350);
}
}
destroy() {
if (document.fullscreenElement) {
document.exitFullscreen();
}
this.container.remove();
if (window._pxViewerCleanup) window._pxViewerCleanup();
}
}
window.PXSlideViewer = PXSlideViewer;