Character count in firefox plugin
Build LinkLog Development Image / development-image (push) Successful in 8s

This commit is contained in:
2026-09-06 10:48:07 +02:00
parent 443aff8323
commit 0e04c583a6
13 changed files with 141 additions and 6 deletions
+3 -1
View File
@@ -53,5 +53,7 @@
"loginFailed": {"message": "Anmeldung fehlgeschlagen"},
"loggedInSuccessfully": {"message": "Erfolgreich angemeldet"},
"unableToLogIn": {"message": "Anmeldung nicht möglich. Überprüfe Backend-URL und Zugangsdaten."},
"signedOut": {"message": "Abgemeldet"}
"signedOut": {"message": "Abgemeldet"},
"characterCount": {"message": "$USED$/$LIMIT$ Zeichen", "placeholders": {"used": {"content": "$1"}, "limit": {"content": "$2"}}},
"characterCountOverLimit": {"message": "$USED$/$LIMIT$ Zeichen - überschreitet das Mastodon-Beitragslimit um $OVER$", "placeholders": {"used": {"content": "$1"}, "limit": {"content": "$2"}, "over": {"content": "$3"}}}
}
+25
View File
@@ -181,5 +181,30 @@
},
"signedOut": {
"message": "Signed out"
},
"characterCount": {
"message": "$USED$/$LIMIT$ characters",
"placeholders": {
"used": {
"content": "$1"
},
"limit": {
"content": "$2"
}
}
},
"characterCountOverLimit": {
"message": "$USED$/$LIMIT$ characters - exceeds the Mastodon post limit by $OVER$",
"placeholders": {
"used": {
"content": "$1"
},
"limit": {
"content": "$2"
},
"over": {
"content": "$3"
}
}
}
}
+3 -1
View File
@@ -53,5 +53,7 @@
"loginFailed": {"message": "Error de inicio de sesión"},
"loggedInSuccessfully": {"message": "Sesión iniciada correctamente"},
"unableToLogIn": {"message": "No se pudo iniciar sesión. Comprueba la URL y las credenciales."},
"signedOut": {"message": "Sesión cerrada"}
"signedOut": {"message": "Sesión cerrada"},
"characterCount": {"message": "$USED$/$LIMIT$ caracteres", "placeholders": {"used": {"content": "$1"}, "limit": {"content": "$2"}}},
"characterCountOverLimit": {"message": "$USED$/$LIMIT$ caracteres: supera el límite de publicación de Mastodon en $OVER$", "placeholders": {"used": {"content": "$1"}, "limit": {"content": "$2"}, "over": {"content": "$3"}}}
}
+3 -1
View File
@@ -53,5 +53,7 @@
"loginFailed": {"message": "Échec de la connexion"},
"loggedInSuccessfully": {"message": "Connexion réussie"},
"unableToLogIn": {"message": "Connexion impossible. Vérifiez lURL du serveur et vos identifiants."},
"signedOut": {"message": "Déconnecté"}
"signedOut": {"message": "Déconnecté"},
"characterCount": {"message": "$USED$/$LIMIT$ caractères", "placeholders": {"used": {"content": "$1"}, "limit": {"content": "$2"}}},
"characterCountOverLimit": {"message": "$USED$/$LIMIT$ caractères : dépasse la limite de publication Mastodon de $OVER$", "placeholders": {"used": {"content": "$1"}, "limit": {"content": "$2"}, "over": {"content": "$3"}}}
}
+3 -1
View File
@@ -53,5 +53,7 @@
"loginFailed": {"message": "Inloggen mislukt"},
"loggedInSuccessfully": {"message": "Succesvol ingelogd"},
"unableToLogIn": {"message": "Inloggen mislukt. Controleer de backend-URL en inloggegevens."},
"signedOut": {"message": "Uitgelogd"}
"signedOut": {"message": "Uitgelogd"},
"characterCount": {"message": "$USED$/$LIMIT$ tekens", "placeholders": {"used": {"content": "$1"}, "limit": {"content": "$2"}}},
"characterCountOverLimit": {"message": "$USED$/$LIMIT$ tekens - overschrijdt de Mastodon-postlimiet met $OVER$", "placeholders": {"used": {"content": "$1"}, "limit": {"content": "$2"}, "over": {"content": "$3"}}}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "__MSG_extensionName__",
"version": "0.2.0",
"version": "0.3.0",
"description": "__MSG_extensionDescription__",
"default_locale": "en-US",
"permissions": [
+12
View File
@@ -149,6 +149,18 @@ button {
color: #f38ba8;
}
.char-count {
margin: -6px 0 10px;
color: #a6adc8;
font-size: 0.75rem;
text-align: right;
}
.char-count.over-limit {
color: #f38ba8;
font-weight: 600;
}
.auth-session {
margin-bottom: 10px;
border: 1px solid #585b70;
+1
View File
@@ -37,6 +37,7 @@
<span data-i18n="commentLabel">Comment</span>
<textarea id="comment" name="comment" rows="3" data-i18n-placeholder="commentPlaceholder" placeholder="Your comment"></textarea>
</label>
<div id="character-count" class="char-count" aria-live="polite"></div>
<fieldset>
<legend data-i18n="tagsLabel">Tags</legend>
+82 -1
View File
@@ -14,10 +14,14 @@ 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 characterCountEl = document.getElementById('character-count');
const sessionStore = browser.storage.session;
const t = window.linklogI18n;
let maxPostCharacters = 500;
let mastodonPostPrefix = 'From my #LinkLog: ';
function normalizeBackendOrigin(backendUrl) {
try {
const url = new URL(backendUrl);
@@ -192,9 +196,11 @@ async function loadExistingTags() {
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = tag;
checkbox.addEventListener('change', updateCharacterCount);
label.append(checkbox, document.createTextNode(` ${tag}`));
return label;
}));
updateCharacterCount();
}
function getTags() {
@@ -203,6 +209,79 @@ function getTags() {
return [...new Set([...selected, ...newTags])].slice(0, 10);
}
// Load public app settings (e.g. the Mastodon post length limit) once a backend is configured.
async function loadConfig() {
const settings = await getSettings();
if (!settings.backendUrl || !(await hasBackendPermission(settings.backendUrl))) return;
try {
const response = await fetch(`${settings.backendUrl}/api/public/config`);
if (!response.ok) return;
const config = await response.json();
if (config.max_post_characters) maxPostCharacters = config.max_post_characters;
} catch (error) {
// Character-count warning is advisory; ignore config load failures.
}
updateCharacterCount();
}
// Load the user's configured Mastodon post prefix, so the estimated post length matches the server.
async function loadMastodonPrefix() {
const settings = await getSettings();
if (!settings.backendUrl || !settings.accessToken || !(await hasBackendPermission(settings.backendUrl))) return;
try {
const response = await fetch(`${settings.backendUrl}/api/user/plugins/mastodon`, {
headers: {'Authorization': `Bearer ${settings.accessToken}`},
});
if (!response.ok) return;
const config = await response.json();
if (config.post_prefix) {
mastodonPostPrefix = config.post_prefix;
} else if (config.hashtag) {
mastodonPostPrefix = `#${String(config.hashtag).trim().replace(/^#/, '')} `;
}
} catch (error) {
// Character-count warning is advisory; ignore config load failures.
}
updateCharacterCount();
}
// Format a UTC timestamp the same way the Mastodon plugin does, so the estimated post length matches the server.
function formatMastodonTimestamp(date) {
const months = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December',
];
const pad = (value) => String(value).padStart(2, '0');
return `${date.getUTCFullYear()} ${months[date.getUTCMonth()]} ${pad(date.getUTCDate())} - ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}`;
}
// Estimate the assembled Mastodon post length, mirroring backend/app/services/plugin_manager.py.
function estimatePostLength() {
const title = titleInput.value.trim();
const comment = commentInput.value.trim();
const url = removeKnownTrackingParams(urlInput.value.trim());
const parts = [mastodonPostPrefix.trim()];
if (title) parts.push(title);
if (comment) parts.push(comment);
if (title) parts.push(`Logged on ${formatMastodonTimestamp(new Date())} UTC from: ${url}`);
const tags = getTags();
if (tags.length) parts.push(tags.join(' '));
return parts.join('\n\n').length;
}
function updateCharacterCount() {
const length = estimatePostLength();
const overLimit = length > maxPostCharacters;
characterCountEl.textContent = overLimit
? t('characterCountOverLimit', [String(length), String(maxPostCharacters), String(length - maxPostCharacters)])
: t('characterCount', [String(length), String(maxPostCharacters)]);
characterCountEl.classList.toggle('over-limit', overLimit);
}
[titleInput, urlInput, commentInput, newTagsInput].forEach((input) => {
input.addEventListener('input', updateCharacterCount);
});
function removeKnownTrackingParams(urlString) {
try {
const url = new URL(urlString);
@@ -361,6 +440,8 @@ refetchTitleButton.addEventListener('click', (e) => {
titleInput.value = '';
fetchTitle(url, { force: true });
});
populateCurrentTab().then(checkExistingLink);
populateCurrentTab().then(checkExistingLink).then(updateCharacterCount);
loadExistingTags();
updateFeedLink();
loadConfig();
loadMastodonPrefix();