forked from ComputerTech/bastebin
245 lines
8.8 KiB
JavaScript
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.
|