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:
parent
f3147bb67b
commit
f90f8146ce
@ -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) {
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user