931 lines
38 KiB
HTML
931 lines
38 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<meta name="description" content="PPT Master Slide Gallery — Browse and view SVG presentation slides across various topics">
|
||
<meta name="keywords" content="slides, SVG, presentation, PPT, PowerPoint">
|
||
<meta property="og:title" content="Slide Gallery — PPT Master">
|
||
<meta property="og:description" content="Browse example presentations — AI-generated, natively editable PowerPoint from any document.">
|
||
<meta property="og:url" content="https://hugohe3.github.io/ppt-master/viewer.html">
|
||
<meta property="og:type" content="website">
|
||
<title>Slide Gallery — PPT Master</title>
|
||
|
||
<!-- Tailwind CSS -->
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
<script>
|
||
tailwind.config = {
|
||
theme: {
|
||
extend: {
|
||
colors: {
|
||
brand: {
|
||
50: '#eef3ff',
|
||
100: '#dde7ff',
|
||
200: '#c0d2ff',
|
||
300: '#9bb6ff',
|
||
400: '#7da0ff',
|
||
500: '#5b8def',
|
||
600: '#4a7be0',
|
||
700: '#3b65c2',
|
||
800: '#2f4f99',
|
||
900: '#243d75',
|
||
}
|
||
},
|
||
fontFamily: {
|
||
sans: ['Inter', 'system-ui', '-apple-system', 'PingFang SC', 'Microsoft YaHei', 'sans-serif'],
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<!-- Fonts & Icons -->
|
||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||
|
||
<!-- Favicon -->
|
||
<link rel="icon"
|
||
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>">
|
||
|
||
<style>
|
||
:root {
|
||
--primary: #5b8def;
|
||
--primary-light: #88aaff;
|
||
--darker: #0d0d0f;
|
||
--surface: #16161a;
|
||
--surface-hover: #1d1d20;
|
||
--border: #26262a;
|
||
--text: #e6e6e9;
|
||
--text-muted: #8a8a93;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Inter', system-ui, -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||
background: var(--darker);
|
||
color: var(--text);
|
||
min-height: 100vh;
|
||
-webkit-font-smoothing: antialiased;
|
||
-moz-osx-font-smoothing: grayscale;
|
||
}
|
||
|
||
/* Subtle ambient glow at the top of the page */
|
||
body::before {
|
||
content: '';
|
||
position: fixed;
|
||
top: -300px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 1200px;
|
||
height: 700px;
|
||
background: radial-gradient(circle, rgba(91, 141, 239, 0.08) 0%, transparent 60%);
|
||
pointer-events: none;
|
||
z-index: 0;
|
||
}
|
||
|
||
.glass {
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
backdrop-filter: blur(10px);
|
||
}
|
||
|
||
/* Override Tailwind utility colors for dark theme */
|
||
.text-gray-800,
|
||
.text-gray-700 { color: var(--text) !important; }
|
||
.text-gray-600,
|
||
.text-gray-500,
|
||
.text-gray-400 { color: var(--text-muted) !important; }
|
||
.bg-white { background: var(--surface) !important; }
|
||
.bg-gray-50 { background: var(--surface-hover) !important; }
|
||
.bg-gray-100 { background: var(--surface) !important; }
|
||
.border-gray-200,
|
||
.border-gray-100 { border-color: var(--border) !important; }
|
||
.hover\:bg-gray-50:hover { background: var(--surface-hover) !important; }
|
||
.bg-brand-50 { background: rgba(91, 141, 239, 0.08) !important; }
|
||
.drop-shadow-lg,
|
||
.drop-shadow { filter: none !important; }
|
||
|
||
kbd {
|
||
background: var(--surface) !important;
|
||
color: var(--text-muted) !important;
|
||
border: 1px solid var(--border) !important;
|
||
}
|
||
|
||
.collection-card {
|
||
display: block;
|
||
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
||
cursor: pointer;
|
||
border: 1px solid var(--border);
|
||
color: inherit;
|
||
text-decoration: none;
|
||
}
|
||
|
||
.collection-card:hover {
|
||
transform: translateY(-3px);
|
||
border-color: #3a3a40;
|
||
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.4);
|
||
}
|
||
|
||
.slide-container {
|
||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.4);
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
.thumbnail {
|
||
transition: transform 0.18s ease, box-shadow 0.18s ease;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.thumbnail:hover {
|
||
transform: scale(1.04);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||
}
|
||
|
||
.nav-btn {
|
||
transition: transform 0.18s ease, background 0.18s ease, color 0.18s ease, border-color 0.18s ease;
|
||
}
|
||
|
||
.nav-btn:hover:not(:disabled) {
|
||
transform: scale(1.06);
|
||
}
|
||
|
||
.nav-btn:disabled {
|
||
opacity: 0.4;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
/* Custom Scrollbar */
|
||
::-webkit-scrollbar {
|
||
width: 6px;
|
||
height: 6px;
|
||
}
|
||
|
||
::-webkit-scrollbar-track {
|
||
background: var(--darker);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb {
|
||
background: var(--border);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb:hover {
|
||
background: #3a3a40;
|
||
}
|
||
|
||
.fade-in {
|
||
animation: fadeIn 0.3s ease-out;
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: scale(0.98);
|
||
}
|
||
|
||
to {
|
||
opacity: 1;
|
||
transform: scale(1);
|
||
}
|
||
}
|
||
|
||
.slide-enter {
|
||
animation: slideEnter 0.4s ease-out;
|
||
}
|
||
|
||
@keyframes slideEnter {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(20px);
|
||
}
|
||
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
/* Fullscreen styles */
|
||
.fullscreen-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100vw;
|
||
height: 100vh;
|
||
background: rgba(0, 0, 0, 0.95);
|
||
z-index: 1000;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.fullscreen-overlay.hidden {
|
||
display: none;
|
||
}
|
||
|
||
.fullscreen-overlay object,
|
||
.fullscreen-overlay img {
|
||
width: 95vw;
|
||
height: 90vh;
|
||
max-width: 95vw;
|
||
max-height: 90vh;
|
||
object-fit: contain;
|
||
}
|
||
|
||
.progress-bar {
|
||
height: 4px;
|
||
transition: width 0.3s ease;
|
||
}
|
||
|
||
.dropdown-menu {
|
||
max-height: min(70vh, 560px);
|
||
overflow-x: hidden;
|
||
overflow-y: auto;
|
||
overscroll-behavior: contain;
|
||
}
|
||
|
||
/* Expanded view for slide viewer */
|
||
.viewer-expanded {
|
||
max-width: 95vw !important;
|
||
}
|
||
|
||
/* Theater mode - hide sidebar */
|
||
.theater-mode .sidebar-panel {
|
||
display: none;
|
||
}
|
||
|
||
.theater-mode .main-slide-area {
|
||
flex: 1;
|
||
max-width: 100%;
|
||
}
|
||
|
||
.theater-mode .slide-container object {
|
||
max-height: 75vh;
|
||
width: auto;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
/* Better slide sizing for large screens */
|
||
@media (min-width: 1920px) {
|
||
.viewer-expanded {
|
||
max-width: 90vw !important;
|
||
}
|
||
|
||
.slide-container object {
|
||
max-height: 70vh;
|
||
}
|
||
}
|
||
|
||
@media (min-width: 2560px) {
|
||
.viewer-expanded {
|
||
max-width: 85vw !important;
|
||
}
|
||
}
|
||
|
||
/* View mode toggle button */
|
||
.view-mode-btn {
|
||
transition: background 0.18s ease, color 0.18s ease;
|
||
}
|
||
|
||
.view-mode-btn.active {
|
||
background: var(--primary);
|
||
color: #fff;
|
||
}
|
||
|
||
/* SVG object styling */
|
||
.slide-container object {
|
||
width: 100%;
|
||
height: auto;
|
||
min-height: 400px;
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body class="antialiased">
|
||
<!-- Header -->
|
||
<header class="glass shadow-lg sticky top-0 z-50">
|
||
<div class="max-w-7xl mx-auto px-4 py-4">
|
||
<div class="flex items-center justify-between">
|
||
<div class="flex items-center space-x-4">
|
||
<a href="index.html" class="text-gray-600 hover:text-brand-600 transition-colors">
|
||
<i class="fas fa-arrow-left"></i>
|
||
<span class="ml-2">Back to Projects</span>
|
||
</a>
|
||
</div>
|
||
|
||
<!-- Collection Selector (shown when viewing slides) -->
|
||
<div id="collectionSelector" class="hidden relative">
|
||
<button onclick="toggleDropdown()"
|
||
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">
|
||
<span id="currentCollectionIcon" class="text-xl">🎴</span>
|
||
<span id="currentCollectionName" class="font-medium text-gray-700">Select Collection</span>
|
||
<i class="fas fa-chevron-down text-gray-400"></i>
|
||
</button>
|
||
<div id="dropdownMenu"
|
||
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">
|
||
<!-- Dropdown items will be generated by JavaScript -->
|
||
</div>
|
||
</div>
|
||
|
||
<div id="keyboardHints" class="hidden items-center space-x-2 text-sm">
|
||
<!-- View Mode Toggle -->
|
||
<div class="flex items-center space-x-1 mr-4 bg-gray-100 rounded-lg p-1">
|
||
<button onclick="setViewMode('normal')" id="normalModeBtn"
|
||
class="view-mode-btn px-3 py-1 rounded-md text-xs font-medium text-gray-600 hover:bg-white"
|
||
title="Normal View">
|
||
<i class="fas fa-columns"></i>
|
||
</button>
|
||
<button onclick="setViewMode('theater')" id="theaterModeBtn"
|
||
class="view-mode-btn px-3 py-1 rounded-md text-xs font-medium text-gray-600 hover:bg-white"
|
||
title="Theater Mode (T)">
|
||
<i class="fas fa-tv"></i>
|
||
</button>
|
||
</div>
|
||
<span class="text-gray-500">Shortcuts:</span>
|
||
<kbd class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded border">←</kbd>
|
||
<kbd class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded border">→</kbd>
|
||
<span class="text-gray-500">Navigate</span>
|
||
<kbd class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded border ml-2">T</kbd>
|
||
<span class="text-gray-500">Theater</span>
|
||
<kbd class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded border ml-2">F</kbd>
|
||
<span class="text-gray-500">Fullscreen</span>
|
||
<kbd class="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded border ml-2">ESC</kbd>
|
||
<span class="text-gray-500">Back</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- Progress bar -->
|
||
<div id="progressBar" class="progress-bar bg-brand-500" style="width: 0%"></div>
|
||
</header>
|
||
|
||
<!-- Main Content -->
|
||
<main id="mainContent" class="max-w-7xl mx-auto px-4 py-8 transition-all duration-300">
|
||
<!-- Library View (Collection Selection) -->
|
||
<div id="libraryView">
|
||
<div class="text-center mb-12 pt-6">
|
||
<h1 class="text-4xl font-bold mb-3" style="color: var(--text); letter-spacing: -0.025em;">
|
||
Slide Gallery
|
||
</h1>
|
||
<p style="color: var(--text-muted);" class="text-lg">
|
||
Select a collection to start browsing
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Collection Cards Grid -->
|
||
<div id="collectionsGrid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||
<!-- Cards will be generated by JavaScript -->
|
||
</div>
|
||
|
||
<!-- Empty State -->
|
||
<div id="emptyState" class="hidden text-center py-20">
|
||
<h3 class="text-xl font-medium" style="color: var(--text);">No slide collections found</h3>
|
||
<p style="color: var(--text-muted);" class="mt-2">Please add slide configurations</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Slide Viewer (Hidden by default) -->
|
||
<div id="viewerView" class="hidden">
|
||
<!-- Title Section -->
|
||
<div class="text-center mb-8 slide-enter">
|
||
<button onclick="backToLibrary()"
|
||
class="inline-flex items-center px-4 py-2 mb-4 transition-colors"
|
||
style="color: var(--text-muted);"
|
||
onmouseover="this.style.color='var(--text)'"
|
||
onmouseout="this.style.color='var(--text-muted)'">
|
||
<i class="fas fa-th-large mr-2"></i>
|
||
Back to Gallery
|
||
</button>
|
||
<h1 id="viewerTitle" class="text-3xl font-bold mb-2" style="color: var(--text); letter-spacing: -0.02em;">
|
||
Slide Title
|
||
</h1>
|
||
<p id="viewerDescription" style="color: var(--text-muted);">
|
||
Slide Description
|
||
</p>
|
||
</div>
|
||
|
||
<!-- Slide Viewer -->
|
||
<div id="slideViewerContainer" class="flex flex-col lg:flex-row gap-6 slide-enter">
|
||
<!-- Main Slide -->
|
||
<div class="main-slide-area flex-1">
|
||
<div class="glass rounded-2xl p-4 slide-container">
|
||
<object id="mainSlide" data="" type="image/svg+xml"
|
||
class="w-full rounded-lg fade-in cursor-pointer" onclick="toggleFullscreen()">
|
||
<p class="text-center text-gray-500 py-10">Loading SVG...</p>
|
||
</object>
|
||
</div>
|
||
|
||
<!-- Navigation Controls -->
|
||
<div class="flex items-center justify-center mt-6 space-x-4">
|
||
<button id="prevBtn" onclick="prevSlide()"
|
||
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">
|
||
<i class="fas fa-chevron-left"></i>
|
||
</button>
|
||
|
||
<div class="flex items-center space-x-2 px-6 py-2 glass rounded-full shadow-md">
|
||
<span id="currentPage" class="text-brand-600 font-bold">1</span>
|
||
<span class="text-gray-400">/</span>
|
||
<span id="totalPages" class="text-gray-600">1</span>
|
||
</div>
|
||
|
||
<button id="nextBtn" onclick="nextSlide()"
|
||
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">
|
||
<i class="fas fa-chevron-right"></i>
|
||
</button>
|
||
|
||
<button onclick="toggleFullscreen()"
|
||
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">
|
||
<i class="fas fa-expand"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Thumbnail Sidebar -->
|
||
<div class="sidebar-panel lg:w-64 xl:w-72">
|
||
<div class="glass rounded-2xl p-4 shadow-lg">
|
||
<h3 class="text-sm font-semibold text-gray-600 mb-3 flex items-center">
|
||
<i class="fas fa-th-large mr-2 text-brand-600"></i>
|
||
Slide Index
|
||
</h3>
|
||
<div id="thumbnailContainer" class="space-y-3 max-h-[60vh] overflow-y-auto pr-2">
|
||
<!-- Thumbnails will be generated by JavaScript -->
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Slide Info -->
|
||
<div class="glass rounded-2xl p-4 shadow-lg mt-4">
|
||
<h3 class="text-sm font-semibold text-gray-600 mb-3 flex items-center">
|
||
<i class="fas fa-info-circle mr-2 text-brand-600"></i>
|
||
Current Slide
|
||
</h3>
|
||
<p id="slideTitle" class="text-gray-800 font-medium">Title</p>
|
||
<p id="slideDesc" class="text-sm text-gray-500 mt-1">Description</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Slide Overview -->
|
||
<div class="mt-12 slide-enter">
|
||
<h2 class="text-xl font-bold mb-6 flex items-center" style="color: var(--text); letter-spacing: -0.015em;">
|
||
<i class="fas fa-list-ul mr-3" style="color: var(--text-muted);"></i>
|
||
Overview
|
||
</h2>
|
||
<div id="overviewGrid" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-4">
|
||
<!-- Overview thumbnails will be generated by JavaScript -->
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
<!-- Footer -->
|
||
<footer class="mt-12 py-6" style="border-top: 1px solid var(--border);">
|
||
<div class="max-w-7xl mx-auto px-4 text-center">
|
||
<p class="text-sm" style="color: var(--text-muted);">
|
||
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>
|
||
</p>
|
||
</div>
|
||
</footer>
|
||
|
||
<!-- Fullscreen Overlay -->
|
||
<div id="fullscreenOverlay" class="fullscreen-overlay hidden" onclick="toggleFullscreen()">
|
||
<button class="absolute top-4 right-4 text-white text-2xl hover:text-gray-300 transition-colors">
|
||
<i class="fas fa-times"></i>
|
||
</button>
|
||
<button onclick="event.stopPropagation(); prevSlide()"
|
||
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">
|
||
<i class="fas fa-chevron-left"></i>
|
||
</button>
|
||
<object id="fullscreenImage" data="" type="image/svg+xml" class="fade-in">
|
||
<p>Loading SVG...</p>
|
||
</object>
|
||
<button onclick="event.stopPropagation(); nextSlide()"
|
||
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">
|
||
<i class="fas fa-chevron-right"></i>
|
||
</button>
|
||
<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">
|
||
<span id="fullscreenPage">1</span> / <span id="fullscreenTotal">1</span>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const EXAMPLES_DATA_URL = 'examples/examples.json';
|
||
|
||
// State
|
||
let collections = [];
|
||
let currentCollection = null;
|
||
let currentSlide = 0;
|
||
let isFullscreen = false;
|
||
let dropdownOpen = false;
|
||
let viewMode = 'normal';
|
||
|
||
// Initialize
|
||
document.addEventListener('DOMContentLoaded', initViewer);
|
||
|
||
async function initViewer() {
|
||
try {
|
||
collections = await loadCollections();
|
||
renderCollections();
|
||
|
||
// Check URL for collection parameter
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const collectionId = urlParams.get('project');
|
||
if (collectionId) {
|
||
const collection = collections.find(c => c.id === collectionId);
|
||
if (collection) {
|
||
openCollection(collection);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
showLoadError(error);
|
||
}
|
||
}
|
||
|
||
async function loadCollections() {
|
||
const response = await fetch(EXAMPLES_DATA_URL, { cache: 'no-cache' });
|
||
if (!response.ok) {
|
||
throw new Error(`Failed to load ${EXAMPLES_DATA_URL}: ${response.status}`);
|
||
}
|
||
const data = await response.json();
|
||
return (data.projects || []).map(project => ({
|
||
...project,
|
||
slides: project.slides || [],
|
||
cover: project.cover || project.slides?.[0]?.file || ''
|
||
}));
|
||
}
|
||
|
||
function showLoadError(error) {
|
||
console.error(error);
|
||
document.getElementById('emptyState').classList.remove('hidden');
|
||
document.querySelector('#emptyState h3').textContent = 'Unable to load slide collections';
|
||
document.querySelector('#emptyState p').textContent = 'Start a local web server or check examples/examples.json.';
|
||
}
|
||
|
||
// Render collection cards
|
||
function renderCollections() {
|
||
const grid = document.getElementById('collectionsGrid');
|
||
|
||
if (collections.length === 0) {
|
||
document.getElementById('emptyState').classList.remove('hidden');
|
||
return;
|
||
}
|
||
|
||
grid.innerHTML = collections.map(collection => {
|
||
const cover = collection.cover || collection.slides[0]?.file || '';
|
||
const coverPath = encodeURI(`examples/${collection.folder}/${cover}`);
|
||
const collectionUrl = `viewer.html?project=${encodeURIComponent(collection.id)}`;
|
||
return `
|
||
<a href="${collectionUrl}" data-collection-id="${collection.id}" class="collection-card rounded-xl overflow-hidden" style="background: var(--surface);">
|
||
<div class="relative" style="aspect-ratio: 16 / 9; background: var(--darker); border-bottom: 1px solid var(--border);">
|
||
<object data="${coverPath}" type="image/svg+xml" class="w-full h-full object-contain" style="pointer-events:none">
|
||
<div class="w-full h-full flex items-center justify-center text-3xl" style="opacity: 0.5;">${collection.icon}</div>
|
||
</object>
|
||
<div class="absolute bottom-3 right-3 px-2.5 py-1 rounded-md text-xs font-medium"
|
||
style="background: rgba(13, 13, 15, 0.85); color: var(--text); border: 1px solid var(--border); backdrop-filter: blur(8px);">
|
||
${collection.slides.length} slides
|
||
</div>
|
||
</div>
|
||
<div class="p-5" style="border-left: 3px solid ${collection.color};">
|
||
<div class="flex items-center gap-2 mb-2">
|
||
<span class="text-base">${collection.icon}</span>
|
||
<h3 class="font-semibold text-base" style="color: var(--text); letter-spacing: -0.01em;">${collection.title}</h3>
|
||
</div>
|
||
<p class="text-sm line-clamp-2" style="color: var(--text-muted); line-height: 1.55;">${collection.description}</p>
|
||
</div>
|
||
</a>
|
||
`}).join('');
|
||
|
||
grid.querySelectorAll('.collection-card').forEach(card => {
|
||
card.addEventListener('click', handleCollectionCardClick);
|
||
});
|
||
|
||
renderDropdown();
|
||
}
|
||
|
||
function handleCollectionCardClick(event) {
|
||
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
|
||
return;
|
||
}
|
||
|
||
event.preventDefault();
|
||
const collection = collections.find(item => item.id === event.currentTarget.dataset.collectionId);
|
||
if (collection) {
|
||
openCollection(collection);
|
||
}
|
||
}
|
||
|
||
// Render dropdown menu
|
||
function renderDropdown() {
|
||
const dropdown = document.getElementById('dropdownMenu');
|
||
dropdown.innerHTML = collections.map(collection => `
|
||
<div class="flex items-center px-4 py-3 hover:bg-gray-50 cursor-pointer transition-colors ${currentCollection?.id === collection.id ? 'bg-brand-50' : ''}"
|
||
onclick='switchCollection(${JSON.stringify(collection).replace(/'/g, "'")})'>
|
||
<span class="text-2xl mr-3">${collection.icon}</span>
|
||
<div class="flex-1 min-w-0">
|
||
<p class="font-medium text-gray-800 truncate">${collection.title}</p>
|
||
<p class="text-xs text-gray-500">${collection.slides.length} slides</p>
|
||
</div>
|
||
${currentCollection?.id === collection.id ? '<i class="fas fa-check text-brand-500"></i>' : ''}
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
// Toggle dropdown
|
||
function toggleDropdown() {
|
||
dropdownOpen = !dropdownOpen;
|
||
document.getElementById('dropdownMenu').classList.toggle('hidden', !dropdownOpen);
|
||
}
|
||
|
||
// Close dropdown when clicking outside
|
||
document.addEventListener('click', (e) => {
|
||
if (!e.target.closest('#collectionSelector')) {
|
||
dropdownOpen = false;
|
||
document.getElementById('dropdownMenu').classList.add('hidden');
|
||
}
|
||
});
|
||
|
||
// Open a collection
|
||
function openCollection(collection) {
|
||
currentCollection = collection;
|
||
currentSlide = 0;
|
||
|
||
// Update URL
|
||
const url = new URL(window.location);
|
||
url.searchParams.set('project', collection.id);
|
||
window.history.pushState({}, '', url);
|
||
|
||
// Update UI
|
||
document.getElementById('libraryView').classList.add('hidden');
|
||
document.getElementById('viewerView').classList.remove('hidden');
|
||
document.getElementById('collectionSelector').classList.remove('hidden');
|
||
document.getElementById('keyboardHints').classList.remove('hidden');
|
||
document.getElementById('keyboardHints').classList.add('flex');
|
||
document.getElementById('mainContent').classList.add('viewer-expanded');
|
||
|
||
setViewMode('normal');
|
||
|
||
// Update header
|
||
document.getElementById('currentCollectionIcon').textContent = collection.icon;
|
||
document.getElementById('currentCollectionName').textContent = collection.title;
|
||
|
||
// Update viewer
|
||
document.getElementById('viewerTitle').innerHTML = `${collection.icon} ${collection.title}`;
|
||
document.getElementById('viewerDescription').textContent = collection.description;
|
||
document.getElementById('totalPages').textContent = collection.slides.length;
|
||
document.getElementById('fullscreenTotal').textContent = collection.slides.length;
|
||
|
||
// Set theme color
|
||
document.getElementById('progressBar').style.background = `linear-gradient(90deg, ${collection.color} 0%, ${collection.color}cc 100%)`;
|
||
|
||
generateThumbnails();
|
||
generateOverview();
|
||
updateSlide();
|
||
renderDropdown();
|
||
|
||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||
}
|
||
|
||
// Switch collection from dropdown
|
||
function switchCollection(collection) {
|
||
toggleDropdown();
|
||
openCollection(collection);
|
||
}
|
||
|
||
// Back to library
|
||
function backToLibrary() {
|
||
currentCollection = null;
|
||
|
||
const url = new URL(window.location);
|
||
url.searchParams.delete('project');
|
||
window.history.pushState({}, '', url);
|
||
|
||
document.getElementById('libraryView').classList.remove('hidden');
|
||
document.getElementById('viewerView').classList.add('hidden');
|
||
document.getElementById('collectionSelector').classList.add('hidden');
|
||
document.getElementById('keyboardHints').classList.add('hidden');
|
||
document.getElementById('keyboardHints').classList.remove('flex');
|
||
document.getElementById('progressBar').style.width = '0%';
|
||
document.getElementById('mainContent').classList.remove('viewer-expanded');
|
||
document.getElementById('slideViewerContainer').classList.remove('theater-mode');
|
||
|
||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||
}
|
||
|
||
// Set view mode
|
||
function setViewMode(mode) {
|
||
viewMode = mode;
|
||
const container = document.getElementById('slideViewerContainer');
|
||
const normalBtn = document.getElementById('normalModeBtn');
|
||
const theaterBtn = document.getElementById('theaterModeBtn');
|
||
|
||
if (mode === 'theater') {
|
||
container.classList.add('theater-mode');
|
||
normalBtn.classList.remove('active');
|
||
theaterBtn.classList.add('active');
|
||
} else {
|
||
container.classList.remove('theater-mode');
|
||
normalBtn.classList.add('active');
|
||
theaterBtn.classList.remove('active');
|
||
}
|
||
}
|
||
|
||
function toggleViewMode() {
|
||
setViewMode(viewMode === 'normal' ? 'theater' : 'normal');
|
||
}
|
||
|
||
// Generate thumbnail sidebar
|
||
function generateThumbnails() {
|
||
if (!currentCollection) return;
|
||
|
||
const container = document.getElementById('thumbnailContainer');
|
||
const basePath = `examples/${currentCollection.folder}/`;
|
||
|
||
container.innerHTML = currentCollection.slides.map((slide, index) => {
|
||
const slidePath = encodeURI(basePath + slide.file);
|
||
return `
|
||
<div class="thumbnail rounded-lg overflow-hidden border-2 ${index === 0 ? 'border-brand-500' : 'border-transparent'}"
|
||
onclick="goToSlide(${index})"
|
||
id="thumb-${index}">
|
||
<object data="${slidePath}" type="image/svg+xml" class="w-full" style="pointer-events:none">
|
||
<div class="w-full h-16 flex items-center justify-center bg-gray-100">📄</div>
|
||
</object>
|
||
<div class="px-2 py-1 bg-gray-50 text-xs text-gray-600 truncate">
|
||
${index + 1}. ${slide.title}
|
||
</div>
|
||
</div>
|
||
`}).join('');
|
||
}
|
||
|
||
// Generate overview grid
|
||
function generateOverview() {
|
||
if (!currentCollection) return;
|
||
|
||
const container = document.getElementById('overviewGrid');
|
||
const basePath = `examples/${currentCollection.folder}/`;
|
||
|
||
container.innerHTML = currentCollection.slides.map((slide, index) => {
|
||
const slidePath = encodeURI(basePath + slide.file);
|
||
return `
|
||
<div class="glass rounded-xl p-2 shadow-md hover:shadow-lg transition-shadow cursor-pointer"
|
||
onclick="goToSlide(${index}); window.scrollTo({top: 0, behavior: 'smooth'})">
|
||
<object data="${slidePath}" type="image/svg+xml" class="w-full rounded-lg" style="pointer-events:none">
|
||
<div class="w-full h-24 flex items-center justify-center bg-gray-100">📄</div>
|
||
</object>
|
||
<div class="mt-2 px-1">
|
||
<p class="text-sm font-medium text-gray-800 truncate">${index + 1}. ${slide.title}</p>
|
||
<p class="text-xs text-gray-500 truncate">${slide.desc}</p>
|
||
</div>
|
||
</div>
|
||
`}).join('');
|
||
}
|
||
|
||
// Update slide display
|
||
function updateSlide() {
|
||
if (!currentCollection) return;
|
||
|
||
const slide = currentCollection.slides[currentSlide];
|
||
const basePath = `examples/${currentCollection.folder}/`;
|
||
const slidePath = encodeURI(basePath + slide.file);
|
||
|
||
const mainSlide = document.getElementById('mainSlide');
|
||
const fullscreenImage = document.getElementById('fullscreenImage');
|
||
|
||
// Update main slide
|
||
mainSlide.classList.remove('fade-in');
|
||
void mainSlide.offsetWidth;
|
||
mainSlide.classList.add('fade-in');
|
||
mainSlide.data = slidePath;
|
||
|
||
// Update fullscreen
|
||
fullscreenImage.data = slidePath;
|
||
|
||
// Update page numbers
|
||
document.getElementById('currentPage').textContent = currentSlide + 1;
|
||
document.getElementById('fullscreenPage').textContent = currentSlide + 1;
|
||
|
||
// Update slide info
|
||
document.getElementById('slideTitle').textContent = slide.title;
|
||
document.getElementById('slideDesc').textContent = slide.desc;
|
||
|
||
// Update progress bar
|
||
const progress = ((currentSlide + 1) / currentCollection.slides.length) * 100;
|
||
document.getElementById('progressBar').style.width = progress + '%';
|
||
|
||
// Update navigation buttons
|
||
document.getElementById('prevBtn').disabled = currentSlide === 0;
|
||
document.getElementById('nextBtn').disabled = currentSlide === currentCollection.slides.length - 1;
|
||
|
||
// Update thumbnail highlights
|
||
currentCollection.slides.forEach((_, i) => {
|
||
const thumb = document.getElementById(`thumb-${i}`);
|
||
if (thumb) {
|
||
if (i === currentSlide) {
|
||
thumb.classList.add('border-brand-500');
|
||
thumb.classList.remove('border-transparent');
|
||
thumb.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||
} else {
|
||
thumb.classList.remove('border-brand-500');
|
||
thumb.classList.add('border-transparent');
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Navigation
|
||
function nextSlide() {
|
||
if (currentCollection && currentSlide < currentCollection.slides.length - 1) {
|
||
currentSlide++;
|
||
updateSlide();
|
||
}
|
||
}
|
||
|
||
function prevSlide() {
|
||
if (currentSlide > 0) {
|
||
currentSlide--;
|
||
updateSlide();
|
||
}
|
||
}
|
||
|
||
function goToSlide(index) {
|
||
currentSlide = index;
|
||
updateSlide();
|
||
}
|
||
|
||
// Fullscreen toggle
|
||
function toggleFullscreen() {
|
||
const overlay = document.getElementById('fullscreenOverlay');
|
||
isFullscreen = !isFullscreen;
|
||
|
||
if (isFullscreen) {
|
||
overlay.classList.remove('hidden');
|
||
document.body.style.overflow = 'hidden';
|
||
} else {
|
||
overlay.classList.add('hidden');
|
||
document.body.style.overflow = '';
|
||
}
|
||
}
|
||
|
||
// Keyboard navigation
|
||
document.addEventListener('keydown', (e) => {
|
||
if (!currentCollection && e.key !== 'Escape') return;
|
||
|
||
switch (e.key) {
|
||
case 'ArrowLeft':
|
||
prevSlide();
|
||
break;
|
||
case 'ArrowRight':
|
||
nextSlide();
|
||
break;
|
||
case 'Escape':
|
||
if (isFullscreen) {
|
||
toggleFullscreen();
|
||
} else if (currentCollection) {
|
||
backToLibrary();
|
||
}
|
||
break;
|
||
case 'f':
|
||
case 'F':
|
||
if (currentCollection) toggleFullscreen();
|
||
break;
|
||
case 't':
|
||
case 'T':
|
||
if (currentCollection && !isFullscreen) toggleViewMode();
|
||
break;
|
||
}
|
||
});
|
||
|
||
// Handle browser back/forward
|
||
window.addEventListener('popstate', () => {
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const collectionId = urlParams.get('project');
|
||
|
||
if (collectionId) {
|
||
const collection = collections.find(c => c.id === collectionId);
|
||
if (collection) {
|
||
openCollection(collection);
|
||
}
|
||
} else {
|
||
backToLibrary();
|
||
}
|
||
});
|
||
|
||
// Touch/swipe support
|
||
let touchStartX = 0;
|
||
let touchEndX = 0;
|
||
|
||
document.addEventListener('touchstart', (e) => {
|
||
touchStartX = e.changedTouches[0].screenX;
|
||
});
|
||
|
||
document.addEventListener('touchend', (e) => {
|
||
touchEndX = e.changedTouches[0].screenX;
|
||
const diff = touchStartX - touchEndX;
|
||
|
||
if (Math.abs(diff) > 50 && currentCollection) {
|
||
if (diff > 0) nextSlide();
|
||
else prevSlide();
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
|
||
</html>
|