返回 ppt-master
viewer.html
根目录 / viewer.html
1 <!DOCTYPE html>
2 <html lang="en">
3
4 <head>
5 <meta charset="UTF-8">
6 <meta name="viewport" content="width=device-width, initial-scale=1.0">
7 <meta name="description" content="PPT Master Slide Gallery — Browse and view SVG presentation slides across various topics">
8 <meta name="keywords" content="slides, SVG, presentation, PPT, PowerPoint">
9 <meta property="og:title" content="Slide Gallery — PPT Master">
10 <meta property="og:description" content="Browse example presentations — AI-generated, natively editable PowerPoint from any document.">
11 <meta property="og:url" content="https://hugohe3.github.io/ppt-master/viewer.html">
12 <meta property="og:type" content="website">
13 <title>Slide Gallery — PPT Master</title>
14
15 <!-- Tailwind CSS -->
16 <script src="https://cdn.tailwindcss.com"></script>
17 <script>
18 tailwind.config = {
19 theme: {
20 extend: {
21 colors: {
22 brand: {
23 50: '#eef3ff',
24 100: '#dde7ff',
25 200: '#c0d2ff',
26 300: '#9bb6ff',
27 400: '#7da0ff',
28 500: '#5b8def',
29 600: '#4a7be0',
30 700: '#3b65c2',
31 800: '#2f4f99',
32 900: '#243d75',
33 }
34 },
35 fontFamily: {
36 sans: ['Inter', 'system-ui', '-apple-system', 'PingFang SC', 'Microsoft YaHei', 'sans-serif'],
37 }
38 }
39 }
40 }
41 </script>
42
43 <!-- Fonts & Icons -->
44 <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
45
46 <!-- Favicon -->
47 <link rel="icon"
48 href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 32' fill='none' stroke='%235b8def' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'><rect x='2' y='2' width='44' height='28' rx='2.5'/><line x1='9' y1='11' x2='22' y2='11'/><line x1='9' y1='17' x2='32' y2='17'/><line x1='9' y1='23' x2='26' y2='23'/></svg>">
49
50 <style>
51 :root {
52 --primary: #5b8def;
53 --primary-light: #88aaff;
54 --darker: #0d0d0f;
55 --surface: #16161a;
56 --surface-hover: #1d1d20;
57 --border: #26262a;
58 --text: #e6e6e9;
59 --text-muted: #8a8a93;
60 }
61
62 body {
63 font-family: 'Inter', system-ui, -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif;
64 background: var(--darker);
65 color: var(--text);
66 min-height: 100vh;
67 -webkit-font-smoothing: antialiased;
68 -moz-osx-font-smoothing: grayscale;
69 }
70
71 /* Subtle ambient glow at the top of the page */
72 body::before {
73 content: '';
74 position: fixed;
75 top: -300px;
76 left: 50%;
77 transform: translateX(-50%);
78 width: 1200px;
79 height: 700px;
80 background: radial-gradient(circle, rgba(91, 141, 239, 0.08) 0%, transparent 60%);
81 pointer-events: none;
82 z-index: 0;
83 }
84
85 .glass {
86 background: var(--surface);
87 border: 1px solid var(--border);
88 backdrop-filter: blur(10px);
89 }
90
91 /* Override Tailwind utility colors for dark theme */
92 .text-gray-800,
93 .text-gray-700 { color: var(--text) !important; }
94 .text-gray-600,
95 .text-gray-500,
96 .text-gray-400 { color: var(--text-muted) !important; }
97 .bg-white { background: var(--surface) !important; }
98 .bg-gray-50 { background: var(--surface-hover) !important; }
99 .bg-gray-100 { background: var(--surface) !important; }
100 .border-gray-200,
101 .border-gray-100 { border-color: var(--border) !important; }
102 .hover\:bg-gray-50:hover { background: var(--surface-hover) !important; }
103 .bg-brand-50 { background: rgba(91, 141, 239, 0.08) !important; }
104 .drop-shadow-lg,
105 .drop-shadow { filter: none !important; }
106
107 kbd {
108 background: var(--surface) !important;
109 color: var(--text-muted) !important;
110 border: 1px solid var(--border) !important;
111 }
112
113 .collection-card {
114 display: block;
115 transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
116 cursor: pointer;
117 border: 1px solid var(--border);
118 color: inherit;
119 text-decoration: none;
120 }
121
122 .collection-card:hover {
123 transform: translateY(-3px);
124 border-color: #3a3a40;
125 box-shadow: 0 8px 28px rgba(0, 0, 0, 0.4);
126 }
127
128 .slide-container {
129 box-shadow: 0 12px 32px rgba(0, 0, 0, 0.4);
130 transition: transform 0.3s ease;
131 }
132
133 .thumbnail {
134 transition: transform 0.18s ease, box-shadow 0.18s ease;
135 cursor: pointer;
136 }
137
138 .thumbnail:hover {
139 transform: scale(1.04);
140 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
141 }
142
143 .nav-btn {
144 transition: transform 0.18s ease, background 0.18s ease, color 0.18s ease, border-color 0.18s ease;
145 }
146
147 .nav-btn:hover:not(:disabled) {
148 transform: scale(1.06);
149 }
150
151 .nav-btn:disabled {
152 opacity: 0.4;
153 cursor: not-allowed;
154 }
155
156 /* Custom Scrollbar */
157 ::-webkit-scrollbar {
158 width: 6px;
159 height: 6px;
160 }
161
162 ::-webkit-scrollbar-track {
163 background: var(--darker);
164 border-radius: 3px;
165 }
166
167 ::-webkit-scrollbar-thumb {
168 background: var(--border);
169 border-radius: 3px;
170 }
171
172 ::-webkit-scrollbar-thumb:hover {
173 background: #3a3a40;
174 }
175
176 .fade-in {
177 animation: fadeIn 0.3s ease-out;
178 }
179
180 @keyframes fadeIn {
181 from {
182 opacity: 0;
183 transform: scale(0.98);
184 }
185
186 to {
187 opacity: 1;
188 transform: scale(1);
189 }
190 }
191
192 .slide-enter {
193 animation: slideEnter 0.4s ease-out;
194 }
195
196 @keyframes slideEnter {
197 from {
198 opacity: 0;
199 transform: translateY(20px);
200 }
201
202 to {
203 opacity: 1;
204 transform: translateY(0);
205 }
206 }
207
208 /* Fullscreen styles */
209 .fullscreen-overlay {
210 position: fixed;
211 top: 0;
212 left: 0;
213 width: 100vw;
214 height: 100vh;
215 background: rgba(0, 0, 0, 0.95);
216 z-index: 1000;
217 display: flex;
218 align-items: center;
219 justify-content: center;
220 }
221
222 .fullscreen-overlay.hidden {
223 display: none;
224 }
225
226 .fullscreen-overlay object,
227 .fullscreen-overlay img {
228 width: 95vw;
229 height: 90vh;
230 max-width: 95vw;
231 max-height: 90vh;
232 object-fit: contain;
233 }
234
235 .progress-bar {
236 height: 4px;
237 transition: width 0.3s ease;
238 }
239
240 .dropdown-menu {
241 max-height: min(70vh, 560px);
242 overflow-x: hidden;
243 overflow-y: auto;
244 overscroll-behavior: contain;
245 }
246
247 /* Expanded view for slide viewer */
248 .viewer-expanded {
249 max-width: 95vw !important;
250 }
251
252 /* Theater mode - hide sidebar */
253 .theater-mode .sidebar-panel {
254 display: none;
255 }
256
257 .theater-mode .main-slide-area {
258 flex: 1;
259 max-width: 100%;
260 }
261
262 .theater-mode .slide-container object {
263 max-height: 75vh;
264 width: auto;
265 margin: 0 auto;
266 }
267
268 /* Better slide sizing for large screens */
269 @media (min-width: 1920px) {
270 .viewer-expanded {
271 max-width: 90vw !important;
272 }
273
274 .slide-container object {
275 max-height: 70vh;
276 }
277 }
278
279 @media (min-width: 2560px) {
280 .viewer-expanded {
281 max-width: 85vw !important;
282 }
283 }
284
285 /* View mode toggle button */
286 .view-mode-btn {
287 transition: background 0.18s ease, color 0.18s ease;
288 }
289
290 .view-mode-btn.active {
291 background: var(--primary);
292 color: #fff;
293 }
294
295 /* SVG object styling */
296 .slide-container object {
297 width: 100%;
298 height: auto;
299 min-height: 400px;
300 }
301 </style>
302 </head>
303
304 <body class="antialiased">
305 <!-- Header -->
306 <header class="glass shadow-lg sticky top-0 z-50">
307 <div class="max-w-7xl mx-auto px-4 py-4">
308 <div class="flex items-center justify-between">
309 <div class="flex items-center space-x-4">
310 <a href="index.html" class="text-gray-600 hover:text-brand-600 transition-colors">
311 <i class="fas fa-arrow-left"></i>
312 <span class="ml-2">Back to Projects</span>
313 </a>
314 </div>
315
316 <!-- Collection Selector (shown when viewing slides) -->
317 <div id="collectionSelector" class="hidden relative">
318 <button onclick="toggleDropdown()"
319 class="flex items-center space-x-2 px-4 py-2 bg-white rounded-lg shadow border border-gray-200 hover:border-brand-400 transition-colors">
320 <span id="currentCollectionIcon" class="text-xl">🎴</span>
321 <span id="currentCollectionName" class="font-medium text-gray-700">Select Collection</span>
322 <i class="fas fa-chevron-down text-gray-400"></i>
323 </button>
324 <div id="dropdownMenu"
325 class="dropdown-menu hidden absolute top-full left-0 mt-2 w-80 bg-white rounded-xl shadow-xl border border-gray-100 z-50">
326 <!-- Dropdown items will be generated by JavaScript -->
327 </div>
328 </div>
329
330 <div id="keyboardHints" class="hidden items-center space-x-2 text-sm">
331 <!-- View Mode Toggle -->
332 <div class="flex items-center space-x-1 mr-4 bg-gray-100 rounded-lg p-1">
333 <button onclick="setViewMode('normal')" id="normalModeBtn"
334 class="view-mode-btn px-3 py-1 rounded-md text-xs font-medium text-gray-600 hover:bg-white"
335 title="Normal View">
336 <i class="fas fa-columns"></i>
337 </button>
338 <button onclick="setViewMode('theater')" id="theaterModeBtn"
339 class="view-mode-btn px-3 py-1 rounded-md text-xs font-medium text-gray-600 hover:bg-white"
340 title="Theater Mode (T)">
341 <i class="fas fa-tv"></i>
342 </button>
343 </div>
344 <span class="text-gray-500">Shortcuts:</span>
345 <kbd class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded border">←</kbd>
346 <kbd class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded border">→</kbd>
347 <span class="text-gray-500">Navigate</span>
348 <kbd class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded border ml-2">T</kbd>
349 <span class="text-gray-500">Theater</span>
350 <kbd class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded border ml-2">F</kbd>
351 <span class="text-gray-500">Fullscreen</span>
352 <kbd class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded border ml-2">ESC</kbd>
353 <span class="text-gray-500">Back</span>
354 </div>
355 </div>
356 </div>
357 <!-- Progress bar -->
358 <div id="progressBar" class="progress-bar bg-brand-500" style="width: 0%"></div>
359 </header>
360
361 <!-- Main Content -->
362 <main id="mainContent" class="max-w-7xl mx-auto px-4 py-8 transition-all duration-300">
363 <!-- Library View (Collection Selection) -->
364 <div id="libraryView">
365 <div class="text-center mb-12 pt-6">
366 <h1 class="text-4xl font-bold mb-3" style="color: var(--text); letter-spacing: -0.025em;">
367 Slide Gallery
368 </h1>
369 <p style="color: var(--text-muted);" class="text-lg">
370 Select a collection to start browsing
371 </p>
372 </div>
373
374 <!-- Collection Cards Grid -->
375 <div id="collectionsGrid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
376 <!-- Cards will be generated by JavaScript -->
377 </div>
378
379 <!-- Empty State -->
380 <div id="emptyState" class="hidden text-center py-20">
381 <h3 class="text-xl font-medium" style="color: var(--text);">No slide collections found</h3>
382 <p style="color: var(--text-muted);" class="mt-2">Please add slide configurations</p>
383 </div>
384 </div>
385
386 <!-- Slide Viewer (Hidden by default) -->
387 <div id="viewerView" class="hidden">
388 <!-- Title Section -->
389 <div class="text-center mb-8 slide-enter">
390 <button onclick="backToLibrary()"
391 class="inline-flex items-center px-4 py-2 mb-4 transition-colors"
392 style="color: var(--text-muted);"
393 onmouseover="this.style.color='var(--text)'"
394 onmouseout="this.style.color='var(--text-muted)'">
395 <i class="fas fa-th-large mr-2"></i>
396 Back to Gallery
397 </button>
398 <h1 id="viewerTitle" class="text-3xl font-bold mb-2" style="color: var(--text); letter-spacing: -0.02em;">
399 Slide Title
400 </h1>
401 <p id="viewerDescription" style="color: var(--text-muted);">
402 Slide Description
403 </p>
404 </div>
405
406 <!-- Slide Viewer -->
407 <div id="slideViewerContainer" class="flex flex-col lg:flex-row gap-6 slide-enter">
408 <!-- Main Slide -->
409 <div class="main-slide-area flex-1">
410 <div class="glass rounded-2xl p-4 slide-container">
411 <object id="mainSlide" data="" type="image/svg+xml"
412 class="w-full rounded-lg fade-in cursor-pointer" onclick="toggleFullscreen()">
413 <p class="text-center text-gray-500 py-10">Loading SVG...</p>
414 </object>
415 </div>
416
417 <!-- Navigation Controls -->
418 <div class="flex items-center justify-center mt-6 space-x-4">
419 <button id="prevBtn" onclick="prevSlide()"
420 class="nav-btn w-12 h-12 rounded-full glass shadow-md flex items-center justify-center text-gray-600 border border-gray-200 hover:bg-brand-500 hover:text-white hover:border-brand-500">
421 <i class="fas fa-chevron-left"></i>
422 </button>
423
424 <div class="flex items-center space-x-2 px-6 py-2 glass rounded-full shadow-md">
425 <span id="currentPage" class="text-brand-600 font-bold">1</span>
426 <span class="text-gray-400">/</span>
427 <span id="totalPages" class="text-gray-600">1</span>
428 </div>
429
430 <button id="nextBtn" onclick="nextSlide()"
431 class="nav-btn w-12 h-12 rounded-full glass shadow-md flex items-center justify-center text-gray-600 border border-gray-200 hover:bg-brand-500 hover:text-white hover:border-brand-500">
432 <i class="fas fa-chevron-right"></i>
433 </button>
434
435 <button onclick="toggleFullscreen()"
436 class="nav-btn w-12 h-12 rounded-full glass shadow-md flex items-center justify-center text-gray-600 border border-gray-200 hover:bg-brand-500 hover:text-white hover:border-brand-500 ml-4">
437 <i class="fas fa-expand"></i>
438 </button>
439 </div>
440 </div>
441
442 <!-- Thumbnail Sidebar -->
443 <div class="sidebar-panel lg:w-64 xl:w-72">
444 <div class="glass rounded-2xl p-4 shadow-lg">
445 <h3 class="text-sm font-semibold text-gray-600 mb-3 flex items-center">
446 <i class="fas fa-th-large mr-2 text-brand-600"></i>
447 Slide Index
448 </h3>
449 <div id="thumbnailContainer" class="space-y-3 max-h-[60vh] overflow-y-auto pr-2">
450 <!-- Thumbnails will be generated by JavaScript -->
451 </div>
452 </div>
453
454 <!-- Slide Info -->
455 <div class="glass rounded-2xl p-4 shadow-lg mt-4">
456 <h3 class="text-sm font-semibold text-gray-600 mb-3 flex items-center">
457 <i class="fas fa-info-circle mr-2 text-brand-600"></i>
458 Current Slide
459 </h3>
460 <p id="slideTitle" class="text-gray-800 font-medium">Title</p>
461 <p id="slideDesc" class="text-sm text-gray-500 mt-1">Description</p>
462 </div>
463 </div>
464 </div>
465
466 <!-- Slide Overview -->
467 <div class="mt-12 slide-enter">
468 <h2 class="text-xl font-bold mb-6 flex items-center" style="color: var(--text); letter-spacing: -0.015em;">
469 <i class="fas fa-list-ul mr-3" style="color: var(--text-muted);"></i>
470 Overview
471 </h2>
472 <div id="overviewGrid" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-4">
473 <!-- Overview thumbnails will be generated by JavaScript -->
474 </div>
475 </div>
476 </div>
477 </main>
478
479 <!-- Footer -->
480 <footer class="mt-12 py-6" style="border-top: 1px solid var(--border);">
481 <div class="max-w-7xl mx-auto px-4 text-center">
482 <p class="text-sm" style="color: var(--text-muted);">
483 PPT Master © 2025–2026 · Built by <a href="https://www.hehugo.com/" target="_blank" style="color: var(--text); text-decoration: none; border-bottom: 1px solid var(--border);">Hugo He</a>
484 </p>
485 </div>
486 </footer>
487
488 <!-- Fullscreen Overlay -->
489 <div id="fullscreenOverlay" class="fullscreen-overlay hidden" onclick="toggleFullscreen()">
490 <button class="absolute top-4 right-4 text-white text-2xl hover:text-gray-300 transition-colors">
491 <i class="fas fa-times"></i>
492 </button>
493 <button onclick="event.stopPropagation(); prevSlide()"
494 class="absolute left-4 top-1/2 -translate-y-1/2 w-12 h-12 rounded-full bg-white/20 hover:bg-white/30 flex items-center justify-center text-white transition-colors">
495 <i class="fas fa-chevron-left"></i>
496 </button>
497 <object id="fullscreenImage" data="" type="image/svg+xml" class="fade-in">
498 <p>Loading SVG...</p>
499 </object>
500 <button onclick="event.stopPropagation(); nextSlide()"
501 class="absolute right-4 top-1/2 -translate-y-1/2 w-12 h-12 rounded-full bg-white/20 hover:bg-white/30 flex items-center justify-center text-white transition-colors">
502 <i class="fas fa-chevron-right"></i>
503 </button>
504 <div class="absolute bottom-4 left-1/2 -translate-x-1/2 px-4 py-2 bg-black/50 rounded-full text-white text-sm">
505 <span id="fullscreenPage">1</span> / <span id="fullscreenTotal">1</span>
506 </div>
507 </div>
508
509 <script>
510 const EXAMPLES_DATA_URL = 'examples/examples.json';
511
512 // State
513 let collections = [];
514 let currentCollection = null;
515 let currentSlide = 0;
516 let isFullscreen = false;
517 let dropdownOpen = false;
518 let viewMode = 'normal';
519
520 // Initialize
521 document.addEventListener('DOMContentLoaded', initViewer);
522
523 async function initViewer() {
524 try {
525 collections = await loadCollections();
526 renderCollections();
527
528 // Check URL for collection parameter
529 const urlParams = new URLSearchParams(window.location.search);
530 const collectionId = urlParams.get('project');
531 if (collectionId) {
532 const collection = collections.find(c => c.id === collectionId);
533 if (collection) {
534 openCollection(collection);
535 }
536 }
537 } catch (error) {
538 showLoadError(error);
539 }
540 }
541
542 async function loadCollections() {
543 const response = await fetch(EXAMPLES_DATA_URL, { cache: 'no-cache' });
544 if (!response.ok) {
545 throw new Error(`Failed to load ${EXAMPLES_DATA_URL}: ${response.status}`);
546 }
547 const data = await response.json();
548 return (data.projects || []).map(project => ({
549 ...project,
550 slides: project.slides || [],
551 cover: project.cover || project.slides?.[0]?.file || ''
552 }));
553 }
554
555 function showLoadError(error) {
556 console.error(error);
557 document.getElementById('emptyState').classList.remove('hidden');
558 document.querySelector('#emptyState h3').textContent = 'Unable to load slide collections';
559 document.querySelector('#emptyState p').textContent = 'Start a local web server or check examples/examples.json.';
560 }
561
562 // Render collection cards
563 function renderCollections() {
564 const grid = document.getElementById('collectionsGrid');
565
566 if (collections.length === 0) {
567 document.getElementById('emptyState').classList.remove('hidden');
568 return;
569 }
570
571 grid.innerHTML = collections.map(collection => {
572 const cover = collection.cover || collection.slides[0]?.file || '';
573 const coverPath = encodeURI(`examples/${collection.folder}/${cover}`);
574 const collectionUrl = `viewer.html?project=${encodeURIComponent(collection.id)}`;
575 return `
576 <a href="${collectionUrl}" data-collection-id="${collection.id}" class="collection-card rounded-xl overflow-hidden" style="background: var(--surface);">
577 <div class="relative" style="aspect-ratio: 16 / 9; background: var(--darker); border-bottom: 1px solid var(--border);">
578 <object data="${coverPath}" type="image/svg+xml" class="w-full h-full object-contain" style="pointer-events:none">
579 <div class="w-full h-full flex items-center justify-center text-3xl" style="opacity: 0.5;">${collection.icon}</div>
580 </object>
581 <div class="absolute bottom-3 right-3 px-2.5 py-1 rounded-md text-xs font-medium"
582 style="background: rgba(13, 13, 15, 0.85); color: var(--text); border: 1px solid var(--border); backdrop-filter: blur(8px);">
583 ${collection.slides.length} slides
584 </div>
585 </div>
586 <div class="p-5" style="border-left: 3px solid ${collection.color};">
587 <div class="flex items-center gap-2 mb-2">
588 <span class="text-base">${collection.icon}</span>
589 <h3 class="font-semibold text-base" style="color: var(--text); letter-spacing: -0.01em;">${collection.title}</h3>
590 </div>
591 <p class="text-sm line-clamp-2" style="color: var(--text-muted); line-height: 1.55;">${collection.description}</p>
592 </div>
593 </a>
594 `}).join('');
595
596 grid.querySelectorAll('.collection-card').forEach(card => {
597 card.addEventListener('click', handleCollectionCardClick);
598 });
599
600 renderDropdown();
601 }
602
603 function handleCollectionCardClick(event) {
604 if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
605 return;
606 }
607
608 event.preventDefault();
609 const collection = collections.find(item => item.id === event.currentTarget.dataset.collectionId);
610 if (collection) {
611 openCollection(collection);
612 }
613 }
614
615 // Render dropdown menu
616 function renderDropdown() {
617 const dropdown = document.getElementById('dropdownMenu');
618 dropdown.innerHTML = collections.map(collection => `
619 <div class="flex items-center px-4 py-3 hover:bg-gray-50 cursor-pointer transition-colors ${currentCollection?.id === collection.id ? 'bg-brand-50' : ''}"
620 onclick='switchCollection(${JSON.stringify(collection).replace(/'/g, "&#39;")})'>
621 <span class="text-2xl mr-3">${collection.icon}</span>
622 <div class="flex-1 min-w-0">
623 <p class="font-medium text-gray-800 truncate">${collection.title}</p>
624 <p class="text-xs text-gray-500">${collection.slides.length} slides</p>
625 </div>
626 ${currentCollection?.id === collection.id ? '<i class="fas fa-check text-brand-500"></i>' : ''}
627 </div>
628 `).join('');
629 }
630
631 // Toggle dropdown
632 function toggleDropdown() {
633 dropdownOpen = !dropdownOpen;
634 document.getElementById('dropdownMenu').classList.toggle('hidden', !dropdownOpen);
635 }
636
637 // Close dropdown when clicking outside
638 document.addEventListener('click', (e) => {
639 if (!e.target.closest('#collectionSelector')) {
640 dropdownOpen = false;
641 document.getElementById('dropdownMenu').classList.add('hidden');
642 }
643 });
644
645 // Open a collection
646 function openCollection(collection) {
647 currentCollection = collection;
648 currentSlide = 0;
649
650 // Update URL
651 const url = new URL(window.location);
652 url.searchParams.set('project', collection.id);
653 window.history.pushState({}, '', url);
654
655 // Update UI
656 document.getElementById('libraryView').classList.add('hidden');
657 document.getElementById('viewerView').classList.remove('hidden');
658 document.getElementById('collectionSelector').classList.remove('hidden');
659 document.getElementById('keyboardHints').classList.remove('hidden');
660 document.getElementById('keyboardHints').classList.add('flex');
661 document.getElementById('mainContent').classList.add('viewer-expanded');
662
663 setViewMode('normal');
664
665 // Update header
666 document.getElementById('currentCollectionIcon').textContent = collection.icon;
667 document.getElementById('currentCollectionName').textContent = collection.title;
668
669 // Update viewer
670 document.getElementById('viewerTitle').innerHTML = `${collection.icon} ${collection.title}`;
671 document.getElementById('viewerDescription').textContent = collection.description;
672 document.getElementById('totalPages').textContent = collection.slides.length;
673 document.getElementById('fullscreenTotal').textContent = collection.slides.length;
674
675 // Set theme color
676 document.getElementById('progressBar').style.background = `linear-gradient(90deg, ${collection.color} 0%, ${collection.color}cc 100%)`;
677
678 generateThumbnails();
679 generateOverview();
680 updateSlide();
681 renderDropdown();
682
683 window.scrollTo({ top: 0, behavior: 'smooth' });
684 }
685
686 // Switch collection from dropdown
687 function switchCollection(collection) {
688 toggleDropdown();
689 openCollection(collection);
690 }
691
692 // Back to library
693 function backToLibrary() {
694 currentCollection = null;
695
696 const url = new URL(window.location);
697 url.searchParams.delete('project');
698 window.history.pushState({}, '', url);
699
700 document.getElementById('libraryView').classList.remove('hidden');
701 document.getElementById('viewerView').classList.add('hidden');
702 document.getElementById('collectionSelector').classList.add('hidden');
703 document.getElementById('keyboardHints').classList.add('hidden');
704 document.getElementById('keyboardHints').classList.remove('flex');
705 document.getElementById('progressBar').style.width = '0%';
706 document.getElementById('mainContent').classList.remove('viewer-expanded');
707 document.getElementById('slideViewerContainer').classList.remove('theater-mode');
708
709 window.scrollTo({ top: 0, behavior: 'smooth' });
710 }
711
712 // Set view mode
713 function setViewMode(mode) {
714 viewMode = mode;
715 const container = document.getElementById('slideViewerContainer');
716 const normalBtn = document.getElementById('normalModeBtn');
717 const theaterBtn = document.getElementById('theaterModeBtn');
718
719 if (mode === 'theater') {
720 container.classList.add('theater-mode');
721 normalBtn.classList.remove('active');
722 theaterBtn.classList.add('active');
723 } else {
724 container.classList.remove('theater-mode');
725 normalBtn.classList.add('active');
726 theaterBtn.classList.remove('active');
727 }
728 }
729
730 function toggleViewMode() {
731 setViewMode(viewMode === 'normal' ? 'theater' : 'normal');
732 }
733
734 // Generate thumbnail sidebar
735 function generateThumbnails() {
736 if (!currentCollection) return;
737
738 const container = document.getElementById('thumbnailContainer');
739 const basePath = `examples/${currentCollection.folder}/`;
740
741 container.innerHTML = currentCollection.slides.map((slide, index) => {
742 const slidePath = encodeURI(basePath + slide.file);
743 return `
744 <div class="thumbnail rounded-lg overflow-hidden border-2 ${index === 0 ? 'border-brand-500' : 'border-transparent'}"
745 onclick="goToSlide(${index})"
746 id="thumb-${index}">
747 <object data="${slidePath}" type="image/svg+xml" class="w-full" style="pointer-events:none">
748 <div class="w-full h-16 flex items-center justify-center bg-gray-100">📄</div>
749 </object>
750 <div class="px-2 py-1 bg-gray-50 text-xs text-gray-600 truncate">
751 ${index + 1}. ${slide.title}
752 </div>
753 </div>
754 `}).join('');
755 }
756
757 // Generate overview grid
758 function generateOverview() {
759 if (!currentCollection) return;
760
761 const container = document.getElementById('overviewGrid');
762 const basePath = `examples/${currentCollection.folder}/`;
763
764 container.innerHTML = currentCollection.slides.map((slide, index) => {
765 const slidePath = encodeURI(basePath + slide.file);
766 return `
767 <div class="glass rounded-xl p-2 shadow-md hover:shadow-lg transition-shadow cursor-pointer"
768 onclick="goToSlide(${index}); window.scrollTo({top: 0, behavior: 'smooth'})">
769 <object data="${slidePath}" type="image/svg+xml" class="w-full rounded-lg" style="pointer-events:none">
770 <div class="w-full h-24 flex items-center justify-center bg-gray-100">📄</div>
771 </object>
772 <div class="mt-2 px-1">
773 <p class="text-sm font-medium text-gray-800 truncate">${index + 1}. ${slide.title}</p>
774 <p class="text-xs text-gray-500 truncate">${slide.desc}</p>
775 </div>
776 </div>
777 `}).join('');
778 }
779
780 // Update slide display
781 function updateSlide() {
782 if (!currentCollection) return;
783
784 const slide = currentCollection.slides[currentSlide];
785 const basePath = `examples/${currentCollection.folder}/`;
786 const slidePath = encodeURI(basePath + slide.file);
787
788 const mainSlide = document.getElementById('mainSlide');
789 const fullscreenImage = document.getElementById('fullscreenImage');
790
791 // Update main slide
792 mainSlide.classList.remove('fade-in');
793 void mainSlide.offsetWidth;
794 mainSlide.classList.add('fade-in');
795 mainSlide.data = slidePath;
796
797 // Update fullscreen
798 fullscreenImage.data = slidePath;
799
800 // Update page numbers
801 document.getElementById('currentPage').textContent = currentSlide + 1;
802 document.getElementById('fullscreenPage').textContent = currentSlide + 1;
803
804 // Update slide info
805 document.getElementById('slideTitle').textContent = slide.title;
806 document.getElementById('slideDesc').textContent = slide.desc;
807
808 // Update progress bar
809 const progress = ((currentSlide + 1) / currentCollection.slides.length) * 100;
810 document.getElementById('progressBar').style.width = progress + '%';
811
812 // Update navigation buttons
813 document.getElementById('prevBtn').disabled = currentSlide === 0;
814 document.getElementById('nextBtn').disabled = currentSlide === currentCollection.slides.length - 1;
815
816 // Update thumbnail highlights
817 currentCollection.slides.forEach((_, i) => {
818 const thumb = document.getElementById(`thumb-${i}`);
819 if (thumb) {
820 if (i === currentSlide) {
821 thumb.classList.add('border-brand-500');
822 thumb.classList.remove('border-transparent');
823 thumb.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
824 } else {
825 thumb.classList.remove('border-brand-500');
826 thumb.classList.add('border-transparent');
827 }
828 }
829 });
830 }
831
832 // Navigation
833 function nextSlide() {
834 if (currentCollection && currentSlide < currentCollection.slides.length - 1) {
835 currentSlide++;
836 updateSlide();
837 }
838 }
839
840 function prevSlide() {
841 if (currentSlide > 0) {
842 currentSlide--;
843 updateSlide();
844 }
845 }
846
847 function goToSlide(index) {
848 currentSlide = index;
849 updateSlide();
850 }
851
852 // Fullscreen toggle
853 function toggleFullscreen() {
854 const overlay = document.getElementById('fullscreenOverlay');
855 isFullscreen = !isFullscreen;
856
857 if (isFullscreen) {
858 overlay.classList.remove('hidden');
859 document.body.style.overflow = 'hidden';
860 } else {
861 overlay.classList.add('hidden');
862 document.body.style.overflow = '';
863 }
864 }
865
866 // Keyboard navigation
867 document.addEventListener('keydown', (e) => {
868 if (!currentCollection && e.key !== 'Escape') return;
869
870 switch (e.key) {
871 case 'ArrowLeft':
872 prevSlide();
873 break;
874 case 'ArrowRight':
875 nextSlide();
876 break;
877 case 'Escape':
878 if (isFullscreen) {
879 toggleFullscreen();
880 } else if (currentCollection) {
881 backToLibrary();
882 }
883 break;
884 case 'f':
885 case 'F':
886 if (currentCollection) toggleFullscreen();
887 break;
888 case 't':
889 case 'T':
890 if (currentCollection && !isFullscreen) toggleViewMode();
891 break;
892 }
893 });
894
895 // Handle browser back/forward
896 window.addEventListener('popstate', () => {
897 const urlParams = new URLSearchParams(window.location.search);
898 const collectionId = urlParams.get('project');
899
900 if (collectionId) {
901 const collection = collections.find(c => c.id === collectionId);
902 if (collection) {
903 openCollection(collection);
904 }
905 } else {
906 backToLibrary();
907 }
908 });
909
910 // Touch/swipe support
911 let touchStartX = 0;
912 let touchEndX = 0;
913
914 document.addEventListener('touchstart', (e) => {
915 touchStartX = e.changedTouches[0].screenX;
916 });
917
918 document.addEventListener('touchend', (e) => {
919 touchEndX = e.changedTouches[0].screenX;
920 const diff = touchStartX - touchEndX;
921
922 if (Math.abs(diff) > 50 && currentCollection) {
923 if (diff > 0) nextSlide();
924 else prevSlide();
925 }
926 });
927 </script>
928 </body>
929
930 </html>
931
931 lines HTML