Files
ETPlot/template.html
T
2025-07-07 14:44:09 +02:00

1293 lines
36 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ title }}</title>
<style>
/* ========================================
CSS VARIABLES AND THEME DEFINITIONS
======================================== */
:root {
--bg-color: #1e1e1e;
--text-color: #ffffff;
--card-bg: #2d2d2d;
--border-color: #404040;
--link-color: #569cd6;
--link-hover: #4a9eff;
--button-bg: #0078d4;
--button-hover: #106ebe;
--breadcrumb-color: #cccccc;
--tree-bg: #252526;
--tree-current-bg: #0078d4;
--success-color: #28a745;
--success-hover: #218838;
--disabled-color: #6c757d;
}
[data-theme="light"] {
--bg-color: #ffffff;
--text-color: #333333;
--card-bg: #f8f8f8;
--border-color: #ddd;
--link-color: #007acc;
--link-hover: #005a9e;
--button-bg: #007acc;
--button-hover: #005a9e;
--breadcrumb-color: #666;
--tree-bg: #f8f8f8;
--tree-current-bg: #007acc;
}
/* ========================================
BASE LAYOUT AND TYPOGRAPHY
======================================== */
* {
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 1rem;
padding-bottom: 2rem;
background-color: var(--bg-color);
color: var(--text-color);
transition: background-color 0.3s, color 0.3s;
line-height: 1.5;
}
h1 {
font-size: 1.8rem;
margin-bottom: 0.5rem;
font-weight: 600;
}
h2 {
color: var(--text-color);
font-size: 1.3rem;
margin: 1.5rem 0 0.5rem 0;
}
/* ========================================
NAVIGATION COMPONENTS
======================================== */
.breadcrumb {
margin: 0.5rem 0 1rem 0;
font-size: 0.9rem;
color: var(--breadcrumb-color);
}
.breadcrumb a {
color: var(--link-color);
text-decoration: none;
}
.breadcrumb a:hover {
text-decoration: underline;
color: var(--link-hover);
}
.breadcrumb .separator {
margin: 0 0.3rem;
color: var(--breadcrumb-color);
opacity: 0.7;
}
.navigation {
margin: 1rem 0;
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.nav-btn {
background: var(--button-bg);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 6px;
cursor: pointer;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.3rem;
font-size: 0.9rem;
font-weight: 500;
transition: all 0.2s ease;
}
.nav-btn:hover {
background: var(--button-hover);
transform: translateY(-1px);
}
.nav-btn:disabled {
background: var(--disabled-color);
cursor: not-allowed;
transform: none;
}
/* ========================================
SEARCH FUNCTIONALITY
======================================== */
.search-container {
margin: 1rem 0;
position: relative;
max-width: 500px;
}
.search-box {
width: 100%;
padding: 0.8rem 2.5rem 0.8rem 1rem;
border: 2px solid var(--border-color);
border-radius: 25px;
background: var(--card-bg);
color: var(--text-color);
font-size: 1rem;
outline: none;
transition: all 0.3s ease;
}
.search-box:focus {
border-color: var(--button-bg);
box-shadow: 0 0 0 3px rgba(0, 120, 212, 0.1);
}
.search-icon {
position: absolute;
right: 1rem;
top: 50%;
transform: translateY(-50%);
color: var(--text-color);
opacity: 0.6;
pointer-events: none;
}
.search-results {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
margin-top: 0.5rem;
max-height: 400px;
overflow-y: auto;
display: none;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 100;
}
.search-result-item {
padding: 0.75rem;
margin: 0;
cursor: pointer;
transition: background-color 0.2s ease;
border-bottom: 1px solid var(--border-color);
}
.search-result-item:last-child {
border-bottom: none;
}
.search-result-item:hover {
background: var(--button-bg);
color: white;
}
.search-highlight {
background: #ffeb3b;
color: #000;
font-weight: bold;
padding: 0.1rem 0.2rem;
border-radius: 2px;
}
/* ========================================
FOLDER TREE
======================================== */
.folder-tree {
background: var(--tree-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
font-family: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace;
font-size: 0.85rem;
max-height: 350px;
overflow-y: auto;
transition: all 0.3s ease;
}
.tree-item {
margin: 0.2rem 0;
white-space: pre;
font-family: inherit;
}
.tree-current {
background: var(--tree-current-bg);
color: white;
padding: 0.2rem 0.4rem;
border-radius: 4px;
font-weight: 500;
}
.tree-link {
color: var(--link-color);
text-decoration: none;
transition: color 0.2s ease;
}
.tree-link:hover {
text-decoration: underline;
color: var(--link-hover);
}
/* ========================================
PLOT GRID
======================================== */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 1.2rem;
padding: 1rem 0;
}
.grid-item {
text-align: center;
background: var(--card-bg);
border-radius: 8px;
padding: 0.8rem;
transition: all 0.2s ease;
border: 1px solid transparent;
}
.grid-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
border-color: var(--border-color);
}
.grid-item img {
max-width: 100%;
border: 1px solid var(--border-color);
border-radius: 6px;
transition: all 0.2s ease;
}
.grid-item a {
color: var(--link-color);
text-decoration: none;
}
.plot-name {
margin-top: 0.8rem;
word-wrap: break-word;
word-break: break-word;
hyphens: auto;
font-size: 0.9rem;
line-height: 1.3;
max-height: 3.9rem;
overflow: hidden;
padding: 0 0.2rem;
font-weight: 500;
}
/* ========================================
SIDEBAR (RECENT PLOTS)
======================================== */
.sidebar {
position: fixed;
top: 0;
right: -350px;
width: 330px;
height: 100vh;
background: var(--card-bg);
border-left: 2px solid var(--border-color);
z-index: 2000;
transition: right 0.3s ease;
overflow-y: auto;
box-shadow: -4px 0 15px rgba(0,0,0,0.2);
}
.sidebar.open {
right: 0;
}
.sidebar-header {
padding: 1.2rem;
border-bottom: 1px solid var(--border-color);
position: sticky;
top: 0;
background: var(--card-bg);
z-index: 1;
}
.sidebar-title {
margin: 0;
font-size: 1.2rem;
color: var(--text-color);
font-weight: 600;
}
.sidebar-close {
position: absolute;
top: 1rem;
right: 1rem;
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--text-color);
padding: 0.2rem;
border-radius: 4px;
transition: background-color 0.2s ease;
}
.sidebar-close:hover {
background: var(--border-color);
}
.sidebar-content {
padding: 1rem;
}
.recent-plot {
display: flex;
gap: 0.7rem;
padding: 0.8rem;
margin-bottom: 0.8rem;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid var(--border-color);
}
.recent-plot:hover {
background: var(--button-bg);
color: white;
transform: translateX(2px);
}
.recent-plot-thumb {
width: 60px;
height: 48px;
object-fit: cover;
border-radius: 4px;
flex-shrink: 0;
}
.recent-plot-info {
flex: 1;
min-width: 0;
}
.recent-plot-name {
font-size: 0.9rem;
font-weight: 600;
margin-bottom: 0.3rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.recent-plot-path {
font-size: 0.8rem;
color: var(--breadcrumb-color);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 1500;
display: none;
backdrop-filter: blur(2px);
}
.sidebar-overlay.open {
display: block;
}
/* ========================================
FLOATING ACTION BUTTONS
======================================== */
.floating-buttons {
position: fixed;
bottom: 20px;
right: 20px;
display: flex;
flex-direction: column;
gap: 10px;
z-index: 1000;
}
.floating-btn {
width: 56px;
height: 56px;
border-radius: 50%;
border: none;
cursor: pointer;
font-size: 1.3rem;
box-shadow: 0 3px 10px rgba(0,0,0,0.3);
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.floating-btn:hover {
transform: scale(1.1);
}
.sidebar-toggle {
background: var(--button-bg);
color: white;
}
.theme-toggle {
background: var(--button-bg);
color: white;
}
.refresh-btn {
background: var(--success-color);
color: white;
}
.refresh-btn:hover {
background: var(--success-hover);
}
.refresh-btn:disabled {
background: var(--disabled-color);
cursor: not-allowed;
transform: none;
}
/* ========================================
KEYBOARD SHORTCUTS HELP
======================================== */
.shortcuts-help {
position: fixed;
bottom: 140px;
right: 20px;
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 1rem;
display: none;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
z-index: 1000;
font-size: 0.9rem;
max-width: 280px;
}
.shortcuts-help h4 {
margin: 0 0 0.8rem 0;
color: var(--text-color);
font-size: 1rem;
}
.shortcut-item {
display: flex;
justify-content: space-between;
align-items: center;
margin: 0.4rem 0;
}
.shortcut-key {
background: var(--border-color);
padding: 0.2rem 0.4rem;
border-radius: 4px;
font-family: 'JetBrains Mono', 'Courier New', monospace;
font-size: 0.8rem;
font-weight: 500;
}
/* ========================================
SUBDIRECTORIES LIST
======================================== */
ul {
list-style: none;
padding: 0;
}
ul li {
margin: 0.5rem 0;
}
ul li a {
color: var(--link-color);
text-decoration: none;
padding: 0.3rem 0;
display: inline-block;
transition: color 0.2s ease;
}
ul li a:hover {
color: var(--link-hover);
text-decoration: underline;
}
/* ========================================
RESPONSIVE DESIGN
======================================== */
@media (max-width: 768px) {
body {
padding: 0.5rem;
}
.grid {
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 0.8rem;
}
.sidebar {
width: 100%;
right: -100%;
}
.navigation {
gap: 0.3rem;
}
.nav-btn {
padding: 0.4rem 0.8rem;
font-size: 0.8rem;
}
.search-container {
max-width: 100%;
}
}
</style>
</head>
<body>
<!-- Main Content -->
<h1>{{ title }}</h1>
<!-- Search Bar -->
<div class="search-container">
<input type="text" class="search-box" id="searchBox" placeholder="Search plots..." />
<span class="search-icon">🔍</span>
<div class="search-results" id="searchResults"></div>
</div>
<!-- Breadcrumb Navigation -->
<div class="breadcrumb" id="breadcrumb"></div>
<!-- Navigation Buttons -->
<div class="navigation">
<button class="nav-btn" onclick="goToContent()" id="contentBtn">
🎯 Go to Content
</button>
<button class="nav-btn" onclick="window.history.back()">
← Back
</button>
{% if relpath != "." %}
<a href="../index.html" class="nav-btn">
↑ Parent Directory
</a>
{% endif %}
</div>
<!-- Folder Tree -->
<div class="folder-tree" id="folderTree"></div>
<!-- Plot Grid -->
<div class="grid">
{% for item in items %}
<div class="grid-item">
<a href="{{ item.pdf_href }}">
<img src="{{ item.png_href }}" alt="{{ item.name }}">
</a>
<div class="plot-name" title="{{ item.name }}">{{ item.name }}</div>
</div>
{% endfor %}
</div>
<!-- Subdirectories -->
{% if subdirs %}
<h2>Subdirectories</h2>
<ul>
{% for sub in subdirs %}
<li><a href="{{ sub }}/index.html">📁 {{ sub }}</a></li>
{% endfor %}
</ul>
{% endif %}
<!-- Recent Plots Sidebar -->
<div class="sidebar-overlay" id="sidebarOverlay" onclick="toggleSidebar()"></div>
<div class="sidebar" id="sidebar">
<div class="sidebar-header">
<h3 class="sidebar-title">Recent Plots</h3>
<button class="sidebar-close" onclick="toggleSidebar()">×</button>
</div>
<div class="sidebar-content" id="sidebarContent">
<div style="text-align: center; color: var(--breadcrumb-color); margin: 2rem 0;">
No recent plots yet
</div>
</div>
</div>
<!-- Floating Action Buttons -->
<div class="floating-buttons">
<button class="floating-btn sidebar-toggle" onclick="toggleSidebar()" id="sidebarToggle" title="Recent Plots (Ctrl+R)">
📋
</button>
<button class="floating-btn theme-toggle" onclick="toggleTheme()" id="themeToggle" title="Toggle Theme (Ctrl+T)">
☀️
</button>
<button class="floating-btn refresh-btn" onclick="refreshGallery()" id="refreshBtn" title="Regenerate Gallery (F5)">
🔄
</button>
</div>
<!-- Keyboard Shortcuts Help -->
<div class="shortcuts-help" id="shortcutsHelp">
<h4>Keyboard Shortcuts</h4>
<div class="shortcut-item">
<span>Search</span>
<span class="shortcut-key">Ctrl+K</span>
</div>
<div class="shortcut-item">
<span>Recent plots</span>
<span class="shortcut-key">Ctrl+R</span>
</div>
<div class="shortcut-item">
<span>Refresh</span>
<span class="shortcut-key">F5</span>
</div>
<div class="shortcut-item">
<span>Toggle theme</span>
<span class="shortcut-key">Ctrl+T</span>
</div>
<div class="shortcut-item">
<span>Help</span>
<span class="shortcut-key">?</span>
</div>
</div>
<script>
// ========================================
// GALLERY APPLICATION CLASS
// ========================================
class GalleryApp {
constructor() {
this.searchTimeout = null;
this.SEARCH_DEBOUNCE_MS = {{ ui.search_debounce_ms|default(300) }};
this.MAX_RECENT_PLOTS = {{ ui.max_recent_plots|default(20) }};
this.init();
}
/**
* Initialize the gallery application
*/
init() {
this.initTheme();
this.buildBreadcrumb();
this.buildFolderTree();
this.initSearch();
this.initKeyboardShortcuts();
this.updateRecentPlotsDisplay();
this.trackPlotClicks();
this.hideContentButtonIfNeeded();
}
/**
* Initialize theme system and load saved preference
*/
initTheme() {
const savedTheme = localStorage.getItem('theme');
const html = document.documentElement;
const themeToggle = document.getElementById('themeToggle');
if (savedTheme === 'light') {
html.setAttribute('data-theme', 'light');
themeToggle.textContent = '🌙';
} else {
html.removeAttribute('data-theme');
themeToggle.textContent = '☀️';
}
}
/**
* Toggle between light and dark themes
*/
toggleTheme() {
const html = document.documentElement;
const themeToggle = document.getElementById('themeToggle');
if (html.getAttribute('data-theme') === 'light') {
html.removeAttribute('data-theme');
themeToggle.textContent = '☀️';
localStorage.setItem('theme', 'dark');
} else {
html.setAttribute('data-theme', 'light');
themeToggle.textContent = '🌙';
localStorage.setItem('theme', 'light');
}
}
/**
* Build breadcrumb navigation based on current path
*/
buildBreadcrumb() {
const currentPath = window.location.pathname;
const pathParts = currentPath.split('/').filter(part => part !== '' && part !== 'index.html');
const breadcrumb = document.getElementById('breadcrumb');
if (pathParts.length === 0) {
breadcrumb.innerHTML = '<span>🏠 Root</span>';
return;
}
let html = '<a href="/">🏠 Root</a>';
for (let i = 0; i < pathParts.length; i++) {
const part = pathParts[i];
html += '<span class="separator">/</span>';
if (i === pathParts.length - 1) {
html += `<span>${decodeURIComponent(part)}</span>`;
} else {
const levelsUp = pathParts.length - 1 - i;
const relativePath = '../'.repeat(levelsUp) + 'index.html';
html += `<a href="${relativePath}">${decodeURIComponent(part)}</a>`;
}
}
breadcrumb.innerHTML = html;
}
/**
* Build and display the folder tree structure
*/
async buildFolderTree() {
const treeContainer = document.getElementById('folderTree');
const currentPath = window.location.pathname;
try {
const tree = await this.buildTreeRecursive(currentPath, 0, currentPath);
treeContainer.innerHTML = tree;
} catch (error) {
console.error('Error building folder tree:', error);
treeContainer.innerHTML = '<div class="tree-item">❌ Error loading folder tree</div>';
}
}
/**
* Recursively build tree structure for folders
*/
async buildTreeRecursive(path, depth, currentPath, maxDepth = 5) {
if (depth > maxDepth) {
const indent = ' '.repeat(depth);
return `<div class="tree-item">${indent}└─ ...</div>`;
}
const indent = ' '.repeat(depth);
const folderName = path.split('/').filter(p => p !== '' && p !== 'index.html').pop() || 'Gallery';
try {
const response = await fetch(path);
const htmlContent = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(htmlContent, 'text/html');
const subdirs = doc.querySelectorAll('h2 + ul li a');
const items = doc.querySelectorAll('.grid-item');
const totalItems = items.length + subdirs.length;
const arrow = depth === 0 ? '' : '└─ ';
let html = '';
if (path === currentPath) {
html += `<div class="tree-item">${indent}${arrow}📁 <span class="tree-current">${folderName}</span> (${totalItems} items)</div>`;
} else {
html += `<div class="tree-item">${indent}${arrow}📁 <a href="${path}" class="tree-link">${folderName}</a> (${totalItems} items)</div>`;
}
for (let i = 0; i < subdirs.length; i++) {
const subdir = subdirs[i];
const href = subdir.getAttribute('href');
if (href) {
const baseUrl = path.replace(/\/[^\/]*$/, '/');
const subPath = baseUrl + href;
html += await this.buildTreeRecursive(subPath, depth + 1, currentPath, maxDepth);
}
}
return html;
} catch (error) {
const arrow = depth === 0 ? '' : '└─ ';
return `<div class="tree-item">${indent}${arrow}📁 ${folderName} (error loading)</div>`;
}
}
/**
* Initialize search functionality with debouncing
*/
initSearch() {
const searchBox = document.getElementById('searchBox');
const searchResults = document.getElementById('searchResults');
searchBox.addEventListener('input', (e) => {
clearTimeout(this.searchTimeout);
const query = e.target.value.trim();
if (query.length === 0) {
searchResults.style.display = 'none';
return;
}
this.searchTimeout = setTimeout(() => {
this.performSearch(query);
}, this.SEARCH_DEBOUNCE_MS);
});
document.addEventListener('click', (e) => {
if (!searchBox.contains(e.target) && !searchResults.contains(e.target)) {
searchResults.style.display = 'none';
}
});
}
/**
* Perform search across plot names
*/
async performSearch(query) {
const searchResults = document.getElementById('searchResults');
searchResults.innerHTML = '<div style="padding: 1rem;">🔍 Searching...</div>';
searchResults.style.display = 'block';
try {
const results = await this.searchPlots(query);
this.displaySearchResults(results, query);
} catch (error) {
console.error('Search error:', error);
searchResults.innerHTML = '<div style="padding: 1rem; color: red;">❌ Search failed</div>';
}
}
/**
* Search for plots matching the query
*/
async searchPlots(query) {
const results = [];
const visited = new Set();
const lowerQuery = query.toLowerCase();
await this.searchInPage(window.location.pathname, lowerQuery, results, visited);
await this.searchRecursive(window.location.pathname, lowerQuery, results, visited, 0, 5);
return results.slice(0, 20);
}
/**
* Search for plots in a specific page
*/
async searchInPage(path, query, results, visited, maxResults = 50) {
if (visited.has(path) || results.length >= maxResults) return;
visited.add(path);
try {
const response = await fetch(path);
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const items = doc.querySelectorAll('.grid-item');
items.forEach(item => {
const nameElement = item.querySelector('.plot-name');
const imgElement = item.querySelector('img');
const linkElement = item.querySelector('a');
if (nameElement && imgElement && linkElement) {
const name = nameElement.textContent.toLowerCase();
if (name.includes(query)) {
results.push({
name: nameElement.textContent,
path: path,
href: linkElement.href,
imgSrc: imgElement.src,
relevance: this.calculateRelevance(name, query)
});
}
}
});
} catch (error) {
console.error('Error searching in', path, error);
}
}
/**
* Recursively search in subdirectories
*/
async searchRecursive(path, query, results, visited, depth, maxDepth) {
if (depth >= maxDepth || results.length >= 50) return;
try {
const response = await fetch(path);
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const subdirs = doc.querySelectorAll('h2 + ul li a');
for (const subdir of subdirs) {
const href = subdir.getAttribute('href');
if (href) {
const baseUrl = path.replace(/\/[^\/]*$/, '/');
const subPath = baseUrl + href;
await this.searchInPage(subPath, query, results, visited);
await this.searchRecursive(subPath, query, results, visited, depth + 1, maxDepth);
}
}
} catch (error) {
console.error('Error in recursive search:', error);
}
}
/**
* Calculate search relevance score
*/
calculateRelevance(text, query) {
const exactMatch = text === query;
const startsWith = text.startsWith(query);
const wordMatch = text.split(/\s+/).some(word => word.startsWith(query));
if (exactMatch) return 100;
if (startsWith) return 80;
if (wordMatch) return 60;
return 40;
}
/**
* Display search results with highlighting
*/
displaySearchResults(results, query) {
const searchResults = document.getElementById('searchResults');
if (results.length === 0) {
searchResults.innerHTML = '<div style="padding: 1rem;">📭 No plots found</div>';
return;
}
results.sort((a, b) => b.relevance - a.relevance);
let html = '';
results.forEach(result => {
const highlightedName = this.highlightText(result.name, query);
const relativePath = this.getRelativePath(result.path);
html += `
<div class="search-result-item" onclick="app.openSearchResult('${result.href}')" title="${result.name}">
<div style="display: flex; gap: 0.7rem; align-items: center;">
<img src="${result.imgSrc}" style="width: 50px; height: 40px; object-fit: cover; border-radius: 4px;" />
<div style="flex: 1; min-width: 0;">
<div style="font-weight: 600; margin-bottom: 0.3rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
${highlightedName}
</div>
<div style="font-size: 0.8rem; color: var(--breadcrumb-color); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
📍 ${relativePath}
</div>
</div>
</div>
</div>
`;
});
searchResults.innerHTML = html;
}
/**
* Highlight search query in text
*/
highlightText(text, query) {
const regex = new RegExp(`(${query})`, 'gi');
return text.replace(regex, '<span class="search-highlight">$1</span>');
}
/**
* Get relative path for display
*/
getRelativePath(fullPath) {
const parts = fullPath.split('/').filter(p => p !== '' && p !== 'index.html');
return parts.length > 0 ? parts.join(' / ') : 'Root';
}
/**
* Open search result and track it
*/
openSearchResult(href) {
this.addToRecentPlots(href);
window.open(href, '_blank');
document.getElementById('searchResults').style.display = 'none';
}
/**
* Add plot to recent plots list
*/
addToRecentPlots(plotHref) {
const plotName = plotHref.split('/').pop().replace('.pdf', '');
const pathParts = plotHref.split('/').filter(p => p !== '' && p !== plotName + '.pdf');
const plotPath = pathParts.join(' / ');
const thumbUrl = plotHref.replace('.pdf', '.png');
const plotInfo = {
name: plotName,
path: plotPath,
href: plotHref,
thumbUrl: thumbUrl,
timestamp: Date.now()
};
let recentPlots = JSON.parse(localStorage.getItem('recentPlots') || '[]');
recentPlots = recentPlots.filter(p => p.href !== plotHref);
recentPlots.unshift(plotInfo);
recentPlots = recentPlots.slice(0, this.MAX_RECENT_PLOTS);
localStorage.setItem('recentPlots', JSON.stringify(recentPlots));
this.updateRecentPlotsDisplay();
}
/**
* Update recent plots sidebar display
*/
updateRecentPlotsDisplay() {
const sidebarContent = document.getElementById('sidebarContent');
const recentPlots = JSON.parse(localStorage.getItem('recentPlots') || '[]');
if (recentPlots.length === 0) {
sidebarContent.innerHTML = `
<div style="text-align: center; color: var(--breadcrumb-color); margin: 2rem 0;">
📭 No recent plots yet<br>
<small style="opacity: 0.7;">Open some plots to see them here</small>
</div>
`;
return;
}
let html = '';
recentPlots.forEach(plot => {
html += `
<div class="recent-plot" onclick="app.openRecentPlot('${plot.href}')" title="${plot.name}">
<img src="${plot.thumbUrl}" class="recent-plot-thumb" alt="${plot.name}" />
<div class="recent-plot-info">
<div class="recent-plot-name">${plot.name}</div>
<div class="recent-plot-path">📍 ${plot.path}</div>
</div>
</div>
`;
});
sidebarContent.innerHTML = html;
}
/**
* Open recent plot and close sidebar
*/
openRecentPlot(href) {
window.open(href, '_blank');
this.toggleSidebar();
}
/**
* Toggle recent plots sidebar
*/
toggleSidebar() {
const sidebar = document.getElementById('sidebar');
const overlay = document.getElementById('sidebarOverlay');
sidebar.classList.toggle('open');
overlay.classList.toggle('open');
}
/**
* Track clicks on plot links
*/
trackPlotClicks() {
document.addEventListener('click', (e) => {
const link = e.target.closest('a[href$=".pdf"]');
if (link) {
this.addToRecentPlots(link.href);
}
});
}
/**
* Initialize keyboard shortcuts
*/
initKeyboardShortcuts() {
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'k') {
e.preventDefault();
document.getElementById('searchBox').focus();
}
if (e.ctrlKey && e.key === 'r') {
e.preventDefault();
this.toggleSidebar();
}
if (e.ctrlKey && e.key === 't') {
e.preventDefault();
this.toggleTheme();
}
if (e.key === 'F5') {
e.preventDefault();
this.refreshGallery();
}
if (e.key === '?' && !e.ctrlKey && !e.altKey && !e.metaKey) {
e.preventDefault();
this.toggleShortcutsHelp();
}
if (e.key === 'Escape') {
document.getElementById('searchResults').style.display = 'none';
if (document.getElementById('sidebar').classList.contains('open')) {
this.toggleSidebar();
}
document.getElementById('shortcutsHelp').style.display = 'none';
}
});
}
/**
* Toggle keyboard shortcuts help display
*/
toggleShortcutsHelp() {
const help = document.getElementById('shortcutsHelp');
help.style.display = help.style.display === 'block' ? 'none' : 'block';
}
/**
* Refresh gallery by calling CGI script
*/
async refreshGallery() {
const refreshBtn = document.getElementById('refreshBtn');
refreshBtn.disabled = true;
refreshBtn.textContent = '⏳';
try {
const response = await fetch('/cgi-bin/refresh_gallery.py', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
if (response.ok) {
refreshBtn.textContent = '✅';
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
throw new Error('Refresh failed');
}
} catch (error) {
console.error('Error refreshing gallery:', error);
refreshBtn.textContent = '❌';
setTimeout(() => {
refreshBtn.disabled = false;
refreshBtn.textContent = '🔄';
}, 3000);
}
}
/**
* Navigate to deepest content folder automatically
*/
async goToContent() {
const contentBtn = document.getElementById('contentBtn');
contentBtn.disabled = true;
contentBtn.textContent = '🔍 Searching...';
let currentPath = window.location.pathname;
let visited = new Set();
let deepestWithContent = currentPath;
while (true) {
if (visited.has(currentPath)) break;
visited.add(currentPath);
try {
const response = await fetch(currentPath);
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const items = doc.querySelectorAll('.grid-item');
const subdirs = doc.querySelectorAll('h2 + ul li a');
if (items.length > 0) {
deepestWithContent = currentPath;
}
if (subdirs.length === 1) {
const nextPath = subdirs[0].getAttribute('href');
if (nextPath) {
const baseUrl = currentPath.replace(/\/[^\/]*$/, '/');
currentPath = baseUrl + nextPath;
} else {
break;
}
} else {
break;
}
} catch (error) {
console.error('Error fetching:', error);
break;
}
}
if (deepestWithContent !== window.location.pathname) {
window.location.href = deepestWithContent;
} else {
contentBtn.textContent = '✓ Already at content';
setTimeout(() => {
contentBtn.disabled = false;
contentBtn.textContent = '🎯 Go to Content';
}, 2000);
}
}
/**
* Hide 'Go to Content' button if not needed
*/
hideContentButtonIfNeeded() {
const items = document.querySelectorAll('.grid-item');
const subdirs = document.querySelectorAll('h2 + ul li a');
const contentBtn = document.getElementById('contentBtn');
if (items.length > 0 && subdirs.length === 0) {
contentBtn.style.display = 'none';
}
}
}
// ========================================
// GLOBAL FUNCTIONS (for backward compatibility)
// ========================================
let app;
function toggleTheme() { app.toggleTheme(); }
function toggleSidebar() { app.toggleSidebar(); }
function refreshGallery() { app.refreshGallery(); }
function goToContent() { app.goToContent(); }
// ========================================
// INITIALIZE APPLICATION
// ========================================
document.addEventListener('DOMContentLoaded', function() {
app = new GalleryApp();
});
</script>
</body>
</html>