Anpassung Tagsuche, Fix Fehler bei längeren Ladezeiten
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
# Paperless-ngx für Thunderbird / Betterbird
|
# Paperless-Connector für Thunderbird / Betterbird
|
||||||
|
|
||||||
Legt Anhänge und ganze E-Mails in einem Paperless-ngx-Archiv ab und fügt
|
Legt Anhänge und ganze E-Mails in einem Paperless-ngx-Archiv ab und fügt
|
||||||
Dokumente aus dem Archiv als Anhang oder Freigabelink in Entwürfe ein.
|
Dokumente aus dem Archiv als Anhang oder Freigabelink in Entwürfe ein.
|
||||||
@@ -85,7 +85,7 @@ für Empfänger, die ohnehin Zugriff auf das Archiv haben sollen.
|
|||||||
|
|
||||||
## Einrichten
|
## Einrichten
|
||||||
|
|
||||||
1. Add-ons-Verwaltung → **Paperless-ngx** → Einstellungen
|
1. Add-ons-Verwaltung → **Paperless-Connector** → Einstellungen
|
||||||
2. Basisadresse eintragen, etwa `https://dms.example.org` — ohne
|
2. Basisadresse eintragen, etwa `https://dms.example.org` — ohne
|
||||||
abschließenden Schrägstrich
|
abschließenden Schrägstrich
|
||||||
3. API-Token eintragen, erzeugt im **Benutzerprofil** von Paperless
|
3. API-Token eintragen, erzeugt im **Benutzerprofil** von Paperless
|
||||||
|
|||||||
@@ -79,7 +79,7 @@
|
|||||||
"message": "Freigabelink im Nachrichtentext"
|
"message": "Freigabelink im Nachrichtentext"
|
||||||
},
|
},
|
||||||
"insertHintLink": {
|
"insertHintLink": {
|
||||||
"message": "Der Link ist ohne Anmeldung abrufbar. 0 Tage bedeutet unbegrenzt."
|
"message": "Der Link ist ohne Anmeldung abrufbar. 0 Tage bedeutet unbegrenzt. Leeres Tag-Feld bedeutet: keine Kennzeichnung setzen."
|
||||||
},
|
},
|
||||||
"optionsTitle": {
|
"optionsTitle": {
|
||||||
"message": "Paperless — Verbindung"
|
"message": "Paperless — Verbindung"
|
||||||
@@ -249,12 +249,19 @@
|
|||||||
"linkValidUnlimited": {
|
"linkValidUnlimited": {
|
||||||
"message": "Link ohne zeitliche Begrenzung"
|
"message": "Link ohne zeitliche Begrenzung"
|
||||||
},
|
},
|
||||||
"linkHeading": {
|
"optionsHintTag": {
|
||||||
"message": "Datei $1",
|
"message": "Tag beim Teilen leer lassen, um keine Kennzeichnung zu setzen."
|
||||||
"placeholders": {
|
},
|
||||||
"1": {
|
"fieldShareBaseUrl": {
|
||||||
"content": "$1"
|
"message": "Adresse für Freigabelinks (optional)"
|
||||||
}
|
},
|
||||||
}
|
"optionsHintShareUrl": {
|
||||||
|
"message": "Nur nötig, wenn der interne Zugang über einen abweichenden Port oder Namen läuft. Freigabelinks werden dann unter dieser Adresse gebaut, damit sie auch von außen funktionieren. Leer = Basisadresse."
|
||||||
|
},
|
||||||
|
"tagsFilterPlaceholder": {
|
||||||
|
"message": "Tippen zum Filtern …"
|
||||||
|
},
|
||||||
|
"tagsNoMatch": {
|
||||||
|
"message": "Keine Übereinstimmung."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@
|
|||||||
"message": "Share link in the message body"
|
"message": "Share link in the message body"
|
||||||
},
|
},
|
||||||
"insertHintLink": {
|
"insertHintLink": {
|
||||||
"message": "The link works without signing in. 0 days means unlimited."
|
"message": "The link works without signing in. 0 days means unlimited. An empty tag field means: do not set a tag."
|
||||||
},
|
},
|
||||||
"optionsTitle": {
|
"optionsTitle": {
|
||||||
"message": "Paperless — Connection"
|
"message": "Paperless — Connection"
|
||||||
@@ -249,12 +249,19 @@
|
|||||||
"linkValidUnlimited": {
|
"linkValidUnlimited": {
|
||||||
"message": "Link without time limit"
|
"message": "Link without time limit"
|
||||||
},
|
},
|
||||||
"linkHeading": {
|
"optionsHintTag": {
|
||||||
"message": "File $1",
|
"message": "Leave the sharing tag empty to set no tag at all."
|
||||||
"placeholders": {
|
},
|
||||||
"1": {
|
"fieldShareBaseUrl": {
|
||||||
"content": "$1"
|
"message": "Address for share links (optional)"
|
||||||
}
|
},
|
||||||
}
|
"optionsHintShareUrl": {
|
||||||
|
"message": "Only needed if internal access uses a different port or name. Share links are then built with this address so they work from outside as well. Empty = base address."
|
||||||
|
},
|
||||||
|
"tagsFilterPlaceholder": {
|
||||||
|
"message": "Type to filter …"
|
||||||
|
},
|
||||||
|
"tagsNoMatch": {
|
||||||
|
"message": "No match."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@
|
|||||||
"message": "Lien de partage dans le corps du message"
|
"message": "Lien de partage dans le corps du message"
|
||||||
},
|
},
|
||||||
"insertHintLink": {
|
"insertHintLink": {
|
||||||
"message": "Le lien est accessible sans connexion. 0 jour signifie sans limite."
|
"message": "Le lien est accessible sans connexion. 0 jour signifie sans limite. Un champ d'étiquette vide signifie : ne pas marquer."
|
||||||
},
|
},
|
||||||
"optionsTitle": {
|
"optionsTitle": {
|
||||||
"message": "Paperless — Connexion"
|
"message": "Paperless — Connexion"
|
||||||
@@ -249,12 +249,19 @@
|
|||||||
"linkValidUnlimited": {
|
"linkValidUnlimited": {
|
||||||
"message": "Lien sans limite de durée"
|
"message": "Lien sans limite de durée"
|
||||||
},
|
},
|
||||||
"linkHeading": {
|
"optionsHintTag": {
|
||||||
"message": "Fichier $1",
|
"message": "Laisser l'étiquette de partage vide pour ne poser aucune étiquette."
|
||||||
"placeholders": {
|
},
|
||||||
"1": {
|
"fieldShareBaseUrl": {
|
||||||
"content": "$1"
|
"message": "Adresse pour les liens de partage (facultatif)"
|
||||||
}
|
},
|
||||||
}
|
"optionsHintShareUrl": {
|
||||||
|
"message": "Nécessaire uniquement si l'accès interne utilise un port ou un nom différent. Les liens de partage seront alors construits avec cette adresse afin de fonctionner aussi depuis l'extérieur. Vide = adresse de base."
|
||||||
|
},
|
||||||
|
"tagsFilterPlaceholder": {
|
||||||
|
"message": "Saisir pour filtrer …"
|
||||||
|
},
|
||||||
|
"tagsNoMatch": {
|
||||||
|
"message": "Aucune correspondance."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,22 @@ let naechsteAuftragsnummer = 1;
|
|||||||
|
|
||||||
// ---------------------------------------------------------------- Menüs
|
// ---------------------------------------------------------------- Menüs
|
||||||
|
|
||||||
browser.runtime.onInstalled.addListener(() => {
|
/**
|
||||||
|
* Menüeinträge anlegen.
|
||||||
|
*
|
||||||
|
* NICHT in onInstalled: Eine Ereignisseite wird beendet und bei Bedarf
|
||||||
|
* neu geladen, das Skript läuft dabei erneut durch. Die Menüs überleben
|
||||||
|
* das aber - ein zweiter menus.create mit derselben Kennung scheitert
|
||||||
|
* dann mit "already exists".
|
||||||
|
*
|
||||||
|
* Deshalb erst removeAll, dann neu anlegen. Das ist bei jedem Aufwachen
|
||||||
|
* richtig und kostet nichts.
|
||||||
|
*/
|
||||||
|
async function menueAufbauen() {
|
||||||
|
try {
|
||||||
|
await browser.menus.removeAll();
|
||||||
|
} catch { /* beim ersten Lauf gibt es nichts zu entfernen */ }
|
||||||
|
|
||||||
browser.menus.create({
|
browser.menus.create({
|
||||||
id: "paperless-anhang",
|
id: "paperless-anhang",
|
||||||
title: t("menuStoreAttachment"),
|
title: t("menuStoreAttachment"),
|
||||||
@@ -113,7 +128,9 @@ browser.runtime.onInstalled.addListener(() => {
|
|||||||
title: t("menuStoreMessage"),
|
title: t("menuStoreMessage"),
|
||||||
contexts: ["message_list"]
|
contexts: ["message_list"]
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
|
menueAufbauen();
|
||||||
|
|
||||||
browser.menus.onClicked.addListener(async (info, tab) => {
|
browser.menus.onClicked.addListener(async (info, tab) => {
|
||||||
try {
|
try {
|
||||||
@@ -249,6 +266,29 @@ async function dialogOeffnen(art, daten) {
|
|||||||
|
|
||||||
// -------------------------------------------- Vermittlung für die Dialoge
|
// -------------------------------------------- Vermittlung für die Dialoge
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offene Verbindungen der Dialoge.
|
||||||
|
*
|
||||||
|
* SEHR WICHTIG: Eine Ereignisseite wird beendet, sobald keine Ereignisse
|
||||||
|
* mehr anstehen. Eine laufende Wartezeit in waitForTask zählt NICHT als
|
||||||
|
* Ereignis - mitten im Ablegen verschwindet sonst der Empfänger, und der
|
||||||
|
* Dialog bekommt "Could not establish connection. Receiving end does not
|
||||||
|
* exist."
|
||||||
|
*
|
||||||
|
* Eine offene Verbindung hält die Seite dagegen am Leben. Die Dialoge
|
||||||
|
* öffnen deshalb beim Laden eine und halten sie bis zum Schliessen.
|
||||||
|
*/
|
||||||
|
const verbindungen = new Set();
|
||||||
|
|
||||||
|
browser.runtime.onConnect.addListener((port) => {
|
||||||
|
verbindungen.add(port);
|
||||||
|
console.debug("[paperless] Dialog verbunden, offen:", verbindungen.size);
|
||||||
|
port.onDisconnect.addListener(() => {
|
||||||
|
verbindungen.delete(port);
|
||||||
|
console.debug("[paperless] Dialog getrennt, offen:", verbindungen.size);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
browser.runtime.onMessage.addListener(async (nachricht, absender) => {
|
browser.runtime.onMessage.addListener(async (nachricht, absender) => {
|
||||||
// Nur Nachrichten aus dieser Erweiterung annehmen. Thunderbird lässt
|
// Nur Nachrichten aus dieser Erweiterung annehmen. Thunderbird lässt
|
||||||
// fremde Absender hier zwar ohnehin nicht durch, solange kein
|
// fremde Absender hier zwar ohnehin nicht durch, solange kein
|
||||||
@@ -390,14 +430,22 @@ async function ablegen(nachricht) {
|
|||||||
const antwort = await c.updateVersion(
|
const antwort = await c.updateVersion(
|
||||||
nachricht.zielId, datei.blob, datei.name,
|
nachricht.zielId, datei.blob, datei.name,
|
||||||
versionslabel(s.versionLabel));
|
versionslabel(s.versionLabel));
|
||||||
const id = await c.waitForTask(antwort);
|
const id = await c.waitForTask(antwort, (sek) => {
|
||||||
|
if (sek % 15 === 0) {
|
||||||
|
console.debug("[paperless] warte auf Aufgabe:", sek, "s");
|
||||||
|
}
|
||||||
|
});
|
||||||
ergebnisse.push({ id, name: datei.name, art: "version",
|
ergebnisse.push({ id, name: datei.name, art: "version",
|
||||||
zielId: nachricht.zielId });
|
zielId: nachricht.zielId });
|
||||||
} else {
|
} else {
|
||||||
const felder = { title: nachricht.titel || datei.name };
|
const felder = { title: nachricht.titel || datei.name };
|
||||||
if (nachricht.erstellt) felder.created = nachricht.erstellt;
|
if (nachricht.erstellt) felder.created = nachricht.erstellt;
|
||||||
const antwort = await c.postDocument(datei.blob, datei.name, felder);
|
const antwort = await c.postDocument(datei.blob, datei.name, felder);
|
||||||
const id = await c.waitForTask(antwort);
|
const id = await c.waitForTask(antwort, (sek) => {
|
||||||
|
if (sek % 15 === 0) {
|
||||||
|
console.debug("[paperless] warte auf Aufgabe:", sek, "s");
|
||||||
|
}
|
||||||
|
});
|
||||||
ergebnisse.push({ id, name: datei.name, art: "neu" });
|
ergebnisse.push({ id, name: datei.name, art: "neu" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2
build.sh
2
build.sh
@@ -7,7 +7,7 @@
|
|||||||
set -eu
|
set -eu
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
NAME="paperless-thunderbird"
|
NAME="paperless-connector"
|
||||||
VERSION="$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' manifest.json | head -1)"
|
VERSION="$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' manifest.json | head -1)"
|
||||||
OUT="${NAME}-${VERSION}.xpi"
|
OUT="${NAME}-${VERSION}.xpi"
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "__MSG_extName__",
|
"name": "__MSG_extName__",
|
||||||
"description": "__MSG_extDescription__",
|
"description": "__MSG_extDescription__",
|
||||||
"version": "1.6.1",
|
"version": "1.8.1",
|
||||||
"author": "Name des Herausgebers",
|
"author": "Name des Herausgebers",
|
||||||
"homepage_url": "https://example.org/paperless-thunderbird",
|
"homepage_url": "https://example.org/paperless-thunderbird",
|
||||||
"default_locale": "de",
|
"default_locale": "de",
|
||||||
|
|||||||
@@ -24,8 +24,17 @@
|
|||||||
<datalist id="typListe"></datalist>
|
<datalist id="typListe"></datalist>
|
||||||
|
|
||||||
<div class="wachsen">
|
<div class="wachsen">
|
||||||
<label data-i18n="fieldTags">Tags</label>
|
<label for="tagSuche" data-i18n="fieldTags">Tags</label>
|
||||||
<div class="liste"><div id="tags" style="padding:4px 8px"></div></div>
|
<input type="text" id="tagSuche"
|
||||||
|
data-i18n-placeholder="tagsFilterPlaceholder"
|
||||||
|
placeholder="Tippen zum Filtern …">
|
||||||
|
<div class="liste">
|
||||||
|
<div id="tagsGewaehlt" class="gewaehlteTags"></div>
|
||||||
|
<div id="tags" style="padding:4px 8px"></div>
|
||||||
|
<div id="tagsLeer" class="hinweis"
|
||||||
|
style="padding:6px 8px; display:none"
|
||||||
|
data-i18n="tagsNoMatch">Keine Übereinstimmung.</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hinweis" data-i18n="classifyHintNew">
|
<div class="hinweis" data-i18n="classifyHintNew">
|
||||||
|
|||||||
119
ui/classify.js
119
ui/classify.js
@@ -14,6 +14,11 @@ const $ = (id) => document.getElementById(id);
|
|||||||
let dokumentId = null;
|
let dokumentId = null;
|
||||||
let alleTags = [];
|
let alleTags = [];
|
||||||
|
|
||||||
|
// Gewählte Tags als Menge. Bewusst getrennt vom angezeigten Zustand der
|
||||||
|
// Kontrollkästchen: Der Filter blendet Zeilen aus, die Auswahl darf davon
|
||||||
|
// nicht berührt werden.
|
||||||
|
const gewaehlt = new Set();
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", start);
|
document.addEventListener("DOMContentLoaded", start);
|
||||||
|
|
||||||
async function start() {
|
async function start() {
|
||||||
@@ -66,30 +71,128 @@ function nameVon(liste, id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function zeigeTags(gesetzt) {
|
function zeigeTags(gesetzt) {
|
||||||
const ziel = $("tags");
|
for (const id of gesetzt) gewaehlt.add(Number(id));
|
||||||
ziel.replaceChildren();
|
const feld = $("tagSuche");
|
||||||
for (const tag of alleTags) {
|
if (feld) {
|
||||||
|
feld.addEventListener("input", zeichneTags);
|
||||||
|
// Eingabetaste soll den Dialog nicht abschicken, sondern beim
|
||||||
|
// einzigen Treffer diesen umschalten - das ist beim Tippen der
|
||||||
|
// erwartete Ablauf.
|
||||||
|
feld.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key !== "Enter") return;
|
||||||
|
e.preventDefault();
|
||||||
|
const treffer = gefilterteTags();
|
||||||
|
if (treffer.length === 1) {
|
||||||
|
umschalten(treffer[0].id);
|
||||||
|
feld.value = "";
|
||||||
|
zeichneTags();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
zeichneTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tags, die zum aktuellen Filter passen und noch nicht gewählt sind. */
|
||||||
|
function gefilterteTags() {
|
||||||
|
const filter = ($("tagSuche")?.value || "").trim().toLowerCase();
|
||||||
|
return alleTags.filter((t) => {
|
||||||
|
if (gewaehlt.has(Number(t.id))) return false;
|
||||||
|
if (!filter) return true;
|
||||||
|
return String(t.name).toLowerCase().includes(filter);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function umschalten(id) {
|
||||||
|
const n = Number(id);
|
||||||
|
if (gewaehlt.has(n)) gewaehlt.delete(n);
|
||||||
|
else gewaehlt.add(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tagliste zeichnen.
|
||||||
|
*
|
||||||
|
* Zwei Bereiche: oben die gewählten, darunter die übrigen. Die gewählten
|
||||||
|
* bleiben immer sichtbar, auch wenn der Filter sie ausschliesst - sonst
|
||||||
|
* verliert man beim Suchen den Überblick, was bereits angehakt ist.
|
||||||
|
*/
|
||||||
|
function zeichneTags() {
|
||||||
|
const filter = ($("tagSuche")?.value || "").trim().toLowerCase();
|
||||||
|
|
||||||
|
const oben = $("tagsGewaehlt");
|
||||||
|
oben.replaceChildren();
|
||||||
|
const gewaehlteListe = alleTags
|
||||||
|
.filter((t) => gewaehlt.has(Number(t.id)))
|
||||||
|
.sort((a, b) => String(a.name).localeCompare(String(b.name), "de"));
|
||||||
|
for (const tag of gewaehlteListe) {
|
||||||
|
oben.append(tagZeile(tag, true, ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
const unten = $("tags");
|
||||||
|
unten.replaceChildren();
|
||||||
|
const rest = gefilterteTags()
|
||||||
|
.sort((a, b) => String(a.name).localeCompare(String(b.name), "de"));
|
||||||
|
for (const tag of rest) {
|
||||||
|
unten.append(tagZeile(tag, false, filter));
|
||||||
|
}
|
||||||
|
|
||||||
|
const leer = $("tagsLeer");
|
||||||
|
if (leer) leer.style.display = (rest.length === 0 && filter) ? "" : "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
function tagZeile(tag, istGewaehlt, filter) {
|
||||||
const zeile = document.createElement("div");
|
const zeile = document.createElement("div");
|
||||||
zeile.className = "wahl";
|
zeile.className = "wahl";
|
||||||
|
|
||||||
const box = document.createElement("input");
|
const box = document.createElement("input");
|
||||||
box.type = "checkbox";
|
box.type = "checkbox";
|
||||||
box.id = `t${tag.id}`;
|
box.id = `t${tag.id}`;
|
||||||
box.value = String(tag.id);
|
box.value = String(tag.id);
|
||||||
box.checked = gesetzt.includes(tag.id);
|
box.checked = istGewaehlt;
|
||||||
|
box.addEventListener("change", () => {
|
||||||
|
umschalten(tag.id);
|
||||||
|
zeichneTags();
|
||||||
|
});
|
||||||
|
|
||||||
const bez = document.createElement("label");
|
const bez = document.createElement("label");
|
||||||
bez.htmlFor = box.id;
|
bez.htmlFor = box.id;
|
||||||
bez.textContent = tag.name;
|
beschriftungSetzen(bez, String(tag.name), filter);
|
||||||
|
|
||||||
zeile.append(box, bez);
|
zeile.append(box, bez);
|
||||||
ziel.append(zeile);
|
return zeile;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Beschriftung mit hervorgehobenem Treffer.
|
||||||
|
*
|
||||||
|
* Aufbau über createElement und textContent, nicht über innerHTML - ein
|
||||||
|
* Tagname stammt aus Paperless und damit mittelbar aus fremden Quellen.
|
||||||
|
*/
|
||||||
|
function beschriftungSetzen(el, name, filter) {
|
||||||
|
el.replaceChildren();
|
||||||
|
if (!filter) {
|
||||||
|
el.textContent = name;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
const pos = name.toLowerCase().indexOf(filter);
|
||||||
|
if (pos < 0) {
|
||||||
|
el.textContent = name;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.append(document.createTextNode(name.slice(0, pos)));
|
||||||
|
const treffer = document.createElement("span");
|
||||||
|
treffer.className = "trefferHervor";
|
||||||
|
treffer.textContent = name.slice(pos, pos + filter.length);
|
||||||
|
el.append(treffer);
|
||||||
|
el.append(document.createTextNode(name.slice(pos + filter.length)));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function speichern() {
|
async function speichern() {
|
||||||
$("speichern").disabled = true;
|
$("speichern").disabled = true;
|
||||||
meldungAnzeigen(browser.i18n.getMessage("msgSaving") || "Wird gespeichert …");
|
meldungAnzeigen(browser.i18n.getMessage("msgSaving") || "Wird gespeichert …");
|
||||||
try {
|
try {
|
||||||
const tags = [...$("tags").querySelectorAll("input:checked")]
|
// Aus der Menge lesen, nicht aus den Kästchen: Gefilterte Zeilen
|
||||||
.map((b) => Number(b.value));
|
// sind gar nicht im Dokument, ihre Auswahl ginge sonst verloren.
|
||||||
|
const tags = [...gewaehlt];
|
||||||
const payload = { title: $("titel").value.trim(), tags };
|
const payload = { title: $("titel").value.trim(), tags };
|
||||||
const erstellt = $("erstellt").value.trim();
|
const erstellt = $("erstellt").value.trim();
|
||||||
if (erstellt) payload.created = erstellt;
|
if (erstellt) payload.created = erstellt;
|
||||||
|
|||||||
@@ -70,5 +70,17 @@ td.zahl, th.zahl { text-align: right; }
|
|||||||
tr.gewaehlt { background: SelectedItem; color: SelectedItemText; }
|
tr.gewaehlt { background: SelectedItem; color: SelectedItemText; }
|
||||||
tbody tr { cursor: default; }
|
tbody tr { cursor: default; }
|
||||||
.hinweis { color: GrayText; font-size: 12px; }
|
.hinweis { color: GrayText; font-size: 12px; }
|
||||||
|
|
||||||
|
/* Gewählte Tags stehen oben und bleiben sichtbar, auch wenn der Filter
|
||||||
|
sie ausschliesst. Sonst verliert man beim Suchen den Überblick, was
|
||||||
|
bereits angehakt ist. */
|
||||||
|
.gewaehlteTags {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-bottom: 1px solid var(--rand);
|
||||||
|
background: color-mix(in srgb, Field 94%, CanvasText);
|
||||||
|
}
|
||||||
|
.gewaehlteTags:empty { display: none; }
|
||||||
|
.gewaehlteTags .wahl label { font-weight: 600; }
|
||||||
|
.trefferHervor { background: color-mix(in srgb, Mark 60%, transparent); }
|
||||||
.fehler { color: #b32; }
|
.fehler { color: #b32; }
|
||||||
.wachsen { flex: 1; display: flex; flex-direction: column; gap: 6px; min-height: 0; }
|
.wachsen { flex: 1; display: flex; flex-direction: column; gap: 6px; min-height: 0; }
|
||||||
|
|||||||
@@ -2,6 +2,29 @@
|
|||||||
* gemeinsam.js - Hilfsmittel für alle Dialogseiten.
|
* gemeinsam.js - Hilfsmittel für alle Dialogseiten.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verbindung zur Ereignisseite, die offen bleibt, solange der Dialog
|
||||||
|
* geöffnet ist.
|
||||||
|
*
|
||||||
|
* Sie überträgt nichts - ihr einziger Zweck ist, die Ereignisseite am
|
||||||
|
* Leben zu halten. Ohne sie wird die Seite während längerer Vorgänge
|
||||||
|
* beendet, etwa beim Warten auf eine Aufgabe in Paperless, und der
|
||||||
|
* laufende Aufruf schlägt mit "Receiving end does not exist" fehl.
|
||||||
|
*/
|
||||||
|
let dauerverbindung = null;
|
||||||
|
|
||||||
|
export function verbindungHalten() {
|
||||||
|
if (dauerverbindung) return;
|
||||||
|
try {
|
||||||
|
dauerverbindung = browser.runtime.connect({ name: "dialog" });
|
||||||
|
dauerverbindung.onDisconnect.addListener(() => {
|
||||||
|
dauerverbindung = null;
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("[paperless] Dauerverbindung nicht möglich:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ruft die Ereignisseite und wirft bei Fehlern eine Ausnahme.
|
* Ruft die Ereignisseite und wirft bei Fehlern eine Ausnahme.
|
||||||
*
|
*
|
||||||
@@ -43,6 +66,10 @@ export async function ruf(befehl, daten = {}) {
|
|||||||
* im HTML hinterlegte deutsche Text stehen.
|
* im HTML hinterlegte deutsche Text stehen.
|
||||||
*/
|
*/
|
||||||
export function uebersetzeSeite() {
|
export function uebersetzeSeite() {
|
||||||
|
// Beim Aufbau der Seite gleich die Verbindung öffnen - jede Seite ruft
|
||||||
|
// diese Funktion, also ist es die verlässlichste Stelle.
|
||||||
|
verbindungHalten();
|
||||||
|
|
||||||
for (const el of document.querySelectorAll("[data-i18n]")) {
|
for (const el of document.querySelectorAll("[data-i18n]")) {
|
||||||
const s = browser.i18n.getMessage(el.dataset.i18n);
|
const s = browser.i18n.getMessage(el.dataset.i18n);
|
||||||
if (s) el.textContent = s;
|
if (s) el.textContent = s;
|
||||||
|
|||||||
Reference in New Issue
Block a user