Plugin follows duplicate behavior behavior of new-entry

This commit is contained in:
2026-08-28 09:37:14 +02:00
parent 50d61371e1
commit 5696c89dee
14 changed files with 105 additions and 16 deletions
+4 -1
View File
@@ -42,7 +42,10 @@
"submissionFailed": {"message": "Senden fehlgeschlagen"},
"linkSaved": {"message": "Link auf $URL$ gespeichert.", "placeholders": {"url": {"content": "$1"}}},
"linkAlreadyExists": {"message": "Dieser Link existiert bereits. Kommentar und Tags wurden aktualisiert und die Veröffentlichung erneut ausgelöst."},
"duplicateLinkWarning": {"message": "Dieser Link existiert bereits. Kommentar und Tags können aktualisiert werden; beim Absenden wird die Veröffentlichung erneut ausgelöst."},
"duplicateLinkSameUrl": {"message": "Dieser Link existiert bereits.\nDu kannst den Eintrag trotzdem speichern; dabei werden Kommentar und Tags des bestehenden Eintrags aktualisiert."},
"duplicateLinkDifferentUrl": {"message": "Dieser Link existiert bereits. Der gespeicherte Link hat jedoch eine andere URL (fehlende oder andere Parameter).\nWenn du den Eintrag speicherst, riskierst du einen doppelten Eintrag."},
"refetchTitle": {"message": "Titel erneut abrufen"},
"titleFetchFailed": {"message": "Titel konnte nicht abgerufen werden"},
"publishingErrors": {"message": "Fehler bei der Veröffentlichung: $ERRORS$", "placeholders": {"errors": {"content": "$1"}}},
"submissionFailedConnection": {"message": "Senden fehlgeschlagen. Überprüfe die Verbindung zum Backend."},
"loggedInAt": {"message": "$USERNAME$ ist bei $BACKEND$ angemeldet", "placeholders": {"username": {"content": "$1"}, "backend": {"content": "$2"}}},
+11 -2
View File
@@ -133,8 +133,17 @@
"linkAlreadyExists": {
"message": "This link already exists. Comment and tags were updated, and publishing was retriggered."
},
"duplicateLinkWarning": {
"message": "This link already exists. Comment and tags can be updated, and by submitting publishing will be retriggered."
"duplicateLinkSameUrl": {
"message": "This link already exists.\nYou can still save the entry, which will update the existing entry's comment and or tags."
},
"duplicateLinkDifferentUrl": {
"message": "This link already exists. But, the stored link has a different URL (missing or different parameters).\nWhen you save the entry you risk a duplicate entry."
},
"refetchTitle": {
"message": "Re-fetch title"
},
"titleFetchFailed": {
"message": "Could not fetch title"
},
"publishingErrors": {
"message": "Publishing errors: $ERRORS$",
+4 -1
View File
@@ -42,7 +42,10 @@
"submissionFailed": {"message": "Error al enviar"},
"linkSaved": {"message": "Enlace guardado en $URL$.", "placeholders": {"url": {"content": "$1"}}},
"linkAlreadyExists": {"message": "Este enlace ya existe. Se actualizaron el comentario y las etiquetas, y se volvió a activar la publicación."},
"duplicateLinkWarning": {"message": "Este enlace ya existe. Puedes actualizar el comentario y las etiquetas; al enviarlo se volverá a activar la publicación."},
"duplicateLinkSameUrl": {"message": "Este enlace ya existe.\nAún puedes guardar la entrada, lo que actualizará el comentario y las etiquetas de la entrada existente."},
"duplicateLinkDifferentUrl": {"message": "Este enlace ya existe. Pero el enlace guardado tiene una URL diferente (parámetros ausentes o distintos).\nSi guardas la entrada, corres el riesgo de crear una entrada duplicada."},
"refetchTitle": {"message": "Volver a obtener el título"},
"titleFetchFailed": {"message": "No se pudo obtener el título"},
"publishingErrors": {"message": "Errores de publicación: $ERRORS$", "placeholders": {"errors": {"content": "$1"}}},
"submissionFailedConnection": {"message": "Error al enviar. Comprueba la conexión con el servidor."},
"loggedInAt": {"message": "$USERNAME$ ha iniciado sesión en $BACKEND$", "placeholders": {"username": {"content": "$1"}, "backend": {"content": "$2"}}},
+4 -1
View File
@@ -42,7 +42,10 @@
"submissionFailed": {"message": "Échec de lenvoi"},
"linkSaved": {"message": "Lien enregistré sur $URL$.", "placeholders": {"url": {"content": "$1"}}},
"linkAlreadyExists": {"message": "Ce lien existe déjà. Le commentaire et les étiquettes ont été mis à jour et la publication a été relancée."},
"duplicateLinkWarning": {"message": "Ce lien existe déjà. Le commentaire et les étiquettes peuvent être mis à jour ; lenvoi relancera la publication."},
"duplicateLinkSameUrl": {"message": "Ce lien existe déjà.\nVous pouvez tout de même enregistrer lentrée ; le commentaire et les étiquettes de lentrée existante seront mis à jour."},
"duplicateLinkDifferentUrl": {"message": "Ce lien existe déjà. Mais le lien enregistré a une URL différente (paramètres manquants ou différents).\nSi vous enregistrez lentrée, vous risquez de créer un doublon."},
"refetchTitle": {"message": "Récupérer à nouveau le titre"},
"titleFetchFailed": {"message": "Impossible de récupérer le titre"},
"publishingErrors": {"message": "Erreurs de publication : $ERRORS$", "placeholders": {"errors": {"content": "$1"}}},
"submissionFailedConnection": {"message": "Échec de lenvoi. Vérifiez la connexion au serveur."},
"loggedInAt": {"message": "$USERNAME$ est connecté à $BACKEND$", "placeholders": {"username": {"content": "$1"}, "backend": {"content": "$2"}}},
+4 -1
View File
@@ -42,7 +42,10 @@
"submissionFailed": {"message": "Verzenden mislukt"},
"linkSaved": {"message": "Koppeling opgeslagen op $URL$.", "placeholders": {"url": {"content": "$1"}}},
"linkAlreadyExists": {"message": "Deze koppeling bestaat al. De opmerking en tags zijn bijgewerkt en publiceren is opnieuw gestart."},
"duplicateLinkWarning": {"message": "Deze koppeling bestaat al. De opmerking en tags kunnen worden bijgewerkt; na verzenden wordt publiceren opnieuw gestart."},
"duplicateLinkSameUrl": {"message": "Deze koppeling bestaat al.\nJe kunt de invoer nog steeds opslaan; de opmerking en tags van de bestaande koppeling worden dan bijgewerkt."},
"duplicateLinkDifferentUrl": {"message": "Deze koppeling bestaat al. De opgeslagen koppeling heeft echter een andere URL (ontbrekende of andere parameters).\nAls je de invoer opslaat, loop je het risico op een dubbele koppeling."},
"refetchTitle": {"message": "Titel opnieuw ophalen"},
"titleFetchFailed": {"message": "Titel kon niet worden opgehaald"},
"publishingErrors": {"message": "Publicatiefouten: $ERRORS$", "placeholders": {"errors": {"content": "$1"}}},
"submissionFailedConnection": {"message": "Verzenden mislukt. Controleer de verbinding met de backend."},
"loggedInAt": {"message": "$USERNAME$ is ingelogd op $BACKEND$", "placeholders": {"username": {"content": "$1"}, "backend": {"content": "$2"}}},
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "__MSG_extensionName__",
"version": "0.1.0",
"version": "0.2.0",
"description": "__MSG_extensionDescription__",
"default_locale": "en-US",
"permissions": [
+1
View File
@@ -26,6 +26,7 @@
<span data-i18n="titleLabel">Title</span>
<input id="title" name="title" type="text" />
</label>
<button type="button" id="refetch-title" class="secondary" data-i18n="refetchTitle">Re-fetch title</button>
<label>
<span data-i18n="urlLabel">URL</span>
+60 -4
View File
@@ -13,6 +13,7 @@ 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 refetchTitleButton = document.getElementById('refetch-title');
const sessionStore = browser.storage.session;
const t = window.linklogI18n;
@@ -35,8 +36,14 @@ async function hasBackendPermission(backendUrl) {
return browser.permissions.contains({origins: [`${origin}/*`]});
}
// Splits on \n into real <br> line breaks without using innerHTML.
function setStatus(message, isError = false) {
statusEl.textContent = message;
const lines = message.split('\n');
statusEl.replaceChildren(
...lines.flatMap((line, index) => (
index === 0 ? [document.createTextNode(line)] : [document.createElement('br'), document.createTextNode(line)]
)),
);
statusEl.classList.remove('hidden');
statusEl.classList.toggle('error', isError);
statusEl.classList.toggle('success', !isError);
@@ -225,20 +232,54 @@ async function checkExistingLink() {
const settings = await getSettings();
if (!settings.backendUrl || !settings.accessToken || !(await hasBackendPermission(settings.backendUrl)) || !titleInput.value || !urlInput.value) return;
try {
const url = removeKnownTrackingParams(urlInput.value);
const response = await fetch(`${settings.backendUrl}/api/links/check?${new URLSearchParams({
title: titleInput.value,
url: removeKnownTrackingParams(urlInput.value),
url,
})}`, {
headers: {'Authorization': `Bearer ${settings.accessToken}`},
});
if (response.ok && (await response.json()).exists) {
setStatus(t('duplicateLinkWarning'), true);
if (!response.ok) return;
const data = await response.json();
if (data.exists) {
// Re-derive the match locally: browser URL normalization (e.g. trailing slashes) can
// make the server's raw string comparison disagree even when the URLs are equivalent.
const urlMatches = data.url_matches || (data.stored_url && removeKnownTrackingParams(data.stored_url) === url);
const message = urlMatches ? t('duplicateLinkSameUrl') : t('duplicateLinkDifferentUrl');
setStatus(message, true);
}
} catch (error) {
// Duplicate checking is advisory; submission remains available.
}
}
// Re-fetch the title from the backend scraper, for when the URL was edited after the initial tab-based fill.
let lastScrapedUrl = null;
async function fetchTitle(url, { force = false } = {}) {
const settings = await getSettings();
if (!settings.backendUrl || !settings.accessToken || !(await hasBackendPermission(settings.backendUrl))) return;
if (!url || (!force && (titleInput.value.trim() || url === lastScrapedUrl))) return;
try {
const response = await fetch(`${settings.backendUrl}/api/scrape?url=${encodeURIComponent(url)}`, {
headers: {'Authorization': `Bearer ${settings.accessToken}`},
});
if (!response.ok) {
if (force) setStatus(t('titleFetchFailed'), true);
return;
}
lastScrapedUrl = url;
const data = await response.json();
if (data.title) {
titleInput.value = data.title;
} else if (force) {
setStatus(t('titleFetchFailed'), true);
}
} catch (error) {
if (force) setStatus(t('titleFetchFailed'), true);
}
}
async function handleSubmit(event) {
event.preventDefault();
setStatus(t('submitting'), false);
@@ -305,6 +346,21 @@ async function handleSubmit(event) {
openSettingsButton.addEventListener('click', () => browser.runtime.openOptionsPage());
warningSettingsButton.addEventListener('click', () => browser.runtime.openOptionsPage());
form.addEventListener('submit', handleSubmit);
urlInput.addEventListener('blur', async () => {
await fetchTitle(removeKnownTrackingParams(urlInput.value.trim()));
checkExistingLink();
});
titleInput.addEventListener('blur', checkExistingLink);
refetchTitleButton.addEventListener('click', (e) => {
e.preventDefault();
const url = removeKnownTrackingParams(urlInput.value.trim());
if (!url) {
setStatus(t('titleFetchFailed'), true);
return;
}
titleInput.value = '';
fetchTitle(url, { force: true });
});
populateCurrentTab().then(checkExistingLink);
loadExistingTags();
updateFeedLink();