d65f0b2091
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 20s
extension / lint (push) Successful in 13s
CI / intimp (push) Successful in 3m41s
CI / intapi (push) Successful in 7m11s
CI / intcore (push) Successful in 7m41s
Operator-asked 2026-05-31 (during sidecar synthetic anchor cleanup): "the add source/subscription button idea to the firefox extension so it can tell me if a source/artist is added or not and offer an option to add it if it isn't." Plan tracked in Scribe task #507. ## Backend - `ExtensionService.probe(url)` — read-only resolution. Reuses `_derive` for platform+slug, then 2 SELECTs. Returns one of: - `source_match` (exact (artist, platform, url) Source exists) - `artist_match` (artist exists, this URL isn't a Source yet; collapses the sidecar-synthetic-only case from v26.06.01.0) - `new` (neither exists) - `unknown_platform` (URL didn't match any artist-page regex) - `GET /api/extension/probe?url=...` route with `X-Extension-Key` auth posture matching `/quick-add-source`. Read-only, side-effect free. - 6 backend tests in tests/test_api_extension.py covering each state + auth + invalid URL. ## Extension - `api.js`: `probeSource(url)` mirroring `quickAddSource` shape. - `background.js`: `PROBE_SOURCE` + `OPEN_ARTIST_PAGE` handlers. The latter strips the `/api` suffix from configured `apiUrl` (placeholder format per options.html) and opens `${base}/artist/{slug}` in a new tab via `browser.tabs.create`. - `content-script.js`: probe-first render — on page-load and SPA navigation, asks the backend for the URL's state and renders the chip in the matching color/copy on FIRST paint instead of flashing generic "Add" and updating after. Click handler branches: `source_match` → OPEN_ARTIST_PAGE; `artist_match`/`new` → existing ADD_AS_SOURCE flow (then re-probes so the chip flips green immediately, no wait for next nav). - `content-script.css`: three state-color modifiers (--new, --artist-match, --source-match) on the FC parchment-on-slate palette. Sage for already-added, amber for artist-exists, accent orange for new. ## Versioning - `extension/manifest.json` + `extension/package.json` → 1.0.6. build.yml's sign-extension job will fire on push to main since no `ext-1.0.6` Forgejo/Gitea release exists yet — exercises the regenerated AMO keys end-to-end. ## Behavior on the sidecar-synthetic case Filesystem-imported "Dymkens"-style artist with only a sidecar synthetic Source: probe returns `artist_match` (not `new`), so the chip reads "+ Add Patreon source to Dymkens" rather than offering to recreate the artist. Clicking adds the real Source; existing `_source_for_sidecar` preference logic (v26.06.01.0) routes future gallery-dl Posts to the real one.
143 lines
4.3 KiB
JavaScript
143 lines
4.3 KiB
JavaScript
(function () {
|
|
if (window.__fc_addsource_injected) return;
|
|
window.__fc_addsource_injected = true;
|
|
|
|
// Cached probe result for the current URL so click-handlers know which
|
|
// action to dispatch without round-tripping again.
|
|
let currentProbe = null;
|
|
|
|
evaluate();
|
|
|
|
const reEval = () => evaluate();
|
|
window.addEventListener('popstate', reEval);
|
|
const origPush = history.pushState;
|
|
history.pushState = function () { origPush.apply(this, arguments); reEval(); };
|
|
|
|
async function evaluate() {
|
|
const url = window.location.href;
|
|
const platform = getPlatformFromUrl(url);
|
|
const onArtist = platform && isArtistPage(url, platform);
|
|
const btn = document.getElementById('fc-add-source-btn');
|
|
if (!onArtist) {
|
|
if (btn) btn.remove();
|
|
currentProbe = null;
|
|
return;
|
|
}
|
|
// On artist pages, ask the backend what state the URL is in BEFORE
|
|
// injecting the button — so the chip can render the right state on
|
|
// first paint instead of flashing the generic "Add" copy and
|
|
// updating afterwards.
|
|
let probe;
|
|
try {
|
|
probe = await browser.runtime.sendMessage({ type: 'PROBE_SOURCE', url });
|
|
} catch (e) {
|
|
probe = { error: e?.message || 'probe failed' };
|
|
}
|
|
currentProbe = probe;
|
|
if (probe?.state === 'unknown_platform') {
|
|
if (btn) btn.remove();
|
|
return;
|
|
}
|
|
renderButton(probe);
|
|
}
|
|
|
|
function renderButton(probe) {
|
|
let btn = document.getElementById('fc-add-source-btn');
|
|
if (!btn) {
|
|
btn = document.createElement('button');
|
|
btn.id = 'fc-add-source-btn';
|
|
btn.addEventListener('click', onClick);
|
|
document.body.appendChild(btn);
|
|
}
|
|
// Reset state classes so re-renders (SPA navigation) don't stack.
|
|
btn.className = 'fc-add-source-btn';
|
|
btn.classList.add(`fc-add-source-btn--${stateModifier(probe)}`);
|
|
btn.textContent = labelFor(probe);
|
|
btn.disabled = false;
|
|
}
|
|
|
|
function stateModifier(probe) {
|
|
if (!probe || probe.error) return 'new';
|
|
return ({
|
|
source_match: 'source-match',
|
|
artist_match: 'artist-match',
|
|
new: 'new',
|
|
})[probe.state] || 'new';
|
|
}
|
|
|
|
function labelFor(probe) {
|
|
if (!probe || probe.error) return '+ Add to FabledCurator';
|
|
const platformName = platformDisplayName(probe.platform);
|
|
const artistName = probe.artist?.name;
|
|
switch (probe.state) {
|
|
case 'source_match':
|
|
return `✓ In FabledCurator · ${platformName}`;
|
|
case 'artist_match':
|
|
return `+ Add ${platformName} source to ${artistName || 'artist'}`;
|
|
case 'new':
|
|
default:
|
|
return '+ Add to FabledCurator';
|
|
}
|
|
}
|
|
|
|
function platformDisplayName(key) {
|
|
return PLATFORMS[key]?.name || key || '';
|
|
}
|
|
|
|
async function onClick() {
|
|
const btn = document.getElementById('fc-add-source-btn');
|
|
if (!btn) return;
|
|
btn.disabled = true;
|
|
const original = btn.textContent;
|
|
const probe = currentProbe;
|
|
|
|
if (probe?.state === 'source_match') {
|
|
btn.textContent = 'Opening…';
|
|
try {
|
|
const r = await browser.runtime.sendMessage({
|
|
type: 'OPEN_ARTIST_PAGE',
|
|
slug: probe.artist?.slug,
|
|
});
|
|
if (r?.error) showToast(`Error: ${r.error}`, 'error');
|
|
} catch (e) {
|
|
showToast(`Error: ${e.message}`, 'error');
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.textContent = original;
|
|
}
|
|
return;
|
|
}
|
|
|
|
btn.textContent = 'Adding…';
|
|
try {
|
|
const r = await browser.runtime.sendMessage({
|
|
type: 'ADD_AS_SOURCE',
|
|
url: window.location.href,
|
|
});
|
|
if (r?.error) {
|
|
showToast(`Error: ${r.error}`, 'error');
|
|
} else {
|
|
const verb = r.created_source ? 'Added' : 'Already a source for';
|
|
showToast(`${verb} ${r.artist?.name || 'artist'} (${r.source?.platform || ''})`, 'success');
|
|
// Re-probe so the chip flips green without waiting for the next
|
|
// navigation.
|
|
evaluate();
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
showToast(`Error: ${e.message}`, 'error');
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.textContent = original;
|
|
}
|
|
}
|
|
|
|
function showToast(text, kind) {
|
|
const t = document.createElement('div');
|
|
t.className = `fc-toast fc-toast--${kind}`;
|
|
t.textContent = text;
|
|
document.body.appendChild(t);
|
|
setTimeout(() => t.remove(), 4000);
|
|
}
|
|
})();
|