// Copyright © 2026 Olaf Kolkman // SPDX-License-Identifier: GPL-3.0-or-later const statusEl = document.getElementById('status'); const form = document.getElementById('link-form'); const titleInput = document.getElementById('title'); const urlInput = document.getElementById('url'); const commentInput = document.getElementById('comment'); const openSettingsButton = document.getElementById('open-settings'); const existingTags = document.getElementById('existing-tags'); const newTagsInput = document.getElementById('new-tags'); const feedLink = document.getElementById('feed-link'); const authWarning = document.getElementById('auth-warning'); const warningSettingsButton = document.getElementById('warning-settings'); const authSession = document.getElementById('auth-session'); const t = window.linklogI18n; function setStatus(message, isError = false) { statusEl.textContent = message; statusEl.classList.remove('hidden'); statusEl.classList.toggle('error', isError); statusEl.classList.toggle('success', !isError); } async function getSettings() { const result = await browser.storage.local.get([ 'backendUrl', 'accessToken', 'tokenExpiresAt', ]); return result; } async function validateSession(settings) { if (!settings.backendUrl || !settings.accessToken) return null; try { const response = await fetch(`${settings.backendUrl}/api/auth/me`, { headers: {'Authorization': `Bearer ${settings.accessToken}`}, }); if (!response.ok) { await browser.storage.local.remove(['accessToken', 'tokenType', 'tokenExpiresAt', 'refreshToken']); return null; } return await response.json(); } catch (error) { return null; } } function showSignedOutState() { form.classList.add('hidden'); authSession.hidden = true; authSession.classList.add('hidden'); authWarning.hidden = false; authWarning.classList.remove('hidden'); } function showSignedInState(user, backendUrl) { authWarning.hidden = true; authWarning.classList.add('hidden'); authSession.textContent = t('loggedInAt', [user.username || user.email, backendUrl]); authSession.hidden = false; authSession.classList.remove('hidden'); form.classList.remove('hidden'); } function showSavedState(message) { document.querySelector('header').classList.add('hidden'); document.querySelector('.extension-footer').classList.add('hidden'); authSession.hidden = true; authSession.classList.add('hidden'); authWarning.hidden = true; authWarning.classList.add('hidden'); form.classList.add('hidden'); statusEl.textContent = message; statusEl.classList.remove('hidden', 'error'); statusEl.classList.add('success'); } async function updateFeedLink() { const settings = await browser.storage.local.get(['backendUrl', 'username', 'accessToken']); if (!settings.backendUrl || !settings.username || !settings.accessToken) return; try { const backend = new URL(settings.backendUrl); feedLink.href = `${backend.origin}/${encodeURIComponent(settings.username)}/`; feedLink.classList.remove('hidden'); } catch (error) { feedLink.classList.add('hidden'); } } async function loadExistingTags() { const settings = await getSettings(); if (!settings.backendUrl || !settings.accessToken) { showSignedOutState(); return; } const user = await validateSession(settings); if (!user) { showSignedOutState(); return; } showSignedInState(user, settings.backendUrl); const response = await fetch(`${settings.backendUrl}/api/tags`, { headers: {'Authorization': `Bearer ${settings.accessToken}`}, }); if (!response.ok) { if (response.status === 401) { showSignedOutState(); return; } existingTags.textContent = t('loadTagsFailed'); return; } const tags = await response.json(); existingTags.replaceChildren(...tags.map((tag) => { const label = document.createElement('label'); const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.value = tag; label.append(checkbox, document.createTextNode(` ${tag}`)); return label; })); } function getTags() { const selected = [...existingTags.querySelectorAll('input:checked')].map((input) => input.value); const newTags = newTagsInput.value.split(',').map((tag) => tag.trim()).filter((tag) => tag.startsWith('#')); return [...new Set([...selected, ...newTags])].slice(0, 10); } function removeKnownTrackingParams(urlString) { try { const url = new URL(urlString); const known = new Set([ 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'utm_id', 'utm_name', 'gclid', 'fbclid', 'dclid', 'msclkid' ]); for (const key of known) { url.searchParams.delete(key); } return url.toString(); } catch (error) { return urlString; } } async function populateCurrentTab() { const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); if (!tab) return; titleInput.value = tab.title || ''; urlInput.value = tab.url || ''; } async function checkExistingLink() { const settings = await getSettings(); if (!settings.backendUrl || !settings.accessToken || !titleInput.value || !urlInput.value) return; try { const response = await fetch(`${settings.backendUrl}/api/links/check?${new URLSearchParams({ title: titleInput.value, url: removeKnownTrackingParams(urlInput.value), })}`, { headers: {'Authorization': `Bearer ${settings.accessToken}`}, }); if (response.ok && (await response.json()).exists) { setStatus(t('duplicateLinkWarning'), true); } } catch (error) { // Duplicate checking is advisory; submission remains available. } } async function handleSubmit(event) { event.preventDefault(); setStatus(t('submitting'), false); const settings = await getSettings(); const token = settings.accessToken; const backendUrl = settings.backendUrl; if (!token || !backendUrl) { setStatus(t('configureAndLogIn'), true); browser.runtime.openOptionsPage(); return; } if (!await validateSession(settings)) { showSignedOutState(); setStatus(t('sessionExpired'), true); browser.runtime.openOptionsPage(); return; } try { const response = await fetch(`${backendUrl}/api/links`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }, body: JSON.stringify({ title: titleInput.value, url: removeKnownTrackingParams(urlInput.value), comment: commentInput.value, timestamp: new Date().toISOString(), tags: getTags(), }) }); if (response.status === 401) { showSignedOutState(); setStatus(t('sessionExpired'), true); browser.runtime.openOptionsPage(); return; } if (!response.ok) { throw new Error(t('submissionFailed')); } const result = await response.json(); const saveMessage = result.duplicate ? t('linkAlreadyExists') : t('linkSaved', backendUrl); if (result.plugin_errors?.length) { const errors = result.plugin_errors.map((error) => `${error.plugin}: ${error.reason}`).join(' '); showSavedState(`${saveMessage} ${t('publishingErrors', errors)}`); return; } showSavedState(saveMessage); } catch (error) { setStatus(t('submissionFailedConnection'), true); } } openSettingsButton.addEventListener('click', () => browser.runtime.openOptionsPage()); warningSettingsButton.addEventListener('click', () => browser.runtime.openOptionsPage()); form.addEventListener('submit', handleSubmit); populateCurrentTab().then(checkExistingLink); loadExistingTags(); updateFeedLink();