- auth: when a background poll or save returns 401 (session expired while the UI is still open), the api() helpers now prompt for the password instead of letting the save fail silently. Concurrent 401s collapse onto a single dialog via a shared reauthPromise, and each caller retries its original request once after login. /api/auth/* is excluded from the retry path. Extracts promptLogin() in app.js and exposes it as window._reauth so both the core util.js api() and the plugin-SDK hyprcrd.js api() can share the flow. - tests: the "open-switches-bucket" detail test assumed buckets[0] was a plain top-level bucket, but the bar renders super-bucket parents with data-bucket-super and hides child buckets entirely, so the .bucket-tab.active[data-bucket] selector found nothing on setups with nested layouts. Test now picks two plain top-level buckets or skips. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
76 lines
2.4 KiB
JavaScript
76 lines
2.4 KiB
JavaScript
// ── Utilities ──
|
|
|
|
export function esc(s) {
|
|
const d = document.createElement('div');
|
|
d.textContent = s;
|
|
return d.innerHTML;
|
|
}
|
|
|
|
export function escRe(s) {
|
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
}
|
|
|
|
let csrfToken = '';
|
|
export function setCsrf(token) { csrfToken = token; }
|
|
export function getCsrf() { return csrfToken; }
|
|
|
|
export async function api(path, opts = {}) {
|
|
const doFetch = () => {
|
|
const headers = { 'Content-Type': 'application/json' };
|
|
if (csrfToken) headers['X-CSRF-Token'] = csrfToken;
|
|
return fetch(path, {
|
|
headers,
|
|
...opts,
|
|
body: opts.body ? JSON.stringify(opts.body) : undefined,
|
|
});
|
|
};
|
|
let res = await doFetch();
|
|
// Session expired mid-use: prompt for re-login, then retry once. Avoid
|
|
// recursion on auth endpoints themselves — promptLogin handles its own
|
|
// fetches directly.
|
|
if (res.status === 401 && !path.startsWith('api/auth/') && typeof window._reauth === 'function') {
|
|
await window._reauth();
|
|
res = await doFetch();
|
|
}
|
|
return res.json();
|
|
}
|
|
|
|
export function $(id) { return document.getElementById(id); }
|
|
|
|
export function fuzzyMatch(needle, haystack) {
|
|
if (needle.length < 3) return null;
|
|
let ni = 0, hi = 0, score = 0, streak = 0;
|
|
while (ni < needle.length && hi < haystack.length) {
|
|
if (needle[ni] === haystack[hi]) {
|
|
streak++; score += streak * 2;
|
|
if (hi === 0 || /[\s\-_./]/.test(haystack[hi - 1])) score += 10;
|
|
if (hi < 5) score += 5 - hi;
|
|
ni++;
|
|
} else { streak = 0; }
|
|
hi++;
|
|
}
|
|
if (ni < needle.length) return null;
|
|
return score - (haystack.length - needle.length) * 0.3;
|
|
}
|
|
|
|
export function countSubstring(str, sub) {
|
|
let c = 0, p = 0;
|
|
while ((p = str.indexOf(sub, p)) !== -1) { c++; p += sub.length; }
|
|
return c;
|
|
}
|
|
|
|
export function highlight(html, words) {
|
|
let result = html;
|
|
for (const w of words) result = result.replace(new RegExp('(' + escRe(w) + ')', 'gi'), '<mark>$1</mark>');
|
|
if (!result.includes('<mark>')) {
|
|
const full = words.join('');
|
|
let fi = 0, out = '';
|
|
for (let i = 0; i < html.length && fi < full.length; i++) {
|
|
if (html[i].toLowerCase() === full[fi]) { out += '<mark>' + html[i] + '</mark>'; fi++; } else { out += html[i]; }
|
|
}
|
|
out += html.substring(out.replace(/<\/?mark>/g, '').length);
|
|
result = out;
|
|
}
|
|
return result;
|
|
}
|