feat: centraliser gestion bookmark config thème dans helper ShaaritThemeConfig partagé pour éliminer duplications lors changements mode/thème, implémenter recherche multi-tags (shaarit_config + legacy themes) avec déduplication par ID, merge intelligent config existant avec partial updates, sérialisation saves concurrents via inflight promise, cleanup automatique duplicates avec deleteBookmark(), et refactoring syncModeToBookmark/syncThemeToBookmark pour déléguer à ShaaritThemeConfig.save

This commit is contained in:
Bruno Charest 2026-04-20 20:39:32 -04:00
parent f3147bb67b
commit f90f8146ce
2 changed files with 148 additions and 131 deletions

View File

@ -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', () => { document.addEventListener('DOMContentLoaded', () => {
// ===== Add Note Button Handler (Android convention) ===== // ===== Add Note Button Handler (Android convention) =====
const addNoteBtn = document.querySelector('.sidebar-add-note'); const addNoteBtn = document.querySelector('.sidebar-add-note');
@ -67,68 +203,9 @@ document.addEventListener('DOMContentLoaded', () => {
async function syncModeToBookmark(mode) { async function syncModeToBookmark(mode) {
try { try {
if (!window.shaarli || !window.shaarli.basePath) return; if (!window.ShaaritThemeConfig) return;
const result = await window.ShaaritThemeConfig.save({ mode });
// Fetch current config bookmark if (result && result.ok) {
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
});
console.log('[shaarit] Mode synced to bookmark:', mode); console.log('[shaarit] Mode synced to bookmark:', mode);
} }
} catch (e) { } catch (e) {

View File

@ -654,77 +654,17 @@
async function syncThemeToBookmark(themeId) { async function syncThemeToBookmark(themeId) {
try { try {
// We fetch the current config bookmark if any if (!window.ShaaritThemeConfig) {
const res = await fetch(shaarli.basePath + '/?searchtags=themes'); console.warn('ShaaritThemeConfig helper not available');
const text = await res.text(); return;
const parser = new DOMParser(); }
const doc = parser.parseFromString(text, 'text/html'); const currentMode = localStorage.getItem('theme')
|| (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
// Get current mode from localStorage await window.ShaaritThemeConfig.save({
const currentMode = localStorage.getItem('theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
let config = {
version: 2,
themes: THEMES, themes: THEMES,
default: themeId, default: themeId,
mode: currentMode 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
}); });
}
} catch (e) { } catch (e) {
console.error('Failed to sync theme to bookmark', e); console.error('Failed to sync theme to bookmark', e);
} }