diff --git a/README.md b/README.md index ba4c90d..1211a6f 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ WebExtensions, LibreOffice UNO. |---|---|---| | Kontextmenü am Anhang | Anhang in Paperless ablegen | ausgewählte Anhänge als Dokument | | Kontextmenü in der Nachrichtenliste | E-Mail in Paperless ablegen | als PDF mit eingebettetem Original, wahlweise als `.eml` | +| Beides | Ablauf | Dateiauswahl, dann je Datei Ziel und Klassifizierung | | Schaltfläche im Nachrichtenfenster | dasselbe für die angezeigte Nachricht | | | Schaltfläche im Verfassenfenster | Aus Paperless einfügen | als Anhang oder als Freigabelink | @@ -46,13 +47,45 @@ alle Anhänge im Original. Es geht also nichts verloren, auch wenn die Darstellung nicht originalgetreu ist. Wer lieber die Rohfassung will, stellt das in den Einstellungen um oder -nimmt das Häkchen im Ablegen-Dialog heraus. +wählt im ersten Fenster je Datei „Original ablegen". **Titel aus dem Betreff.** Präfixe wie `AW:` oder `Fwd:` werden entfernt — sie tragen im Archiv nichts bei und stören beim Sortieren. -**Klassifizierung direkt danach.** Sonst trägt ein neues Dokument nur den -Titel, und nachträglich macht man es erfahrungsgemäß nicht mehr. +**Zwei Schritte, dann je Datei einer.** + +``` +Fenster 1 Welche Dateien sollen abgelegt werden? + [x] Rechnung.pdf (•) Original ( ) PDF aufbereiten + [x] Anschreiben.html (•) Original ( ) PDF aufbereiten + +Fenster 2 Je Datei: Ziel und Klassifizierung + ┌─ Ziel ──────────┐ ┌─ Klassifizierung ─┐ + │ ( ) Neu │ │ Titel [.......] │ + │ (•) An bestehen-│ │ Erstellt[....] │ + │ des anhängen│ │ Korresp.[.......] │ + │ [suche] │ │ Typ [.......] │ + │ Trefferliste │ │ Tags [suche] │ + └─────────────────┘ └───────────────────┘ +``` + +Die Trennung hat einen praktischen Grund: **Ziel und Klassifizierung sind +je Datei verschieden.** Ein einzelner Dialog für alle würde die Angaben +der ersten Datei auf alle übertragen — bei einer Rechnung samt +Lastschriftmandat wäre das falsch. + +**Klassifizierung des Ziels wird übernommen.** Wählt man ein bestehendes +Dokument aus der Trefferliste, stehen Titel, Datum, Korrespondent, Typ und +Tags schon im rechten Kasten und lassen sich anpassen. Eine neue Version +gehört inhaltlich zum selben Vorgang; die Angaben neu einzutippen wäre +Arbeit für nichts, und ein abweichender Korrespondent brächte die Version +vom Original weg. + +**Ein Aufruf je Datei.** Bewusst nicht alle in einem Durchgang: Manifest V3 +beendet die Ereignisseite, sobald sie untätig scheint, und ein Aufruf über +zehn Anhänge lief regelmäßig in `Receiving end does not exist`. Nebeneffekt: +Der Fortschritt ist sichtbar, und scheitert eine Datei, sind die übrigen +trotzdem abgelegt. ### Warum kein originalgetreues Abbild @@ -173,7 +206,7 @@ Dauerhaft: ``` Add-ons-Verwaltung → Zahnrad → Add-on aus Datei installieren -→ paperless-thunderbird-1.0.0.xpi +→ paperless-connector-.xpi ``` --- @@ -228,7 +261,7 @@ bei der LibreOffice-Fassung, weil hier **keine fremden Dokumente geöffnet** werden — der heikelste Punkt entfällt. Die wichtigsten Maßnahmen: - Zugriffsrecht nur für die eingetragene Adresse, zur Laufzeit erbeten -- Nur die fünf tatsächlich benötigten Berechtigungen — `compose.send` war +- Nur die sechs tatsächlich benötigten Berechtigungen — `compose.send` war im ersten Entwurf enthalten, ohne je benutzt zu werden, und ist entfernt - Nur `http` und `https` als Basisadresse - Dateinamen aus Anhängen werden bereinigt — sie stammen vom Absender @@ -248,9 +281,11 @@ background.js Menüs, Schaltflächen, Vermittlung lib/ client.js REST-Client (fetch, FormData) settings.js Einstellungen, Dateinamen, Hilfsmittel + pdf.js PDF-Erzeuger ohne Fremdbibliothek + mailpdf.js Aufbereitung einer Nachricht ui/ - store.html/.js Ablegen: Anhänge oder ganze Nachricht - classify.html/.js Klassifizierung direkt danach + store.html/.js Schritt 1: welche Dateien + schritt.html/.js Schritt 2: Ziel und Klassifizierung je Datei insert.html/.js Einfügen als Anhang oder Link options.html/.js Verbindung einrichten gemeinsam.js/.css geteilte Hilfsmittel @@ -259,11 +294,28 @@ icons/ ``` Die Dialoge sprechen **nicht** selbst mit Paperless, sondern lassen die -Ereignisseite arbeiten. Dort hängt die zur Laufzeit erteilte Berechtigung; -ein Dialogfenster müsste sie sonst erneut erbitten. +Ereignisseite arbeiten. So liegen Token und Verbindungslogik an einer +Stelle, und die Dialoge bleiben reine Oberfläche. + +Damit die Ereignisseite währenddessen nicht beendet wird, hält jeder +Dialog eine offene Verbindung dorthin und sendet alle 15 Sekunden ein +Lebenszeichen. Manifest V3 räumt sie sonst mitten im Vorgang ab — +Wartezeiten zählen dort nicht als Ereignis. --- +### Eine Stolperstelle bei der Gestaltung + +Die Dialoge sind auf feste Fensterhöhe mit blätternden Listen ausgelegt. +Drei Dinge mussten dafür stimmen, und jedes einzelne führte für sich zum +Überlauf: + +| | | +|---|---| +| `height: 100vh` am Body | mit `min-height` wächst die Seite über das Fenster hinaus | +| kein `fieldset` als Flex-Behälter | dessen innerer anonymer Kasten löst `height: 100%` nicht auf — deshalb `
` | +| kein `flex-wrap` bei den Spalten | mit Umbruch bemisst sich die Zeilenhöhe nach dem Inhalt, nicht nach dem Behälter | + ## Vor einer Veröffentlichung | Datei | Was | @@ -274,6 +326,28 @@ ein Dialogfenster müsste sie sonst erneut erbitten. Die Kennung sollte einer Domain folgen, die dir gehört. Aktuell steht dort `paperless@example.org`. +### Anforderungen der Prüfung auf addons.thunderbird.net + +Aus der Ablehnung von 1.16.0 übernommen, seit 1.16.1 umgesetzt: + +- **Kein async-Listener an `runtime.onMessage`.** Ein async-Listener + gibt immer ein Promise zurück und beansprucht damit jede Nachricht. Der + Listener prüft deshalb synchron Absender und Befehl und gibt nur für + bekannte Befehle das Promise zurück, sonst `false`. Neue Befehle kommen + in die Tabelle `BEFEHLE` in `background.js`. +- **Keine ungenutzten Dateien im Paket.** `build.sh` packt nur die in + `DATEIEN` aufgeführten Dateien und weist auf alles andere im Quellbaum + hin. `make_icons.py` erzeugt weiterhin alle Symbole samt SVG — im Paket + landen nur die sieben, die Manifest und Code verwenden. +- **Beschreibung mit Bedienungsanleitung und Bildschirmfotos** auf der + Add-on-Seite, siehe `ATN-BESCHREIBUNG.md`. +- Vor dem Hochladen den Linter laufen lassen: + ```bash + git clone https://github.com/thunderbird/webext-linter + cd webext-linter && npm install + node verify.js /pfad/zu/paperless-connector-.xpi + ``` + --- ## Lizenz @@ -288,6 +362,3 @@ schließt jede Gewährleistung aus. Vor dem ersten produktiven Einsatz: eine Testnachricht ablegen und in Paperless nachsehen, ob Titel, Datum und Anhänge stimmen. - -Es wird weder eine Gewähr noch eine Haftung übernommen. Für entstandene -Schäden haftet der jeweilige Anwender. Die Nutzun erfolgt auf eigenes Risiko. diff --git a/_locales/de/messages.json b/_locales/de/messages.json index 926ee2a..5257776 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -26,9 +26,6 @@ "storeHintPreselect": { "message": "Kleine Bilder und Signaturen sind vorab abgewählt — sie sind meist Teil der Nachricht, keine eigenen Dokumente." }, - "storeWholeMessage": { - "message": "Die vollständige Nachricht als .eml" - }, "storeAttachmentsInside": { "message": "Die $1 Anhänge bleiben darin enthalten.", "placeholders": { @@ -46,26 +43,9 @@ "storeTargetExisting": { "message": "Als neue Version an ein bestehendes anhängen" }, - "storeHintTitleMulti": { - "message": "Bei mehreren Dateien wird der Titel um den Dateinamen ergänzt." - }, - "storeClassifyAfter": { - "message": "Nach dem Ablegen die Klassifizierung bearbeiten" - }, "classifyTitle": { "message": "Klassifizierung bearbeiten" }, - "classifyHintNew": { - "message": "Unbekannte Korrespondenten und Dokumenttypen werden angelegt." - }, - "classifyMoreStored": { - "message": "$1 Dateien abgelegt", - "placeholders": { - "1": { - "content": "$1" - } - } - }, "insertTitle": { "message": "Aus Paperless einfügen" }, @@ -235,9 +215,6 @@ "errNoPermission": { "message": "Ohne Zugriffsrecht auf die Adresse kann die Erweiterung nicht arbeiten." }, - "linkLabel": { - "message": "Datei" - }, "linkValidUntil": { "message": "Link gültig bis $1", "placeholders": { @@ -279,12 +256,6 @@ "optionsHintMailFormat": { "message": "Das PDF ist lesbar und durchsuchbar; HTML-Nachrichten werden dabei als Text gesetzt. Die Originalnachricht steckt als Datei im PDF, es geht also nichts verloren." }, - "storeWholePdf": { - "message": "Die Nachricht als PDF, mit eingebettetem Original" - }, - "storeAsPdf": { - "message": "Als PDF aufbereiten, Original und Anhänge einbetten" - }, "fieldPdfFontSize": { "message": "Schriftgröße" }, @@ -321,9 +292,6 @@ "artPdfUnavailable": { "message": "Für dieses Format nicht möglich - nur Textdateien lassen sich aufbereiten." }, - "btnSkipAll": { - "message": "Alle überspringen" - }, "classifyProgress": { "message": "$1 von $2", "placeholders": { @@ -335,17 +303,6 @@ } } }, - "msgUploadingOf": { - "message": "Wird abgelegt … $1 von $2", - "placeholders": { - "1": { - "content": "$1" - }, - "2": { - "content": "$2" - } - } - }, "stepTitle": { "message": "In Paperless ablegen" }, diff --git a/_locales/en/messages.json b/_locales/en/messages.json index d9aaa1f..82eb0e7 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -26,9 +26,6 @@ "storeHintPreselect": { "message": "Small images and signatures are deselected — they usually belong to the message, not to the content." }, - "storeWholeMessage": { - "message": "The complete message as .eml" - }, "storeAttachmentsInside": { "message": "The $1 attachments remain inside it.", "placeholders": { @@ -46,26 +43,9 @@ "storeTargetExisting": { "message": "Append as a new version to an existing document" }, - "storeHintTitleMulti": { - "message": "With several files the title is extended by the file name." - }, - "storeClassifyAfter": { - "message": "Edit the classification after storing" - }, "classifyTitle": { "message": "Edit classification" }, - "classifyHintNew": { - "message": "Unknown correspondents and document types will be created." - }, - "classifyMoreStored": { - "message": "$1 files stored", - "placeholders": { - "1": { - "content": "$1" - } - } - }, "insertTitle": { "message": "Insert from Paperless" }, @@ -235,9 +215,6 @@ "errNoPermission": { "message": "Without access permission for the address the extension cannot work." }, - "linkLabel": { - "message": "File" - }, "linkValidUntil": { "message": "Link valid until $1", "placeholders": { @@ -279,12 +256,6 @@ "optionsHintMailFormat": { "message": "The PDF is readable and searchable; HTML messages are rendered as text. The original message is embedded in the PDF, so nothing is lost." }, - "storeWholePdf": { - "message": "The message as PDF, with the original embedded" - }, - "storeAsPdf": { - "message": "Prepare as PDF, embedding original and attachments" - }, "fieldPdfFontSize": { "message": "Font size" }, @@ -321,9 +292,6 @@ "artPdfUnavailable": { "message": "Not possible for this format - only text files can be prepared." }, - "btnSkipAll": { - "message": "Skip all" - }, "classifyProgress": { "message": "$1 of $2", "placeholders": { @@ -335,17 +303,6 @@ } } }, - "msgUploadingOf": { - "message": "Storing … $1 of $2", - "placeholders": { - "1": { - "content": "$1" - }, - "2": { - "content": "$2" - } - } - }, "stepTitle": { "message": "Store in Paperless" }, diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index c042878..5eb3b7c 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -26,9 +26,6 @@ "storeHintPreselect": { "message": "Les petites images et les signatures sont décochées — elles font partie du message, pas du contenu." }, - "storeWholeMessage": { - "message": "Le message complet au format .eml" - }, "storeAttachmentsInside": { "message": "Les $1 pièces jointes y restent incluses.", "placeholders": { @@ -46,26 +43,9 @@ "storeTargetExisting": { "message": "Ajouter comme nouvelle version à un document existant" }, - "storeHintTitleMulti": { - "message": "Avec plusieurs fichiers, le titre est complété par le nom du fichier." - }, - "storeClassifyAfter": { - "message": "Modifier le classement après l'enregistrement" - }, "classifyTitle": { "message": "Modifier le classement" }, - "classifyHintNew": { - "message": "Les correspondants et types de document inconnus seront créés." - }, - "classifyMoreStored": { - "message": "$1 fichiers enregistrés", - "placeholders": { - "1": { - "content": "$1" - } - } - }, "insertTitle": { "message": "Insérer depuis Paperless" }, @@ -235,9 +215,6 @@ "errNoPermission": { "message": "Sans droit d'accès à l'adresse, l'extension ne peut pas fonctionner." }, - "linkLabel": { - "message": "Fichier" - }, "linkValidUntil": { "message": "Lien valable jusqu'au $1", "placeholders": { @@ -279,12 +256,6 @@ "optionsHintMailFormat": { "message": "Le PDF est lisible et consultable ; les messages HTML y sont rendus sous forme de texte. Le message d'origine est intégré au PDF, rien n'est perdu." }, - "storeWholePdf": { - "message": "Le message en PDF, avec l'original intégré" - }, - "storeAsPdf": { - "message": "Préparer en PDF, avec l'original et les pièces jointes intégrés" - }, "fieldPdfFontSize": { "message": "Taille de police" }, @@ -321,9 +292,6 @@ "artPdfUnavailable": { "message": "Impossible pour ce format - seuls les fichiers texte peuvent être préparés." }, - "btnSkipAll": { - "message": "Tout ignorer" - }, "classifyProgress": { "message": "$1 sur $2", "placeholders": { @@ -335,17 +303,6 @@ } } }, - "msgUploadingOf": { - "message": "Enregistrement … $1 sur $2", - "placeholders": { - "1": { - "content": "$1" - }, - "2": { - "content": "$2" - } - } - }, "stepTitle": { "message": "Enregistrer dans Paperless" }, diff --git a/background.js b/background.js index 2d7e24c..35c1b82 100644 --- a/background.js +++ b/background.js @@ -315,81 +315,81 @@ browser.runtime.onConnect.addListener((port) => { }); }); -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 - // externally_connectable gesetzt ist - die Prüfung kostet nichts und - // hält die Zusicherung explizit. - if (absender?.id && absender.id !== browser.runtime.id) { - return { ok: false, fehler: "Fremder Absender" }; - } +/* + * Befehle aus den Dialogen. + * + * Der Listener selbst ist bewusst NICHT async: Ein async-Listener liefert + * immer ein Promise zurück und beansprucht damit jede Nachricht - auch + * solche, die gar nicht für ihn bestimmt sind. Stattdessen prüft er + * synchron, ob die Nachricht aus dieser Erweiterung stammt und einen + * bekannten Befehl trägt, und gibt nur dann das Promise des Handlers + * zurück. Alles andere lehnt er mit false ab, sodass andere Empfänger + * antworten können. + * Siehe https://webextension-api.thunderbird.net/en/mv3/guides/runtimeMessaging.html + */ +const BEFEHLE = { + auftrag: async (n) => auftraege.get(n.nummer) || null, + + einstellungen: async () => ladeEinstellungen(), + + berechtigungPruefen: async () => berechtigungPruefen(), + + suchen: async (n) => (await client()).search(n.query), + + dokument: async (n) => (await client()).document(n.id), + + versionen: async (n) => { + const c = await client(); + return c.versions(await c.document(n.id)); + }, + + stammdaten: async () => { + const c = await client(); + const [tags, korr, typen] = await Promise.all( + [c.tags(), c.correspondents(), c.documentTypes()]); + return { tags, korr, typen }; + }, + + schrittOeffnen: async (n) => { + await dialogOeffnen("schritt", n.auftrag); + return undefined; + }, + + dateiListe: async (n) => dateiListe(n.auftrag), + + ablegenEine: async (n) => ablegenEine(n), + + klassifizieren: async (n) => { + const c = await client(); + const payload = { ...n.payload }; + payload.correspondent = await aufloesen(c, "correspondents/", n.korrName); + payload.document_type = await aufloesen(c, "document_types/", n.typName); + await c.patchDocument(n.id, payload); + return undefined; + }, + + anhaengen: async (n) => anhaengenAnEntwurf(n), + + freigabelink: async (n) => freigabelink(n), +}; + +async function befehlAusfuehren(handler, nachricht) { try { - switch (nachricht?.befehl) { - case "auftrag": - return { ok: true, daten: auftraege.get(nachricht.nummer) || null }; - - case "einstellungen": - return { ok: true, daten: await ladeEinstellungen() }; - - case "berechtigungPruefen": - return { ok: true, daten: await berechtigungPruefen() }; - - case "suchen": { - const c = await client(); - return { ok: true, daten: await c.search(nachricht.query) }; - } - - case "dokument": { - const c = await client(); - return { ok: true, daten: await c.document(nachricht.id) }; - } - - case "versionen": { - const c = await client(); - const doc = await c.document(nachricht.id); - return { ok: true, daten: await c.versions(doc) }; - } - - case "stammdaten": { - const c = await client(); - const [tags, korr, typen] = await Promise.all( - [c.tags(), c.correspondents(), c.documentTypes()]); - return { ok: true, daten: { tags, korr, typen } }; - } - - case "schrittOeffnen": - await dialogOeffnen("schritt", nachricht.auftrag); - return { ok: true }; - - case "dateiListe": - return { ok: true, daten: dateiListe(nachricht.auftrag) }; - - case "ablegenEine": - return { ok: true, daten: await ablegenEine(nachricht) }; - - case "klassifizieren": { - const c = await client(); - const payload = { ...nachricht.payload }; - payload.correspondent = await aufloesen( - c, "correspondents/", nachricht.korrName); - payload.document_type = await aufloesen( - c, "document_types/", nachricht.typName); - await c.patchDocument(nachricht.id, payload); - return { ok: true }; - } - - case "anhaengen": - return { ok: true, daten: await anhaengenAnEntwurf(nachricht) }; - - case "freigabelink": - return { ok: true, daten: await freigabelink(nachricht) }; - - default: - return { ok: false, fehler: `Unbekannter Befehl: ${nachricht?.befehl}` }; - } + const daten = await handler(nachricht); + return daten === undefined ? { ok: true } : { ok: true, daten }; } catch (err) { return { ok: false, fehler: String(err?.message || err) }; } +} + +browser.runtime.onMessage.addListener((nachricht, absender) => { + // Nur Nachrichten aus dieser Erweiterung mit bekanntem Befehl annehmen. + if (absender?.id !== browser.runtime.id) return false; + const befehl = nachricht?.befehl; + if (typeof befehl !== "string" || !Object.hasOwn(BEFEHLE, befehl)) { + return false; + } + return befehlAusfuehren(BEFEHLE[befehl], nachricht); }); async function client() { diff --git a/build.sh b/build.sh index 4bb52d6..4aa6c04 100644 --- a/build.sh +++ b/build.sh @@ -11,18 +11,45 @@ NAME="paperless-connector" VERSION="$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' manifest.json | head -1)" OUT="${NAME}-${VERSION}.xpi" -# Vollständigkeit prüfen. zip überspringt fehlende Pfade stillschweigend - -# ohne diese Prüfung entstünde ein Paket, das sich nicht laden lässt. +# Paketinhalt als feste Liste. Nur was hier steht, landet in der .xpi - +# so geraten keine Altlasten (alte Dialoge, ungenutzte Symbole, Quell-SVGs +# aus make_icons.py) ins Paket. addons.thunderbird.net lehnt Pakete mit +# ungenutzten Dateien ab. +DATEIEN=" +manifest.json +background.js +LICENSE +lib/client.js +lib/settings.js +lib/pdf.js +lib/mailpdf.js +ui/gemeinsam.css +ui/gemeinsam.js +ui/store.html +ui/store.js +ui/schritt.html +ui/schritt.js +ui/insert.html +ui/insert.js +ui/options.html +ui/options.js +_locales/de/messages.json +_locales/en/messages.json +_locales/fr/messages.json +icons/paperless-logo_16.png +icons/paperless-logo_32.png +icons/paperless-logo_64.png +icons/paperless-store_16.png +icons/paperless-store_32.png +icons/paperless-insert_16.png +icons/paperless-insert_32.png +" + +# Vollständigkeit prüfen. zip überspringt fehlende Pfade nicht immer +# lautstark - ohne diese Prüfung entstünde ein Paket, das sich nicht +# laden lässt. FEHLT="" -for f in manifest.json background.js \ - lib/client.js lib/settings.js lib/pdf.js lib/mailpdf.js \ - ui/store.html ui/store.js ui/schritt.html ui/schritt.js \ - ui/insert.html ui/insert.js ui/options.html ui/options.js \ - ui/gemeinsam.js ui/gemeinsam.css \ - _locales/de/messages.json _locales/en/messages.json \ - _locales/fr/messages.json \ - icons/paperless-logo_64.png -do +for f in $DATEIEN; do [ -e "$f" ] || FEHLT="$FEHLT $f\n" done if [ -n "$FEHLT" ]; then @@ -32,14 +59,18 @@ if [ -n "$FEHLT" ]; then exit 1 fi -rm -f "$OUT" +# Umgekehrte Prüfung: Dateien im Quellbaum, die nicht im Paket landen. +# Nur ein Hinweis - README, build.sh usw. gehören bewusst nicht hinein. +for f in $(find lib ui icons _locales -type f | sort); do + case " $(echo $DATEIEN) " in + *" $f "*) ;; + *) echo "Hinweis: nicht im Paket: $f" >&2 ;; + esac +done -zip -r -q "$OUT" \ - manifest.json background.js \ - lib ui icons _locales \ - LICENSE \ - -x "*.oxt" "*.xpi" "build.sh" "README.md" "SICHERHEIT.md" \ - "make_icons.py" "*/.*" +rm -f "$OUT" +# shellcheck disable=SC2086 +zip -q -X "$OUT" $DATEIEN if ! unzip -l "$OUT" | grep -q 'manifest.json'; then echo "ABBRUCH: manifest.json fehlt im erzeugten Paket." >&2 diff --git a/icons/paperless-insert.svg b/icons/paperless-insert.svg deleted file mode 100644 index a58fab3..0000000 --- a/icons/paperless-insert.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/icons/paperless-insert_16.png b/icons/paperless-insert_16.png index d8af0b8..932f285 100644 Binary files a/icons/paperless-insert_16.png and b/icons/paperless-insert_16.png differ diff --git a/icons/paperless-insert_32.png b/icons/paperless-insert_32.png index 323ab4b..473037e 100644 Binary files a/icons/paperless-insert_32.png and b/icons/paperless-insert_32.png differ diff --git a/icons/paperless-insert_64.png b/icons/paperless-insert_64.png deleted file mode 100644 index 0f58de4..0000000 Binary files a/icons/paperless-insert_64.png and /dev/null differ diff --git a/icons/paperless-logo.svg b/icons/paperless-logo.svg deleted file mode 100644 index 1685cae..0000000 --- a/icons/paperless-logo.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/icons/paperless-logo_16.png b/icons/paperless-logo_16.png index cca5463..4537592 100644 Binary files a/icons/paperless-logo_16.png and b/icons/paperless-logo_16.png differ diff --git a/icons/paperless-logo_32.png b/icons/paperless-logo_32.png index 4471dfc..7c740e7 100644 Binary files a/icons/paperless-logo_32.png and b/icons/paperless-logo_32.png differ diff --git a/icons/paperless-logo_64.png b/icons/paperless-logo_64.png index cd3bd39..1ff8358 100644 Binary files a/icons/paperless-logo_64.png and b/icons/paperless-logo_64.png differ diff --git a/icons/paperless-share.svg b/icons/paperless-share.svg deleted file mode 100644 index 0b18cdd..0000000 --- a/icons/paperless-share.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/icons/paperless-share_16.png b/icons/paperless-share_16.png deleted file mode 100644 index 8bb0136..0000000 Binary files a/icons/paperless-share_16.png and /dev/null differ diff --git a/icons/paperless-share_32.png b/icons/paperless-share_32.png deleted file mode 100644 index 959b8f8..0000000 Binary files a/icons/paperless-share_32.png and /dev/null differ diff --git a/icons/paperless-share_64.png b/icons/paperless-share_64.png deleted file mode 100644 index ee538c5..0000000 Binary files a/icons/paperless-share_64.png and /dev/null differ diff --git a/icons/paperless-store.svg b/icons/paperless-store.svg deleted file mode 100644 index 953713d..0000000 --- a/icons/paperless-store.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/icons/paperless-store_16.png b/icons/paperless-store_16.png index 9f0cfa5..3963015 100644 Binary files a/icons/paperless-store_16.png and b/icons/paperless-store_16.png differ diff --git a/icons/paperless-store_32.png b/icons/paperless-store_32.png index 6c51b23..03f1796 100644 Binary files a/icons/paperless-store_32.png and b/icons/paperless-store_32.png differ diff --git a/icons/paperless-store_64.png b/icons/paperless-store_64.png deleted file mode 100644 index 7b76fbf..0000000 Binary files a/icons/paperless-store_64.png and /dev/null differ diff --git a/manifest.json b/manifest.json index 96d1f6b..7092ef5 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "__MSG_extName__", "description": "__MSG_extDescription__", - "version": "1.16.0", + "version": "1.16.1", "author": "Marco Morath", "homepage_url": "https://gitea.cloud.mmorath.de/marco.morath/thunderbird-paperless-connector", "default_locale": "de", @@ -16,6 +16,7 @@ "storage", "menus", "messagesRead", + "accountsRead", "compose", "notifications" ], diff --git a/ui/classify.html b/ui/classify.html deleted file mode 100644 index dcad507..0000000 --- a/ui/classify.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - - Klassifizierung bearbeiten - - - -

Klassifizierung bearbeiten

-
- - - - - - - - - - - - - - - -
- - -
-
-
- -
-
- -
- Unbekannte Korrespondenten und Dokumenttypen werden angelegt. -
- -
- -
- - - -
- - - - diff --git a/ui/classify.js b/ui/classify.js deleted file mode 100644 index e712a1c..0000000 --- a/ui/classify.js +++ /dev/null @@ -1,262 +0,0 @@ -/** - * classify.js - Klassifizierung eines gerade abgelegten Dokuments. - * - * Wird unmittelbar nach dem Ablegen aufgerufen. Ohne diesen Schritt trägt - * ein neues Dokument nur den Titel, und nachträglich macht man es - * erfahrungsgemäß nicht mehr. - */ - -import { kurz } from "../lib/settings.js"; -import { uebersetzeSeite, ruf, fehlerAnzeigen, meldungAnzeigen } - from "./gemeinsam.js"; - -const $ = (id) => document.getElementById(id); -let dokumentId = null; -let alleTags = []; - -// Bei mehreren abgelegten Dateien wird jede einzeln klassifiziert. -// Andernfalls trüge nur die erste eine sinnvolle Zuordnung, und die -// übrigen fielen später als unklassifiziert auf - wenn überhaupt. -let liste = []; -let stelle = 0; - -// 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() { - uebersetzeSeite(); - const p = new URLSearchParams(location.search); - - // "ids" für mehrere, "id" für den Einzelfall - letzteres bleibt für - // Aufrufe aus anderen Stellen erhalten. - const roh = p.get("ids") || p.get("id") || ""; - liste = roh.split(",").map((x) => Number(x)).filter(Boolean); - stelle = 0; - - $("ueberspringen").addEventListener("click", weiter); - $("abbrechenAlle").addEventListener("click", () => window.close()); - $("speichern").addEventListener("click", speichern); - - if (!liste.length) { - fehlerAnzeigen("Kein Dokument angegeben."); - return; - } - - try { - const stamm = await ruf("stammdaten"); - alleTags = stamm.tags; - fuelleListe("korrListe", stamm.korr); - fuelleListe("typListe", stamm.typen); - zustand.korr = stamm.korr; - zustand.typen = stamm.typen; - } catch (err) { - fehlerAnzeigen(err.message); - return; - } - - await anzeigen(); -} - -const zustand = { korr: [], typen: [] }; - -/** Das Dokument an der aktuellen Stelle laden und darstellen. */ -async function anzeigen() { - dokumentId = liste[stelle]; - $("speichern").disabled = false; - $("ueberspringen").disabled = false; - meldungAnzeigen(""); - - // Der Knopf zum Abbrechen aller weiteren ist nur sinnvoll, wenn noch - // welche folgen. - $("abbrechenAlle").hidden = liste.length < 2; - - try { - const doc = await ruf("dokument", { id: dokumentId }); - - const zaehler = liste.length > 1 - ? ` · ${browser.i18n.getMessage("classifyProgress", - [String(stelle + 1), String(liste.length)]) - || `${stelle + 1} von ${liste.length}`}` - : ""; - $("quelle").textContent = `#${dokumentId} · ${kurz(doc.title, 60)}` - + zaehler; - - $("titel").value = doc.title || ""; - $("erstellt").value = (doc.created || "").slice(0, 10); - $("korr").value = nameVon(zustand.korr, doc.correspondent); - $("typ").value = nameVon(zustand.typen, doc.document_type); - zeigeTags(doc.tags || []); - if ($("tagSuche")) $("tagSuche").value = ""; - } catch (err) { - fehlerAnzeigen(err.message); - } -} - -/** Zum nächsten Dokument, oder schliessen. */ -async function weiter() { - stelle += 1; - if (stelle >= liste.length) { - window.close(); - return; - } - await anzeigen(); -} - -function fuelleListe(id, eintraege) { - const dl = $(id); - dl.replaceChildren(); - for (const e of eintraege) { - const o = document.createElement("option"); - o.value = e.name; - dl.append(o); - } -} - -function nameVon(liste, id) { - return liste.find((x) => x.id === id)?.name || ""; -} - -function zeigeTags(gesetzt) { - 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 = istGewaehlt; - box.addEventListener("change", () => { - umschalten(tag.id); - zeichneTags(); - }); - - const bez = document.createElement("label"); - bez.htmlFor = box.id; - beschriftungSetzen(bez, String(tag.name), filter); - - zeile.append(box, bez); - 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 { - // 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; - - await ruf("klassifizieren", { - id: dokumentId, - payload, - korrName: $("korr").value.trim(), - typName: $("typ").value.trim() - }); - meldungAnzeigen(browser.i18n.getMessage("msgSaved") || "Gespeichert."); - setTimeout(weiter, 600); - } catch (err) { - fehlerAnzeigen(err.message); - $("speichern").disabled = false; - } -} diff --git a/ui/insert.html b/ui/insert.html index ce7d005..cb67eb2 100644 --- a/ui/insert.html +++ b/ui/insert.html @@ -29,7 +29,7 @@
-
+
@@ -68,6 +68,7 @@
Der Link ist ohne Anmeldung abrufbar. 0 Tage bedeutet unbegrenzt. + Leeres Tag-Feld bedeutet: keine Kennzeichnung setzen.
Nr