bastebin/static/js/app.js

245 lines
8.8 KiB
JavaScript

// ── Config ─────────────────────────────────────────────────────────────────
window.PBCFG = null;
// Map config theme keys → CSS custom property names used in style.css
const _CSS_VAR_MAP = {
primary: '--primary',
primary_hover: '--primary-h',
danger: '--danger',
background: '--bg',
surface: '--surface',
border: '--border',
text_primary: '--text',
text_secondary: '--text-sub',
text_muted: '--text-muted',
code_bg: '--code-bg',
navbar_bg: '--nav-bg',
navbar_border: '--nav-border',
};
// Map config ui keys → CSS custom property names
const _UI_VAR_MAP = {
border_radius: '--radius',
};
// Fetch config and initialise application when ready
document.addEventListener('DOMContentLoaded', async () => {
try {
const resp = await fetch('/api/config');
if (resp.ok) window.PBCFG = await resp.json();
} catch (e) {
console.warn('Could not load /api/config, using CSS fallbacks.', e);
}
initialiseTheme();
applyUiVars();
initAutoSave();
// Attach theme toggle listener (fixing CSP blocking of inline onclick)
const toggleBtn = document.getElementById('themeToggle');
if (toggleBtn) {
toggleBtn.addEventListener('click', toggleTheme);
}
});
// ── Theme Management ────────────────────────────────────────────────────────
function initialiseTheme() {
const saved = localStorage.getItem('theme');
const configDefault = window.PBCFG?.theme?.default || 'auto';
let theme;
if (saved === 'light' || saved === 'dark') {
theme = saved;
} else if (configDefault === 'dark') {
theme = 'dark';
} else if (configDefault === 'light') {
theme = 'light';
} else {
// 'auto' or unset → follow OS preference
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
applyTheme(theme);
// Live OS theme changes (only when user hasn't pinned a preference)
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function (e) {
if (!localStorage.getItem('theme')) {
applyTheme(e.matches ? 'dark' : 'light');
}
});
}
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
applyConfigCssVars(theme);
swapPrismTheme(theme);
updateThemeToggle(theme);
}
function applyConfigCssVars(theme) {
const cfg = window.PBCFG?.theme;
if (!cfg) return;
const palette = cfg[theme] || {};
const root = document.documentElement;
for (const [key, cssVar] of Object.entries(_CSS_VAR_MAP)) {
if (palette[key] !== undefined) {
root.style.setProperty(cssVar, palette[key]);
}
}
}
function applyUiVars() {
const ui = window.PBCFG?.ui;
if (!ui) return;
const root = document.documentElement;
for (const [key, cssVar] of Object.entries(_UI_VAR_MAP)) {
if (ui[key] !== undefined) {
root.style.setProperty(cssVar, ui[key]);
}
}
}
function swapPrismTheme(theme) {
const light = document.getElementById('prism-light');
const dark = document.getElementById('prism-dark');
if (!light || !dark) return;
if (theme === 'dark') {
light.disabled = true;
dark.disabled = false;
} else {
light.disabled = false;
dark.disabled = true;
}
}
function toggleTheme() {
const current = document.documentElement.getAttribute('data-theme') || 'light';
const newTheme = current === 'light' ? 'dark' : 'light';
applyTheme(newTheme);
if (window.PBCFG?.theme?.allow_user_toggle !== false) {
localStorage.setItem('theme', newTheme);
}
}
function updateThemeToggle(theme) {
const btn = document.querySelector('.theme-toggle');
if (!btn) return;
btn.textContent = theme === 'light' ? '🌙' : '☀️';
btn.title = `Switch to ${theme === 'light' ? 'dark' : 'light'} mode`;
}
// ── Clipboard ───────────────────────────────────────────────────────────────
async function copyToClipboard(text) {
try {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text);
return true;
}
} catch (e) { /* fall through */ }
// Fallback: temporary textarea
const ta = document.createElement('textarea');
ta.value = text;
ta.style.cssText = 'position:fixed;top:0;left:0;opacity:0';
document.body.appendChild(ta);
ta.focus();
ta.select();
let ok = false;
try { ok = document.execCommand('copy'); } catch (_) {}
document.body.removeChild(ta);
return ok;
}
// ── Time formatting ─────────────────────────────────────────────────────────
window.formatDateTime = function (dateString) {
const date = new Date(dateString);
const now = new Date();
const diffMs = now - date;
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMs / 3600000);
const diffDays = Math.floor(diffMs / 86400000);
if (diffMins < 1) return 'Just now';
if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? 's' : ''} ago`;
if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
if (diffDays < 30) { const w = Math.floor(diffDays / 7); return `${w} week${w > 1 ? 's' : ''} ago`; }
if (diffDays < 365) { const m = Math.floor(diffDays / 30); return `${m} month${m > 1 ? 's' : ''} ago`; }
const y = Math.floor(diffDays / 365);
return `${y} year${y > 1 ? 's' : ''} ago`;
};
// ── Draft auto-save ─────────────────────────────────────────────────────────
function initAutoSave() {
const contentTextarea = document.getElementById('content');
const titleInput = document.getElementById('title');
if (!contentTextarea) return;
if (window.PBCFG?.features?.auto_save_draft !== false) {
loadDraft();
}
let saveTimeout;
function saveDraft() {
clearTimeout(saveTimeout);
saveTimeout = setTimeout(() => {
const draft = {
title: titleInput ? titleInput.value : '',
content: contentTextarea.value,
timestamp: new Date().toISOString()
};
if (draft.content.trim()) {
localStorage.setItem('paste_draft', JSON.stringify(draft));
} else {
localStorage.removeItem('paste_draft');
}
}, 1000);
}
contentTextarea.addEventListener('input', saveDraft);
if (titleInput) titleInput.addEventListener('input', saveDraft);
}
function loadDraft() {
const raw = localStorage.getItem('paste_draft');
if (!raw) return;
try {
const draft = JSON.parse(raw);
const maxDays = window.PBCFG?.features?.draft_max_age_days ?? 7;
const maxAge = maxDays * 86400000;
const draftAge = Date.now() - new Date(draft.timestamp).getTime();
if (draftAge > maxAge) { localStorage.removeItem('paste_draft'); return; }
if (draft.content && draft.content.trim() &&
confirm('A draft was found. Would you like to restore it?')) {
const title = document.getElementById('title');
const content = document.getElementById('content');
if (title) title.value = draft.title || '';
if (content) content.value = draft.content || '';
}
} catch (e) {
localStorage.removeItem('paste_draft');
}
}
function clearDraft() {
localStorage.removeItem('paste_draft');
}
// ── Keyboard shortcuts ──────────────────────────────────────────────────────
document.addEventListener('keydown', function (e) {
if (window.PBCFG?.features?.keyboard_shortcuts === false) return;
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
const textarea = document.getElementById('content');
if (textarea) { e.preventDefault(); textarea.focus(); }
}
});
// Removed redundant DOMContentLoaded at the bottom — handled by the single listener at the top.