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
|
||||
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
|
||||
|
||||
1. Add-ons-Verwaltung → **Paperless-ngx** → Einstellungen
|
||||
1. Add-ons-Verwaltung → **Paperless-Connector** → Einstellungen
|
||||
2. Basisadresse eintragen, etwa `https://dms.example.org` — ohne
|
||||
abschließenden Schrägstrich
|
||||
3. API-Token eintragen, erzeugt im **Benutzerprofil** von Paperless
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
"message": "Freigabelink im Nachrichtentext"
|
||||
},
|
||||
"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": {
|
||||
"message": "Paperless — Verbindung"
|
||||
@@ -249,12 +249,19 @@
|
||||
"linkValidUnlimited": {
|
||||
"message": "Link ohne zeitliche Begrenzung"
|
||||
},
|
||||
"linkHeading": {
|
||||
"message": "Datei $1",
|
||||
"placeholders": {
|
||||
"1": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
"optionsHintTag": {
|
||||
"message": "Tag beim Teilen leer lassen, um keine Kennzeichnung zu setzen."
|
||||
},
|
||||
"fieldShareBaseUrl": {
|
||||
"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"
|
||||
},
|
||||
"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": {
|
||||
"message": "Paperless — Connection"
|
||||
@@ -249,12 +249,19 @@
|
||||
"linkValidUnlimited": {
|
||||
"message": "Link without time limit"
|
||||
},
|
||||
"linkHeading": {
|
||||
"message": "File $1",
|
||||
"placeholders": {
|
||||
"1": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
"optionsHintTag": {
|
||||
"message": "Leave the sharing tag empty to set no tag at all."
|
||||
},
|
||||
"fieldShareBaseUrl": {
|
||||
"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"
|
||||
},
|
||||
"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": {
|
||||
"message": "Paperless — Connexion"
|
||||
@@ -249,12 +249,19 @@
|
||||
"linkValidUnlimited": {
|
||||
"message": "Lien sans limite de durée"
|
||||
},
|
||||
"linkHeading": {
|
||||
"message": "Fichier $1",
|
||||
"placeholders": {
|
||||
"1": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
"optionsHintTag": {
|
||||
"message": "Laisser l'étiquette de partage vide pour ne poser aucune étiquette."
|
||||
},
|
||||
"fieldShareBaseUrl": {
|
||||
"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
|
||||
|
||||
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({
|
||||
id: "paperless-anhang",
|
||||
title: t("menuStoreAttachment"),
|
||||
@@ -113,7 +128,9 @@ browser.runtime.onInstalled.addListener(() => {
|
||||
title: t("menuStoreMessage"),
|
||||
contexts: ["message_list"]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
menueAufbauen();
|
||||
|
||||
browser.menus.onClicked.addListener(async (info, tab) => {
|
||||
try {
|
||||
@@ -249,6 +266,29 @@ async function dialogOeffnen(art, daten) {
|
||||
|
||||
// -------------------------------------------- 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) => {
|
||||
// Nur Nachrichten aus dieser Erweiterung annehmen. Thunderbird lässt
|
||||
// fremde Absender hier zwar ohnehin nicht durch, solange kein
|
||||
@@ -390,14 +430,22 @@ async function ablegen(nachricht) {
|
||||
const antwort = await c.updateVersion(
|
||||
nachricht.zielId, datei.blob, datei.name,
|
||||
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",
|
||||
zielId: nachricht.zielId });
|
||||
} else {
|
||||
const felder = { title: nachricht.titel || datei.name };
|
||||
if (nachricht.erstellt) felder.created = nachricht.erstellt;
|
||||
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" });
|
||||
}
|
||||
}
|
||||
|
||||
2
build.sh
2
build.sh
@@ -7,7 +7,7 @@
|
||||
set -eu
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
NAME="paperless-thunderbird"
|
||||
NAME="paperless-connector"
|
||||
VERSION="$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' manifest.json | head -1)"
|
||||
OUT="${NAME}-${VERSION}.xpi"
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 3,
|
||||
"name": "__MSG_extName__",
|
||||
"description": "__MSG_extDescription__",
|
||||
"version": "1.6.1",
|
||||
"version": "1.8.1",
|
||||
"author": "Name des Herausgebers",
|
||||
"homepage_url": "https://example.org/paperless-thunderbird",
|
||||
"default_locale": "de",
|
||||
|
||||
@@ -24,8 +24,17 @@
|
||||
<datalist id="typListe"></datalist>
|
||||
|
||||
<div class="wachsen">
|
||||
<label data-i18n="fieldTags">Tags</label>
|
||||
<div class="liste"><div id="tags" style="padding:4px 8px"></div></div>
|
||||
<label for="tagSuche" data-i18n="fieldTags">Tags</label>
|
||||
<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 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 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);
|
||||
|
||||
async function start() {
|
||||
@@ -66,30 +71,128 @@ function nameVon(liste, id) {
|
||||
}
|
||||
|
||||
function zeigeTags(gesetzt) {
|
||||
const ziel = $("tags");
|
||||
ziel.replaceChildren();
|
||||
for (const tag of alleTags) {
|
||||
for (const id of gesetzt) gewaehlt.add(Number(id));
|
||||
const feld = $("tagSuche");
|
||||
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");
|
||||
zeile.className = "wahl";
|
||||
|
||||
const box = document.createElement("input");
|
||||
box.type = "checkbox";
|
||||
box.id = `t${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");
|
||||
bez.htmlFor = box.id;
|
||||
bez.textContent = tag.name;
|
||||
beschriftungSetzen(bez, String(tag.name), filter);
|
||||
|
||||
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() {
|
||||
$("speichern").disabled = true;
|
||||
meldungAnzeigen(browser.i18n.getMessage("msgSaving") || "Wird gespeichert …");
|
||||
try {
|
||||
const tags = [...$("tags").querySelectorAll("input:checked")]
|
||||
.map((b) => Number(b.value));
|
||||
// Aus der Menge lesen, nicht aus den Kästchen: Gefilterte Zeilen
|
||||
// sind gar nicht im Dokument, ihre Auswahl ginge sonst verloren.
|
||||
const tags = [...gewaehlt];
|
||||
const payload = { title: $("titel").value.trim(), tags };
|
||||
const erstellt = $("erstellt").value.trim();
|
||||
if (erstellt) payload.created = erstellt;
|
||||
|
||||
@@ -70,5 +70,17 @@ td.zahl, th.zahl { text-align: right; }
|
||||
tr.gewaehlt { background: SelectedItem; color: SelectedItemText; }
|
||||
tbody tr { cursor: default; }
|
||||
.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; }
|
||||
.wachsen { flex: 1; display: flex; flex-direction: column; gap: 6px; min-height: 0; }
|
||||
|
||||
@@ -2,6 +2,29 @@
|
||||
* 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.
|
||||
*
|
||||
@@ -43,6 +66,10 @@ export async function ruf(befehl, daten = {}) {
|
||||
* im HTML hinterlegte deutsche Text stehen.
|
||||
*/
|
||||
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]")) {
|
||||
const s = browser.i18n.getMessage(el.dataset.i18n);
|
||||
if (s) el.textContent = s;
|
||||
|
||||
Reference in New Issue
Block a user