diff --git a/shaarli-pro/js/script.js b/shaarli-pro/js/script.js index 4c99b1b..896bc07 100644 --- a/shaarli-pro/js/script.js +++ b/shaarli-pro/js/script.js @@ -1,3 +1,139 @@ +// ===================================================================== +// Shared theme-config bookmark helper +// Maintains a single private bookmark titled "themes" tagged "shaarit_config" +// Used by the dark/light switch (script.js) AND the theme picker (tools.html) +// Prevents creating a new bookmark on every theme change (duplicate bug) +// ===================================================================== +(function () { + const CONFIG_TAG = 'shaarit_config'; + const LEGACY_TAG = 'themes'; + const CONFIG_TITLE = 'themes'; + const CONFIG_URL = 'https://shaarit.app/config/themes'; + + let inflight = null; // serialize concurrent saves + + function getBasePath() { + return (window.shaarli && window.shaarli.basePath) || ''; + } + + async function fetchDoc(url) { + const res = await fetch(url, { credentials: 'same-origin' }); + const text = await res.text(); + return new DOMParser().parseFromString(text, 'text/html'); + } + + // Returns array of { id, editUrl } for every bookmark matching the config, + // searched across the new tag and the legacy "themes" tag (dedup by id). + async function findCandidates() { + const basePath = getBasePath(); + const byId = new Map(); + for (const tag of [CONFIG_TAG, LEGACY_TAG]) { + let doc; + try { + doc = await fetchDoc(basePath + '/?searchtags=' + encodeURIComponent(tag)); + } catch (e) { continue; } + doc.querySelectorAll('.link-outer[data-id]').forEach(el => { + const id = el.getAttribute('data-id'); + if (!id || byId.has(id)) return; + byId.set(id, { id, editUrl: basePath + '/admin/shaare/' + id }); + }); + } + // Sort by numeric id asc so "primary" is deterministic (oldest kept) + return [...byId.values()].sort((a, b) => Number(a.id) - Number(b.id)); + } + + // Reads existing config JSON from the edit page's textarea (raw value, + // not affected by the markdown plugin). + async function readExistingConfig(editUrl) { + try { + const doc = await fetchDoc(editUrl); + const ta = doc.querySelector('textarea[name="lf_description"]'); + if (!ta) return null; + const raw = (ta.textContent || '').trim(); + if (!raw) return null; + return JSON.parse(raw); + } catch (e) { + return null; + } + } + + async function getTokenFrom(url) { + const doc = await fetchDoc(url); + const tokenEl = doc.querySelector('input[name="token"]'); + const idEl = doc.querySelector('input[name="lf_id"]'); + return { + token: tokenEl ? tokenEl.value : '', + lfId: idEl ? idEl.value : '', + }; + } + + async function deleteBookmark(id, token) { + if (!id || !token) return; + const basePath = getBasePath(); + try { + await fetch(basePath + '/admin/shaare/delete?id=' + encodeURIComponent(id) + + '&token=' + encodeURIComponent(token), { credentials: 'same-origin' }); + } catch (e) { /* ignore */ } + } + + // Merge partial updates into the persisted config and save to the single + // bookmark. Deletes any duplicates left over from previous buggy saves. + async function save(partial) { + // Serialize so rapid toggles do not race and create duplicates. + const run = async () => { + const basePath = getBasePath(); + const candidates = await findCandidates(); + const primary = candidates[0] || null; + const duplicates = candidates.slice(1); + + // Build merged config + let existing = null; + if (primary) existing = await readExistingConfig(primary.editUrl); + const base = existing && typeof existing === 'object' ? existing : {}; + const config = Object.assign( + { version: 2, themes: [], default: 'DEFAULT', mode: 'dark' }, + base, + partial || {} + ); + + // Get token + lf_id from the primary's edit page (or add page) + const src = primary ? primary.editUrl : basePath + '/admin/add-shaare'; + const { token, lfId } = await getTokenFrom(src); + if (!token) return { ok: false, reason: 'no-token' }; + + const fd = new FormData(); + fd.append('lf_title', CONFIG_TITLE); + fd.append('lf_url', CONFIG_URL); + fd.append('lf_tags', CONFIG_TAG); + fd.append('lf_description', JSON.stringify(config)); + fd.append('lf_private', 'on'); + fd.append('save_edit', 'Save'); + fd.append('token', token); + if (lfId) fd.append('lf_id', lfId); + + await fetch(basePath + '/admin/shaare', { + method: 'POST', + body: fd, + credentials: 'same-origin', + }); + + // Clean up any leftover duplicates (reuse the same token, Shaarli + // accepts the session token for both edit and delete). + for (const dup of duplicates) { + await deleteBookmark(dup.id, token); + } + + return { ok: true, id: lfId || (primary && primary.id) || null, config }; + }; + + const p = (inflight ? inflight.catch(() => {}) : Promise.resolve()).then(run); + inflight = p.finally(() => { if (inflight === p) inflight = null; }); + return inflight; + } + + window.ShaaritThemeConfig = { save, findCandidates }; +})(); + document.addEventListener('DOMContentLoaded', () => { // ===== Add Note Button Handler (Android convention) ===== const addNoteBtn = document.querySelector('.sidebar-add-note'); @@ -67,68 +203,9 @@ document.addEventListener('DOMContentLoaded', () => { async function syncModeToBookmark(mode) { try { - if (!window.shaarli || !window.shaarli.basePath) return; - - // Fetch current config bookmark - const res = await fetch(window.shaarli.basePath + '/?searchtags=themes'); - const text = await res.text(); - const parser = new DOMParser(); - const doc = parser.parseFromString(text, 'text/html'); - - let config = { version: 2, themes: [], default: 'DEFAULT', mode: mode }; - let editUrl = null; - - // Find existing config - const linkCard = doc.querySelector('.linklist-item'); - if (linkCard) { - const descEl = linkCard.querySelector('.link-description p'); - if (descEl) { - try { - const parsed = JSON.parse(descEl.textContent.trim()); - // Preserve existing data - if (parsed.themes) config.themes = parsed.themes; - if (parsed.default) config.default = parsed.default; - // Update mode - config.mode = mode; - } catch(e) {} - } - const editBtn = linkCard.querySelector('.link-action-edit'); - if (editBtn) editUrl = editBtn.getAttribute('href'); - } - - // Prepare form data - const formData = new FormData(); - formData.append('lf_title', 'themes'); - formData.append('lf_url', 'https://shaarit.app/config/themes'); - formData.append('lf_tags', 'themes'); - formData.append('lf_description', JSON.stringify(config)); - formData.append('lf_private', 'on'); - formData.append('save_edit', 'Save'); - - let token = ''; - - if (editUrl) { - // Update existing - const editRes = await fetch(editUrl); - const editDoc = parser.parseFromString(await editRes.text(), 'text/html'); - const tokenEl = editDoc.querySelector('input[name="token"]'); - const idEl = editDoc.querySelector('input[name="lf_id"]'); - if (tokenEl) token = tokenEl.value; - if (idEl) formData.append('lf_id', idEl.value); - } else { - // Create new - const addRes = await fetch(window.shaarli.basePath + '/admin/add-shaare'); - const addDoc = parser.parseFromString(await addRes.text(), 'text/html'); - const tokenEl = addDoc.querySelector('input[name="token"]'); - if (tokenEl) token = tokenEl.value; - } - - if (token) { - formData.append('token', token); - await fetch(window.shaarli.basePath + '/admin/shaare', { - method: 'POST', - body: formData - }); + if (!window.ShaaritThemeConfig) return; + const result = await window.ShaaritThemeConfig.save({ mode }); + if (result && result.ok) { console.log('[shaarit] Mode synced to bookmark:', mode); } } catch (e) { diff --git a/shaarli-pro/tools.html b/shaarli-pro/tools.html index 1ecb3ec..1b6aae7 100644 --- a/shaarli-pro/tools.html +++ b/shaarli-pro/tools.html @@ -654,77 +654,17 @@ async function syncThemeToBookmark(themeId) { try { - // We fetch the current config bookmark if any - const res = await fetch(shaarli.basePath + '/?searchtags=themes'); - const text = await res.text(); - const parser = new DOMParser(); - const doc = parser.parseFromString(text, 'text/html'); - - // Get current mode from localStorage - const currentMode = localStorage.getItem('theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); - - let config = { - version: 2, + if (!window.ShaaritThemeConfig) { + console.warn('ShaaritThemeConfig helper not available'); + return; + } + const currentMode = localStorage.getItem('theme') + || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); + await window.ShaaritThemeConfig.save({ themes: THEMES, default: themeId, - mode: currentMode - }; - let editUrl = null; - - // Find existing config - const linkCard = doc.querySelector('.linklist-item'); - if (linkCard) { - const descEl = linkCard.querySelector('.link-description p'); - if (descEl) { - try { - const parsed = JSON.parse(descEl.textContent.trim()); - // Preserve existing themes list if it exists and is valid - if (parsed.themes && Array.isArray(parsed.themes)) { - config.themes = parsed.themes; - } - // Update default and mode - config.default = themeId; - config.mode = currentMode; - } catch(e) {} - } - const editBtn = linkCard.querySelector('.link-action-edit'); - if (editBtn) editUrl = editBtn.getAttribute('href'); - } - - // Prepare form data - const formData = new FormData(); - formData.append('lf_title', 'themes'); - formData.append('lf_url', 'https://shaarit.app/config/themes'); - formData.append('lf_tags', 'themes'); - formData.append('lf_description', JSON.stringify(config)); - formData.append('lf_private', 'on'); // Private bookmark - formData.append('save_edit', 'Save'); - - let token = ''; - - if (editUrl) { - // Update existing - const editRes = await fetch(editUrl); - const editDoc = parser.parseFromString(await editRes.text(), 'text/html'); - const tokenEl = editDoc.querySelector('input[name="token"]'); - const idEl = editDoc.querySelector('input[name="lf_id"]'); - if (tokenEl) token = tokenEl.value; - if (idEl) formData.append('lf_id', idEl.value); - } else { - // Create new - const addRes = await fetch(shaarli.basePath + '/admin/add-shaare'); - const addDoc = parser.parseFromString(await addRes.text(), 'text/html'); - const tokenEl = addDoc.querySelector('input[name="token"]'); - if (tokenEl) token = tokenEl.value; - } - - if (token) { - formData.append('token', token); - await fetch(shaarli.basePath + '/admin/shaare', { - method: 'POST', - body: formData - }); - } + mode: currentMode, + }); } catch (e) { console.error('Failed to sync theme to bookmark', e); }