d3245f0c22
CI / lint (push) Successful in 4s
CI / backend-lint-and-test (push) Successful in 37s
CI / frontend-build (push) Successful in 40s
extension / lint (push) Successful in 36s
CI / intimp (push) Successful in 4m4s
CI / intapi (push) Successful in 8m2s
CI / intcore (push) Successful in 8m45s
extension / lint (pull_request) Successful in 14s
Pre-upload verify request: after capturing the live browser cookies,
hit a known authenticated endpoint with credentials:'include' from the
extension's background context. If the platform reports we're not
logged in, abort the upload so we don't overwrite FC-side credentials
with stale data.
- platforms.js: add `verify` config per cookie-auth platform
- hentaifoundry: HEAD /?enterAgree=1 (mirrors gallery-dl's HF
_init_site_filters; same 401 path the operator hit 2026-06-03)
- patreon: GET /api/current_user (clean 401 when logged out)
- subscribestar, deviantart: no stable auth endpoint, skip verify
- cookies.js: verifyCookiesForPlatform() returns {ok, status, reason}.
ok=true/false/null tri-state — null = verify not configured, caller
treats as "proceed".
- background.js EXPORT_COOKIES + EXPORT_ALL_COOKIES: verify gates the
upload; failures bubble up with the platform's name + reason.
- popup.js: success message now appends "(verified ✓)" when applicable.
- manifest + package.json: 1.0.6 → 1.0.7.
241 lines
9.3 KiB
JavaScript
241 lines
9.3 KiB
JavaScript
document.addEventListener('DOMContentLoaded', init);
|
|
|
|
const CONNECTION_TEST_INTERVAL = 2 * 60 * 1000;
|
|
|
|
async function init() {
|
|
try {
|
|
const cfg = await browser.runtime.sendMessage({ type: 'GET_CONFIG' });
|
|
if (!cfg || !cfg.apiUrl || !cfg.apiKey) {
|
|
showSetupRequired();
|
|
return;
|
|
}
|
|
document.getElementById('setup-required').classList.add('hidden');
|
|
document.getElementById('main-content').classList.remove('hidden');
|
|
setupEventListeners();
|
|
showPlatformsLoading();
|
|
testConnectionIfNeeded();
|
|
loadPlatformStatus().catch(e => showError(`Failed to load platforms: ${e.message}`));
|
|
} catch (e) {
|
|
showSetupRequired();
|
|
const alert = document.querySelector('#setup-required .alert');
|
|
alert.textContent = '';
|
|
const s = document.createElement('strong'); s.textContent = 'Error';
|
|
const p = document.createElement('p'); p.textContent = e.message;
|
|
alert.appendChild(s); alert.appendChild(p);
|
|
alert.classList.add('alert-error');
|
|
}
|
|
}
|
|
|
|
function showSetupRequired() {
|
|
document.getElementById('setup-required').classList.remove('hidden');
|
|
document.getElementById('main-content').classList.add('hidden');
|
|
document.getElementById('open-settings-btn').addEventListener('click', () => {
|
|
browser.runtime.openOptionsPage();
|
|
});
|
|
}
|
|
|
|
function showPlatformsLoading() {
|
|
const c = document.getElementById('platforms-list');
|
|
c.textContent = '';
|
|
const d = document.createElement('div');
|
|
d.style.cssText = 'text-align:center;padding:18px;color:var(--on-surface-variant);';
|
|
d.textContent = 'Loading platforms…';
|
|
c.appendChild(d);
|
|
}
|
|
|
|
async function testConnectionIfNeeded() {
|
|
const stored = await browser.storage.local.get(['lastConnectionTest', 'lastConnectionStatus']);
|
|
const now = Date.now();
|
|
if (now - (stored.lastConnectionTest || 0) < CONNECTION_TEST_INTERVAL && stored.lastConnectionStatus !== undefined) {
|
|
updateConnectionDot(stored.lastConnectionStatus);
|
|
if (!stored.lastConnectionStatus) showError('Cannot connect to backend (cached).');
|
|
return;
|
|
}
|
|
const r = await browser.runtime.sendMessage({ type: 'TEST_CONNECTION' });
|
|
await browser.storage.local.set({ lastConnectionTest: now, lastConnectionStatus: r.connected });
|
|
updateConnectionDot(r.connected);
|
|
if (!r.connected) showError(`Cannot connect to backend: ${r.error}`);
|
|
}
|
|
|
|
function updateConnectionDot(connected) {
|
|
const d = document.getElementById('connection-status');
|
|
d.classList.toggle('connected', connected);
|
|
d.title = connected ? 'Connected to FabledCurator' : 'Disconnected';
|
|
}
|
|
|
|
async function loadPlatformStatus() {
|
|
const status = await browser.runtime.sendMessage({ type: 'GET_PLATFORM_STATUS' });
|
|
const c = document.getElementById('platforms-list');
|
|
c.textContent = '';
|
|
for (const [key, platform] of Object.entries(PLATFORMS)) {
|
|
c.appendChild(createPlatformCard(key, platform, status[key] || {}));
|
|
}
|
|
}
|
|
|
|
function createPlatformCard(key, platform, status) {
|
|
const card = document.createElement('div');
|
|
card.className = 'platform-card';
|
|
card.dataset.platform = key;
|
|
|
|
const isTokenOnly = platform.authType === 'token' && !['discord', 'pixiv'].includes(key);
|
|
const discordNeedsToken = key === 'discord' && !status.hasToken;
|
|
if (isTokenOnly || discordNeedsToken) card.classList.add('disabled');
|
|
|
|
const icon = document.createElement('div');
|
|
icon.className = 'platform-icon';
|
|
icon.style.background = platform.color;
|
|
icon.textContent = platform.name[0];
|
|
|
|
const info = document.createElement('div');
|
|
info.className = 'info';
|
|
const name = document.createElement('div');
|
|
name.className = 'name';
|
|
name.textContent = platform.name;
|
|
const st = document.createElement('div');
|
|
st.className = `status ${statusClass(status, platform, key)}`;
|
|
st.textContent = statusText(status, platform, key);
|
|
info.appendChild(name); info.appendChild(st);
|
|
|
|
const act = document.createElement('span');
|
|
act.className = 'action-icon';
|
|
act.textContent = '↥';
|
|
|
|
card.appendChild(icon); card.appendChild(info); card.appendChild(act);
|
|
|
|
if (!isTokenOnly && !discordNeedsToken) {
|
|
card.addEventListener('click', () => exportPlatformCookies(key, card));
|
|
}
|
|
return card;
|
|
}
|
|
|
|
function statusText(s, platform, key) {
|
|
if (key === 'discord') return s.hasToken ? 'Token captured — ready' : 'Open Discord to capture token';
|
|
if (key === 'pixiv') return s.hasToken ? 'Token captured — ready' : 'Click to authenticate via OAuth';
|
|
if (platform.authType === 'token') return 'Manual token entry required';
|
|
if (s.error) return 'Error checking cookies';
|
|
if (!s.hasCookies || !s.cookieCount) return 'No cookies — log in first';
|
|
return `${s.cookieCount} cookies ready`;
|
|
}
|
|
function statusClass(s, platform, key) {
|
|
if (key === 'discord') return s.hasToken ? 'ready' : 'no-cookies';
|
|
if (key === 'pixiv') return s.hasToken ? 'ready' : 'no-cookies';
|
|
if (platform.authType === 'token') return 'no-cookies';
|
|
if (s.error) return 'error';
|
|
if (!s.hasCookies || !s.cookieCount) return 'no-cookies';
|
|
return 'ready';
|
|
}
|
|
|
|
async function exportPlatformCookies(key, card) {
|
|
card.classList.add('loading'); hideStatusMessage();
|
|
try {
|
|
const r = await browser.runtime.sendMessage({ type: 'EXPORT_COOKIES', platform: key });
|
|
if (r.error) showError(r.error);
|
|
else {
|
|
const n = r.cookieCount ?? null;
|
|
const verifiedSuffix = r.verified ? ' (verified ✓)' : '';
|
|
const msg = n !== null
|
|
? `${PLATFORMS[key].name}: ${n} cookies exported${verifiedSuffix}`
|
|
: `${PLATFORMS[key].name}: token exported`;
|
|
showSuccess(msg);
|
|
await loadPlatformStatus();
|
|
}
|
|
} catch (e) { showError(e.message); }
|
|
finally { card.classList.remove('loading'); }
|
|
}
|
|
|
|
async function exportAllCookies() {
|
|
const btn = document.getElementById('export-all-btn');
|
|
btn.disabled = true; btn.textContent = 'Exporting…'; hideStatusMessage();
|
|
try {
|
|
const r = await browser.runtime.sendMessage({ type: 'EXPORT_ALL_COOKIES' });
|
|
const wins = Object.values(r).filter(x => x.success).length;
|
|
const fails = Object.values(r).filter(x => !x.success && !x.skipped).length;
|
|
if (wins && !fails) showSuccess(`Exported ${wins} platforms`);
|
|
else if (wins) showWarning(`${wins} succeeded, ${fails} failed`);
|
|
else if (fails) showError('All exports failed. Are you logged in?');
|
|
else showWarning('Nothing to export');
|
|
await loadPlatformStatus();
|
|
} catch (e) { showError(e.message); }
|
|
finally { btn.disabled = false; btn.textContent = 'Export all platforms'; }
|
|
}
|
|
|
|
async function loadSources() {
|
|
const c = document.getElementById('sources-list');
|
|
c.textContent = '';
|
|
const d = document.createElement('div');
|
|
d.style.cssText = 'text-align:center;padding:18px;color:var(--on-surface-variant);';
|
|
d.textContent = 'Loading sources…';
|
|
c.appendChild(d);
|
|
const r = await browser.runtime.sendMessage({ type: 'LIST_SOURCES' });
|
|
c.textContent = '';
|
|
if (r.error) {
|
|
const e = document.createElement('div');
|
|
e.style.cssText = 'padding:12px;color:var(--error);';
|
|
e.textContent = r.error;
|
|
c.appendChild(e);
|
|
return;
|
|
}
|
|
if (!r.sources || r.sources.length === 0) {
|
|
const empty = document.createElement('div');
|
|
empty.style.cssText = 'text-align:center;padding:18px;color:var(--on-surface-variant);';
|
|
empty.textContent = 'No sources yet.';
|
|
c.appendChild(empty);
|
|
return;
|
|
}
|
|
for (const src of r.sources) c.appendChild(createSourceRow(src));
|
|
}
|
|
|
|
function createSourceRow(src) {
|
|
const row = document.createElement('div');
|
|
row.className = 'source-row';
|
|
const info = document.createElement('div');
|
|
info.className = 'info';
|
|
const name = document.createElement('div');
|
|
name.className = 'name';
|
|
name.textContent = `${src.platform} · #${src.id}`;
|
|
const url = document.createElement('div');
|
|
url.className = 'url';
|
|
url.textContent = src.url;
|
|
info.appendChild(name); info.appendChild(url);
|
|
const play = document.createElement('button');
|
|
play.className = 'play';
|
|
play.textContent = '▶';
|
|
play.title = 'Check now';
|
|
play.addEventListener('click', async () => {
|
|
play.disabled = true;
|
|
const r = await browser.runtime.sendMessage({ type: 'CHECK_SOURCE', sourceId: src.id });
|
|
play.disabled = false;
|
|
if (r.error) showError(r.error);
|
|
else showSuccess(`Triggered check for source #${src.id}`);
|
|
});
|
|
row.appendChild(info); row.appendChild(play);
|
|
return row;
|
|
}
|
|
|
|
function setupEventListeners() {
|
|
document.getElementById('export-all-btn').addEventListener('click', exportAllCookies);
|
|
document.getElementById('settings-btn').addEventListener('click', () => browser.runtime.openOptionsPage());
|
|
for (const tab of document.querySelectorAll('.tab')) {
|
|
tab.addEventListener('click', () => {
|
|
for (const t of document.querySelectorAll('.tab')) t.classList.remove('active');
|
|
tab.classList.add('active');
|
|
for (const p of document.querySelectorAll('.tab-panel')) p.classList.add('hidden');
|
|
document.getElementById(`tab-${tab.dataset.tab}`).classList.remove('hidden');
|
|
if (tab.dataset.tab === 'sources') loadSources();
|
|
});
|
|
}
|
|
}
|
|
|
|
function showSuccess(m) { showStatusMessage(m, 'success'); }
|
|
function showError(m) { showStatusMessage(m, 'error'); }
|
|
function showWarning(m) { showStatusMessage(m, 'warning'); }
|
|
function showStatusMessage(text, kind) {
|
|
const el = document.getElementById('status-message');
|
|
el.textContent = text;
|
|
el.className = `status-message ${kind}`;
|
|
el.classList.remove('hidden');
|
|
}
|
|
function hideStatusMessage() {
|
|
document.getElementById('status-message').classList.add('hidden');
|
|
}
|