Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e9d62cf95 | |||
| 8380dc1584 | |||
| 01a972d719 | |||
| 3f96e6f49d |
95
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-<version>.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 `<section class="kasten">` |
|
||||
| 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-<version>.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.
|
||||
|
||||
@@ -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": {
|
||||
@@ -44,28 +41,11 @@
|
||||
"message": "Neues Dokument anlegen"
|
||||
},
|
||||
"storeTargetExisting": {
|
||||
"message": "An bestehendes Dokument anhängen"
|
||||
},
|
||||
"storeHintTitleMulti": {
|
||||
"message": "Bei mehreren Dateien wird der Titel um den Dateinamen ergänzt."
|
||||
},
|
||||
"storeClassifyAfter": {
|
||||
"message": "Nach dem Ablegen die Klassifizierung bearbeiten"
|
||||
"message": "Als neue Version an ein bestehendes anhängen"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
@@ -308,5 +279,46 @@
|
||||
},
|
||||
"marginBottom": {
|
||||
"message": "unten"
|
||||
},
|
||||
"artOriginal": {
|
||||
"message": "Original ablegen"
|
||||
},
|
||||
"artPdf": {
|
||||
"message": "PDF aufbereiten"
|
||||
},
|
||||
"artMessagePdf": {
|
||||
"message": "als PDF aufbereiten"
|
||||
},
|
||||
"artPdfUnavailable": {
|
||||
"message": "Für dieses Format nicht möglich - nur Textdateien lassen sich aufbereiten."
|
||||
},
|
||||
"classifyProgress": {
|
||||
"message": "$1 von $2",
|
||||
"placeholders": {
|
||||
"1": {
|
||||
"content": "$1"
|
||||
},
|
||||
"2": {
|
||||
"content": "$2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"stepTitle": {
|
||||
"message": "In Paperless ablegen"
|
||||
},
|
||||
"btnNext": {
|
||||
"message": "Weiter"
|
||||
},
|
||||
"stepWholeMessage": {
|
||||
"message": "Die ganze Nachricht"
|
||||
},
|
||||
"stepHintExisting": {
|
||||
"message": "Die Klassifizierung des Ziels wird übernommen und kann angepasst werden."
|
||||
},
|
||||
"msgLoadingTarget": {
|
||||
"message": "Klassifizierung des Ziels wird geladen …"
|
||||
},
|
||||
"msgOpeningStep": {
|
||||
"message": "Wird vorbereitet …"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
@@ -44,28 +41,11 @@
|
||||
"message": "Create a new document"
|
||||
},
|
||||
"storeTargetExisting": {
|
||||
"message": "Append to an existing document"
|
||||
},
|
||||
"storeHintTitleMulti": {
|
||||
"message": "With several files the title is extended by the file name."
|
||||
},
|
||||
"storeClassifyAfter": {
|
||||
"message": "Edit the classification after storing"
|
||||
"message": "Append as a new version to an existing document"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
@@ -308,5 +279,46 @@
|
||||
},
|
||||
"marginBottom": {
|
||||
"message": "bottom"
|
||||
},
|
||||
"artOriginal": {
|
||||
"message": "store original"
|
||||
},
|
||||
"artPdf": {
|
||||
"message": "prepare as PDF"
|
||||
},
|
||||
"artMessagePdf": {
|
||||
"message": "prepare as PDF"
|
||||
},
|
||||
"artPdfUnavailable": {
|
||||
"message": "Not possible for this format - only text files can be prepared."
|
||||
},
|
||||
"classifyProgress": {
|
||||
"message": "$1 of $2",
|
||||
"placeholders": {
|
||||
"1": {
|
||||
"content": "$1"
|
||||
},
|
||||
"2": {
|
||||
"content": "$2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"stepTitle": {
|
||||
"message": "Store in Paperless"
|
||||
},
|
||||
"btnNext": {
|
||||
"message": "Next"
|
||||
},
|
||||
"stepWholeMessage": {
|
||||
"message": "The whole message"
|
||||
},
|
||||
"stepHintExisting": {
|
||||
"message": "The target's classification is copied and can be adjusted."
|
||||
},
|
||||
"msgLoadingTarget": {
|
||||
"message": "Loading the target's classification …"
|
||||
},
|
||||
"msgOpeningStep": {
|
||||
"message": "Preparing …"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
@@ -44,28 +41,11 @@
|
||||
"message": "Créer un nouveau document"
|
||||
},
|
||||
"storeTargetExisting": {
|
||||
"message": "Ajouter à 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"
|
||||
"message": "Ajouter comme nouvelle version à un document existant"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
@@ -308,5 +279,46 @@
|
||||
},
|
||||
"marginBottom": {
|
||||
"message": "bas"
|
||||
},
|
||||
"artOriginal": {
|
||||
"message": "enregistrer l'original"
|
||||
},
|
||||
"artPdf": {
|
||||
"message": "préparer en PDF"
|
||||
},
|
||||
"artMessagePdf": {
|
||||
"message": "préparer en PDF"
|
||||
},
|
||||
"artPdfUnavailable": {
|
||||
"message": "Impossible pour ce format - seuls les fichiers texte peuvent être préparés."
|
||||
},
|
||||
"classifyProgress": {
|
||||
"message": "$1 sur $2",
|
||||
"placeholders": {
|
||||
"1": {
|
||||
"content": "$1"
|
||||
},
|
||||
"2": {
|
||||
"content": "$2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"stepTitle": {
|
||||
"message": "Enregistrer dans Paperless"
|
||||
},
|
||||
"btnNext": {
|
||||
"message": "Suivant"
|
||||
},
|
||||
"stepWholeMessage": {
|
||||
"message": "Le message entier"
|
||||
},
|
||||
"stepHintExisting": {
|
||||
"message": "Le classement de la cible est repris et peut être ajusté."
|
||||
},
|
||||
"msgLoadingTarget": {
|
||||
"message": "Chargement du classement de la cible …"
|
||||
},
|
||||
"msgOpeningStep": {
|
||||
"message": "Préparation …"
|
||||
}
|
||||
}
|
||||
|
||||
291
background.js
@@ -16,7 +16,8 @@ import {
|
||||
ladeEinstellungen, vollstaendig, sichererName, versionslabel,
|
||||
titelAusBetreff, zugriffsmuster, t
|
||||
} from "./lib/settings.js";
|
||||
import { baueMailPdf, alsEmpfaenger } from "./lib/mailpdf.js";
|
||||
import { baueMailPdf, baueTextPdf, alsEmpfaenger, umwandelbar }
|
||||
from "./lib/mailpdf.js";
|
||||
|
||||
/**
|
||||
* Ergebnis einer Nachrichten-Abfrage vereinheitlichen.
|
||||
@@ -255,7 +256,10 @@ async function dialogOeffnen(art, daten) {
|
||||
// Absender und Dateinamen; das muss nicht bis zum Neustart im Speicher
|
||||
// liegen, nur weil jemand den Dialog offen stehen liess.
|
||||
setTimeout(() => auftraege.delete(nummer), 30 * 60 * 1000);
|
||||
const seite = art === "einfuegen" ? "ui/insert.html" : "ui/store.html";
|
||||
const seite = {
|
||||
einfuegen: "ui/insert.html",
|
||||
schritt: "ui/schritt.html"
|
||||
}[art] || "ui/store.html";
|
||||
|
||||
// Die Adresse MUSS über runtime.getURL aufgelöst werden. Ein relativer
|
||||
// Pfad wird in Manifest V3 nicht gegen das Erweiterungsverzeichnis
|
||||
@@ -266,8 +270,12 @@ async function dialogOeffnen(art, daten) {
|
||||
await browser.windows.create({
|
||||
url,
|
||||
type: "popup",
|
||||
width: art === "einfuegen" ? 760 : 820,
|
||||
height: art === "einfuegen" ? 640 : 720
|
||||
// Der Schrittdialog steht zweispaltig. Die Höhe ist auf den Inhalt
|
||||
// gerechnet: rund 540 px für die Spalten plus Kopf, Meldung und
|
||||
// Fusszeile, dazu etwas Spielraum - die Systemschrift fällt je
|
||||
// nach Umgebung anders aus.
|
||||
width: { schritt: 980, einfuegen: 760 }[art] || 820,
|
||||
height: { schritt: 760, einfuegen: 640 }[art] || 520
|
||||
});
|
||||
} catch (err) {
|
||||
// Manche Stände lehnen type "popup" ab. Dann ein gewöhnliches Fenster.
|
||||
@@ -296,80 +304,92 @@ const verbindungen = new Set();
|
||||
browser.runtime.onConnect.addListener((port) => {
|
||||
verbindungen.add(port);
|
||||
console.debug("[paperless] Dialog verbunden, offen:", verbindungen.size);
|
||||
|
||||
// Die Lebenszeichen werden nicht ausgewertet - allein ihr Eintreffen
|
||||
// hält die Ereignisseite wach.
|
||||
port.onMessage.addListener(() => {});
|
||||
|
||||
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
|
||||
// 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" };
|
||||
}
|
||||
try {
|
||||
switch (nachricht?.befehl) {
|
||||
case "auftrag":
|
||||
return { ok: true, daten: auftraege.get(nachricht.nummer) || null };
|
||||
/*
|
||||
* 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,
|
||||
|
||||
case "einstellungen":
|
||||
return { ok: true, daten: await ladeEinstellungen() };
|
||||
einstellungen: async () => ladeEinstellungen(),
|
||||
|
||||
case "berechtigungPruefen":
|
||||
return { ok: true, daten: await berechtigungPruefen() };
|
||||
berechtigungPruefen: async () => berechtigungPruefen(),
|
||||
|
||||
case "suchen": {
|
||||
suchen: async (n) => (await client()).search(n.query),
|
||||
|
||||
dokument: async (n) => (await client()).document(n.id),
|
||||
|
||||
versionen: async (n) => {
|
||||
const c = await client();
|
||||
return { ok: true, daten: await c.search(nachricht.query) };
|
||||
}
|
||||
return c.versions(await c.document(n.id));
|
||||
},
|
||||
|
||||
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": {
|
||||
stammdaten: async () => {
|
||||
const c = await client();
|
||||
const [tags, korr, typen] = await Promise.all(
|
||||
[c.tags(), c.correspondents(), c.documentTypes()]);
|
||||
return { ok: true, daten: { tags, korr, typen } };
|
||||
}
|
||||
return { tags, korr, typen };
|
||||
},
|
||||
|
||||
case "ablegen":
|
||||
return { ok: true, daten: await ablegen(nachricht) };
|
||||
schrittOeffnen: async (n) => {
|
||||
await dialogOeffnen("schritt", n.auftrag);
|
||||
return undefined;
|
||||
},
|
||||
|
||||
case "klassifizieren": {
|
||||
dateiListe: async (n) => dateiListe(n.auftrag),
|
||||
|
||||
ablegenEine: async (n) => ablegenEine(n),
|
||||
|
||||
klassifizieren: async (n) => {
|
||||
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 };
|
||||
}
|
||||
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;
|
||||
},
|
||||
|
||||
case "anhaengen":
|
||||
return { ok: true, daten: await anhaengenAnEntwurf(nachricht) };
|
||||
anhaengen: async (n) => anhaengenAnEntwurf(n),
|
||||
|
||||
case "freigabelink":
|
||||
return { ok: true, daten: await freigabelink(nachricht) };
|
||||
freigabelink: async (n) => freigabelink(n),
|
||||
};
|
||||
|
||||
default:
|
||||
return { ok: false, fehler: `Unbekannter Befehl: ${nachricht?.befehl}` };
|
||||
}
|
||||
async function befehlAusfuehren(handler, nachricht) {
|
||||
try {
|
||||
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() {
|
||||
@@ -684,43 +704,160 @@ async function datenHolen(auftrag, s) {
|
||||
if (!d.ausgewaehlt) continue;
|
||||
const datei = await browser.messages.getAttachmentFile(
|
||||
d.messageId, d.partName);
|
||||
|
||||
// Aufbereiten nur, wenn der Anwender es gewählt hat UND das Format es
|
||||
// hergibt. Eine Rechnung als PDF ist bereits das Dokument; sie durch
|
||||
// eine Textfassung zu ersetzen wäre Unfug.
|
||||
if (d.alsPdf && umwandelbar(d.contentType, d.name)) {
|
||||
try {
|
||||
out.push(await anhangAlsPdf(datei, d, s));
|
||||
continue;
|
||||
} catch (err) {
|
||||
console.error("[paperless] Anhang konnte nicht aufbereitet werden",
|
||||
d.name, err);
|
||||
// Nicht abbrechen: lieber das Original ablegen als gar nichts.
|
||||
}
|
||||
}
|
||||
out.push({ blob: datei, name: sichererName(d.name) });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async function ablegen(nachricht) {
|
||||
/**
|
||||
* Einen Textanhang als PDF aufbereiten, mit der Ursprungsdatei darin.
|
||||
*
|
||||
* Sinnvoll etwa bei einem HTML-Anhang, den im Archiv sonst niemand
|
||||
* ansieht. Für Formate ohne Textinhalt greift diese Funktion nicht -
|
||||
* das prüft umwandelbar() vorher.
|
||||
*/
|
||||
async function anhangAlsPdf(datei, beschreibung, s) {
|
||||
const roh = new Uint8Array(await datei.arrayBuffer());
|
||||
let text = new TextDecoder("utf-8", { fatal: false }).decode(roh);
|
||||
|
||||
const istHtml = /^text\/html$/i.test(beschreibung.contentType || "")
|
||||
|| /\.(html?|xhtml)$/i.test(beschreibung.name || "")
|
||||
|| /<\s*html[\s>]/i.test(text.slice(0, 2000));
|
||||
|
||||
if (istHtml && browser.messengerUtilities?.convertToPlainText) {
|
||||
try {
|
||||
text = await browser.messengerUtilities.convertToPlainText(text);
|
||||
} catch { /* dann bleibt der Rohtext */ }
|
||||
}
|
||||
|
||||
const basis = String(beschreibung.name || "Dokument")
|
||||
.replace(/\.[^.]{1,8}$/, "");
|
||||
|
||||
const pdf = baueTextPdf({
|
||||
titel: basis,
|
||||
text,
|
||||
dateien: [{
|
||||
name: sichererName(beschreibung.name || "original"),
|
||||
daten: roh,
|
||||
mime: beschreibung.contentType || "application/octet-stream",
|
||||
beschreibung: "Originaldatei"
|
||||
}]
|
||||
}, {
|
||||
schrift: Number(s.pdfSchriftgroesse) || 11,
|
||||
format: s.pdfFormat || "a4",
|
||||
rand: {
|
||||
links: Number(s.pdfRandLinks ?? 20),
|
||||
rechts: Number(s.pdfRandRechts ?? 10),
|
||||
oben: Number(s.pdfRandOben ?? 10),
|
||||
unten: Number(s.pdfRandUnten ?? 10)
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
blob: new Blob([pdf], { type: "application/pdf" }),
|
||||
name: sichererName(`${basis}.pdf`)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Dateien eines Auftrags aufzählen, ohne sie zu laden.
|
||||
*
|
||||
* Der Dialog fragt damit ab, wie viele Schritte anstehen, und ruft dann
|
||||
* jeden einzeln auf.
|
||||
*/
|
||||
function dateiListe(auftrag) {
|
||||
if (auftrag.quelle === "nachricht") {
|
||||
return [{ art: "nachricht" }];
|
||||
}
|
||||
return (auftrag.dateien || [])
|
||||
.map((d, i) => ({ art: "anhang", index: i, name: d.name }))
|
||||
.filter((_x, i) => auftrag.dateien[i].ausgewaehlt);
|
||||
}
|
||||
|
||||
/**
|
||||
* EINE Datei ablegen.
|
||||
*
|
||||
* Bewusst je Datei ein eigener Aufruf statt einer Schleife über alle:
|
||||
*
|
||||
* * Die Ereignisseite wird in Manifest V3 beendet, sobald sie untätig
|
||||
* scheint. Ein Aufruf über zehn Anhänge lief regelmäßig in
|
||||
* "Receiving end does not exist".
|
||||
* * Der Dialog kann den Fortschritt anzeigen.
|
||||
* * Scheitert eine Datei, sind die übrigen trotzdem abgelegt.
|
||||
*/
|
||||
async function ablegenEine(nachricht) {
|
||||
const c = await client();
|
||||
const s = await ladeEinstellungen();
|
||||
const dateien = await datenHolen(nachricht.auftrag, s);
|
||||
if (!dateien.length) throw new PaperlessError(t("errNothingSelected"));
|
||||
const auftrag = nachricht.auftrag;
|
||||
const eintrag = nachricht.eintrag;
|
||||
|
||||
let datei;
|
||||
if (eintrag.art === "nachricht") {
|
||||
const alle = await datenHolen(auftrag, s);
|
||||
datei = alle[0];
|
||||
} else {
|
||||
const beschreibung = auftrag.dateien[eintrag.index];
|
||||
const roh = await browser.messages.getAttachmentFile(
|
||||
beschreibung.messageId, beschreibung.partName);
|
||||
|
||||
if (beschreibung.alsPdf
|
||||
&& umwandelbar(beschreibung.contentType, beschreibung.name)) {
|
||||
try {
|
||||
datei = await anhangAlsPdf(roh, beschreibung, s);
|
||||
} catch (err) {
|
||||
console.error("[paperless] Aufbereitung gescheitert",
|
||||
beschreibung.name, err);
|
||||
datei = { blob: roh, name: sichererName(beschreibung.name) };
|
||||
}
|
||||
} else {
|
||||
datei = { blob: roh, name: sichererName(beschreibung.name) };
|
||||
}
|
||||
}
|
||||
|
||||
if (!datei) throw new PaperlessError(t("errNothingSelected"));
|
||||
|
||||
const melden = (sek) => {
|
||||
if (sek % 15 === 0) {
|
||||
console.debug("[paperless] warte auf Aufgabe:", sek, "s");
|
||||
}
|
||||
};
|
||||
|
||||
const ergebnisse = [];
|
||||
for (const datei of dateien) {
|
||||
if (nachricht.ziel === "vorhanden") {
|
||||
const antwort = await c.updateVersion(
|
||||
nachricht.zielId, datei.blob, datei.name,
|
||||
versionslabel(s.versionLabel));
|
||||
const id = await c.waitForTask(antwort, (sek) => {
|
||||
if (sek % 15 === 0) {
|
||||
console.debug("[paperless] warte auf Aufgabe:", sek, "s");
|
||||
const id = await c.waitForTask(antwort, melden);
|
||||
return { id, name: datei.name, art: "version", zielId: nachricht.zielId };
|
||||
}
|
||||
});
|
||||
ergebnisse.push({ id, name: datei.name, art: "version",
|
||||
zielId: nachricht.zielId });
|
||||
} else {
|
||||
const felder = { title: nachricht.titel || datei.name };
|
||||
|
||||
// Bei mehreren Dateien wird der Titel um den Dateinamen ergänzt -
|
||||
// sonst hiessen alle gleich und wären im Archiv nicht auseinander-
|
||||
// zuhalten.
|
||||
const grundtitel = nachricht.titel || datei.name;
|
||||
const titel = nachricht.mehrere
|
||||
? `${grundtitel} - ${datei.name.replace(/\.[^.]{1,8}$/, "")}`
|
||||
: grundtitel;
|
||||
|
||||
const felder = { title: titel.slice(0, 127) };
|
||||
if (nachricht.erstellt) felder.created = nachricht.erstellt;
|
||||
|
||||
const antwort = await c.postDocument(datei.blob, datei.name, felder);
|
||||
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" });
|
||||
}
|
||||
}
|
||||
return ergebnisse;
|
||||
const id = await c.waitForTask(antwort, melden);
|
||||
return { id, name: datei.name, art: "neu" };
|
||||
}
|
||||
|
||||
async function aufloesen(c, pfad, name) {
|
||||
|
||||
67
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.
|
||||
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/classify.html ui/classify.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 \
|
||||
# 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
|
||||
do
|
||||
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 $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
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<polygon points="0.6,0.5 13,0.5 18.4,5.9 18.4,23.4 0.6,23.4" fill="#fcfcfa" stroke="#175420" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<polygon points="13,0.5 18.4,5.9 13,5.9" fill="#175420"/>
|
||||
<line x1="2.8" y1="7.9" x2="13.2" y2="7.9" fill="none" stroke="#175420" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="2.8" y1="11.7" x2="15.4" y2="11.7" fill="none" stroke="#175420" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="2.8" y1="15.5" x2="11.2" y2="15.5" fill="none" stroke="#175420" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="16.9" cy="17.1" r="7.8" fill="#ffffff"/>
|
||||
<circle cx="16.9" cy="17.1" r="6.7" fill="#1e60a8"/>
|
||||
<polygon points="16.9,20.6 14,17.4 19.8,17.4" fill="#fcfcfa"/>
|
||||
<line x1="16.9" y1="14.125" x2="16.9" y2="17.7" fill="none" stroke="#fcfcfa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 885 B After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<rect x="0.4" y="0.4" width="23.2" height="23.2" rx="4" fill="#175420"/>
|
||||
<polygon points="5,3.9 14.6,3.9 19,8.3 19,20.1 5,20.1" fill="#fcfcfa"/>
|
||||
<polygon points="14.6,3.9 19,8.3 14.6,8.3" fill="#3a8a44"/>
|
||||
<line x1="7" y1="9.9" x2="14.4" y2="9.9" fill="none" stroke="#175420" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="7" y1="13.3" x2="16.2" y2="13.3" fill="none" stroke="#175420" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="7" y1="16.7" x2="13" y2="16.7" fill="none" stroke="#175420" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 763 B |
|
Before Width: | Height: | Size: 680 B After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 7.5 KiB |
@@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<polygon points="0.6,0.5 13,0.5 18.4,5.9 18.4,23.4 0.6,23.4" fill="#fcfcfa" stroke="#175420" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<polygon points="13,0.5 18.4,5.9 13,5.9" fill="#175420"/>
|
||||
<line x1="2.8" y1="7.9" x2="13.2" y2="7.9" fill="none" stroke="#175420" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="2.8" y1="11.7" x2="15.4" y2="11.7" fill="none" stroke="#175420" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="2.8" y1="15.5" x2="11.2" y2="15.5" fill="none" stroke="#175420" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="16.9" cy="17.1" r="7.8" fill="#ffffff"/>
|
||||
<circle cx="16.9" cy="17.1" r="6.7" fill="#117a7a"/>
|
||||
<line x1="13.7" y1="17.1" x2="19.5" y2="13.9" fill="none" stroke="#fcfcfa" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="13.7" y1="17.1" x2="19.5" y2="20.3" fill="none" stroke="#fcfcfa" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="19.5" cy="13.9" r="1.65" fill="#fcfcfa"/>
|
||||
<circle cx="19.5" cy="20.3" r="1.65" fill="#fcfcfa"/>
|
||||
<circle cx="13.7" cy="17.1" r="1.65" fill="#fcfcfa"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 883 B |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
@@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<polygon points="0.6,0.5 13,0.5 18.4,5.9 18.4,23.4 0.6,23.4" fill="#fcfcfa" stroke="#175420" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<polygon points="13,0.5 18.4,5.9 13,5.9" fill="#175420"/>
|
||||
<line x1="2.8" y1="7.9" x2="13.2" y2="7.9" fill="none" stroke="#175420" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="2.8" y1="11.7" x2="15.4" y2="11.7" fill="none" stroke="#175420" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="2.8" y1="15.5" x2="11.2" y2="15.5" fill="none" stroke="#175420" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="16.9" cy="17.1" r="7.8" fill="#ffffff"/>
|
||||
<circle cx="16.9" cy="17.1" r="6.7" fill="#c86a14"/>
|
||||
<polygon points="15.7,13.6 12.8,16.8 18.6,16.8" fill="#fcfcfa"/>
|
||||
<line x1="15.7" y1="16.5" x2="15.7" y2="20.075" fill="none" stroke="#fcfcfa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="18.6" y1="14.5" x2="22" y2="14.5" fill="none" stroke="#fcfcfa" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="20.3" y1="12.8" x2="20.3" y2="16.2" fill="none" stroke="#fcfcfa" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 889 B After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
105
lib/client.js
@@ -183,16 +183,113 @@ export class PaperlessClient {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dokumente suchen. Vier Wege, in dieser Reihenfolge:
|
||||
*
|
||||
* 1. Archiv-Seriennummer, wenn die Eingabe nur aus Ziffern besteht.
|
||||
* Sie ist ein eigenes Feld - die Volltextsuche findet sie NICHT.
|
||||
*
|
||||
* 2. Die GLOBALE SUCHE von Paperless (/api/search/). Dieselbe
|
||||
* Schnittstelle, die die Suchleiste oben in der Weboberfläche
|
||||
* bedient. Sie verbindet Titelsuche und Volltext und liefert
|
||||
* damit die Ergebnisse, die man aus der Oberfläche kennt.
|
||||
*
|
||||
* 3. Teilzeichenkette in Titel und Inhalt. Greift, wenn die globale
|
||||
* Suche nichts liefert oder in der eingesetzten Version fehlt.
|
||||
*
|
||||
* 4. Volltextsuche über den Index, mit UND-Verknüpfung. Sie
|
||||
* beherrscht Wortstämme und findet auch Beugungsformen.
|
||||
*
|
||||
* Zwei Fallstricke, die hier berücksichtigt sind:
|
||||
*
|
||||
* * Bei der Volltextsuche darf KEIN ordering mitgegeben werden.
|
||||
* Paperless sortiert dort nach Relevanz; eine eigene
|
||||
* Sortierangabe führte dazu, dass die Suchanfrage verworfen wurde
|
||||
* und schlicht die zuletzt hinzugefügten Dokumente kamen.
|
||||
*
|
||||
* * Ohne UND-Verknüpfung sucht der Index mit ODER. "Anschreiben
|
||||
* Markus Straub" lieferte dann alles, was irgendeines der drei
|
||||
* Wörter enthält - der gesuchte Treffer ging darin unter.
|
||||
*/
|
||||
async search(query, limit = 50) {
|
||||
const q = String(query || "").trim();
|
||||
|
||||
if (!q) return this.#suchlauf({ ordering: "-added" }, limit);
|
||||
|
||||
// 1. Seriennummer
|
||||
if (/^\d+$/.test(q)) {
|
||||
const asn = await this.#suchlauf({ archive_serial_number: q }, limit);
|
||||
if (asn.length) return asn;
|
||||
}
|
||||
|
||||
// 2. Globale Suche wie in der Weboberfläche
|
||||
const global = await this.#globaleSuche(q, limit);
|
||||
if (global.length) return global;
|
||||
|
||||
// 3. Titel und Inhalt
|
||||
const text = await this.#suchlauf(
|
||||
{ title_content: q, ordering: "-created" }, limit);
|
||||
if (text.length) return text;
|
||||
|
||||
// 4. Volltext mit UND, ohne ordering.
|
||||
//
|
||||
// Nach dem Ersetzen der Sonderzeichen ERNEUT zerlegen: Aus
|
||||
// "Müller-Lüdenscheidt" würde sonst ein Wort mit Leerzeichen, und
|
||||
// die UND-Verknüpfung wäre wieder aufgehoben.
|
||||
const worte = q.replace(/[+\-&|!(){}[\]^"~*?:\\/]/g, " ")
|
||||
.split(/\s+/).filter(Boolean);
|
||||
if (!worte.length) return [];
|
||||
return this.#suchlauf({ query: worte.join(" AND ") }, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Schnittstelle hinter der Suchleiste der Weboberfläche.
|
||||
*
|
||||
* Sie gibt es erst ab neueren Paperless-Ständen, und die Form der
|
||||
* Antwort ist nicht überall gleich - mal ein Feld "documents", mal
|
||||
* "results". Deshalb wird beides angenommen und bei jedem Fehler
|
||||
* stillschweigend auf den nächsten Weg ausgewichen.
|
||||
*/
|
||||
async #globaleSuche(q, limit) {
|
||||
let antwort;
|
||||
try {
|
||||
const params = new URLSearchParams({ query: q,
|
||||
page_size: String(limit) });
|
||||
antwort = await this.#json(`search/?${params}`);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
if (!antwort) return [];
|
||||
|
||||
for (const schluessel of ["documents", "results"]) {
|
||||
const rows = antwort[schluessel];
|
||||
if (Array.isArray(rows) && rows.length) {
|
||||
// Die globale Suche liefert je nach Version verschiedene
|
||||
// Objektarten. Nur echte Dokumente durchlassen.
|
||||
return rows
|
||||
.filter((r) => r && typeof r === "object"
|
||||
&& r.id !== undefined && r.title !== undefined)
|
||||
.slice(0, limit);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
async #suchlauf(zusatz, limit) {
|
||||
const params = new URLSearchParams({
|
||||
page_size: String(limit),
|
||||
ordering: "-added",
|
||||
fields: "id,title,created,added,archive_serial_number,mime_type," +
|
||||
"original_file_name"
|
||||
// Ohne diese Einschränkung liefert Paperless je Treffer auch den
|
||||
// vollständigen Inhalt - bei fünfzig Treffern mehrere Megabyte.
|
||||
fields: "id,title,created,added,archive_serial_number,mime_type,"
|
||||
+ "original_file_name"
|
||||
});
|
||||
if (query) params.set("query", query);
|
||||
for (const [k, v] of Object.entries(zusatz)) params.set(k, String(v));
|
||||
try {
|
||||
const data = await this.#json(`documents/?${params}`);
|
||||
return data?.results || [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
document(id) {
|
||||
|
||||
@@ -190,6 +190,79 @@ function kopfblock(d, daten, groesse) {
|
||||
d.y = oben - hoehe;
|
||||
}
|
||||
|
||||
/**
|
||||
* Beliebigen Text als PDF setzen, mit der Ursprungsdatei als Anhang.
|
||||
*
|
||||
* Für Anhänge, die Text enthalten - HTML, Nur-Text, eingebettete
|
||||
* Nachrichten. Formate wie DOCX oder XLSX lassen sich hier NICHT
|
||||
* umwandeln; dafür bräuchte es eine Darstellungsmaschine.
|
||||
*/
|
||||
export function baueTextPdf(daten, opt = {}) {
|
||||
const basis = begrenzen(Number(opt.schrift) || 11, 7, 16);
|
||||
const papier = FORMATE[String(opt.format || "a4").toLowerCase()]
|
||||
|| FORMATE.a4;
|
||||
const r = opt.rand || {};
|
||||
const grenze = (wert, vorgabe) =>
|
||||
Math.min(papier.breite / 3, Math.max(0, Number(wert) ?? vorgabe));
|
||||
|
||||
const d = new PdfDokument({
|
||||
titel: daten.titel || "Dokument",
|
||||
breite: mm(papier.breite),
|
||||
hoehe: mm(papier.hoehe),
|
||||
randLinks: mm(grenze(r.links ?? 20, 20)),
|
||||
randRechts: mm(grenze(r.rechts ?? 10, 10)),
|
||||
randOben: mm(grenze(r.oben ?? 10, 10)),
|
||||
randUnten: mm(grenze(r.unten ?? 10, 10))
|
||||
});
|
||||
d.neueSeite();
|
||||
|
||||
d.absatz(daten.titel || "Dokument",
|
||||
{ groesse: basis + 2, fett: true, abstand: 1.25 });
|
||||
d.abstand(6);
|
||||
d.linie();
|
||||
d.abstand(8);
|
||||
|
||||
const text = String(daten.text ?? "").replace(/\r\n?/g, "\n").trimEnd();
|
||||
if (text) {
|
||||
for (const zeile of text.split("\n")) {
|
||||
d.absatz(zeile || " ", { groesse: basis, abstand: 1.35 });
|
||||
}
|
||||
} else {
|
||||
d.absatz("(kein Textinhalt - siehe eingebettete Originaldatei)",
|
||||
{ groesse: basis, farbe: GRAU_TEXT });
|
||||
}
|
||||
|
||||
d.abstand(10);
|
||||
d.linie();
|
||||
d.absatz("Aus der Originaldatei erzeugt, die diesem PDF beiliegt.",
|
||||
{ groesse: Math.max(6.5, basis - 3), farbe: GRAU_TEXT });
|
||||
|
||||
for (const f of daten.dateien ?? []) {
|
||||
d.anhang(f.name, f.daten, {
|
||||
beschreibung: f.beschreibung || "",
|
||||
mime: f.mime || "application/octet-stream"
|
||||
});
|
||||
}
|
||||
return d.bauen();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lässt sich diese Datei als PDF aufbereiten?
|
||||
*
|
||||
* Nur was Text enthält. Bei DOCX oder XLSX müsste die Erweiterung das
|
||||
* Format auswerten und setzen - dafür gibt es keine Grundlage.
|
||||
*/
|
||||
export function umwandelbar(mime, name = "") {
|
||||
const m = String(mime || "").toLowerCase();
|
||||
if (/^text\//.test(m)) return true;
|
||||
if (m === "message/rfc822") return true;
|
||||
if (["application/json", "application/xml", "application/xhtml+xml",
|
||||
"application/rtf"].includes(m)) return true;
|
||||
// Manche Server melden alles als octet-stream. Dann entscheidet die
|
||||
// Endung.
|
||||
return /\.(txt|htm|html|xml|json|csv|md|log|eml)$/i.test(String(name));
|
||||
}
|
||||
|
||||
function begrenzen(wert, min, max) {
|
||||
return Math.min(max, Math.max(min, wert));
|
||||
}
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
"manifest_version": 3,
|
||||
"name": "__MSG_extName__",
|
||||
"description": "__MSG_extDescription__",
|
||||
"version": "1.11.3",
|
||||
"author": "Name des Herausgebers",
|
||||
"homepage_url": "https://example.org/paperless-thunderbird",
|
||||
"version": "1.16.1",
|
||||
"author": "Marco Morath",
|
||||
"homepage_url": "https://gitea.cloud.mmorath.de/marco.morath/thunderbird-paperless-connector",
|
||||
"default_locale": "de",
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "paperless@example.org",
|
||||
"id": "webmaster@mmorath.de",
|
||||
"strict_min_version": "128.0"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title data-i18n="classifyTitle">Klassifizierung bearbeiten</title>
|
||||
<link rel="stylesheet" href="gemeinsam.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1 data-i18n="classifyTitle">Klassifizierung bearbeiten</h1>
|
||||
<div id="quelle" class="hinweis"></div>
|
||||
|
||||
<label for="titel" data-i18n="fieldTitle">Titel</label>
|
||||
<input type="text" id="titel">
|
||||
|
||||
<label for="erstellt" data-i18n="fieldCreated">Erstellt</label>
|
||||
<input type="text" id="erstellt" placeholder="JJJJ-MM-TT">
|
||||
|
||||
<label for="korr" data-i18n="fieldCorrespondent">Korrespondent</label>
|
||||
<input type="text" id="korr" list="korrListe" autocomplete="off">
|
||||
<datalist id="korrListe"></datalist>
|
||||
|
||||
<label for="typ" data-i18n="fieldDocType">Dokumenttyp</label>
|
||||
<input type="text" id="typ" list="typListe" autocomplete="off">
|
||||
<datalist id="typListe"></datalist>
|
||||
|
||||
<div class="wachsen">
|
||||
<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">
|
||||
Unbekannte Korrespondenten und Dokumenttypen werden angelegt.
|
||||
</div>
|
||||
|
||||
<div id="meldung" class="hinweis"></div>
|
||||
|
||||
<div class="fuss">
|
||||
<button id="ueberspringen" data-i18n="btnSkip">Überspringen</button>
|
||||
<button id="speichern" class="haupt" data-i18n="btnSave">Speichern</button>
|
||||
</div>
|
||||
|
||||
<script type="module" src="classify.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
212
ui/classify.js
@@ -1,212 +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 = [];
|
||||
|
||||
// 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);
|
||||
dokumentId = Number(p.get("id"));
|
||||
const weitere = Number(p.get("weiter") || 1);
|
||||
|
||||
$("ueberspringen").addEventListener("click", () => window.close());
|
||||
$("speichern").addEventListener("click", speichern);
|
||||
|
||||
try {
|
||||
const [doc, stamm] = await Promise.all([
|
||||
ruf("dokument", { id: dokumentId }),
|
||||
ruf("stammdaten")
|
||||
]);
|
||||
alleTags = stamm.tags;
|
||||
|
||||
$("quelle").textContent =
|
||||
`#${dokumentId} · ${kurz(doc.title, 70)}`
|
||||
+ (weitere > 1
|
||||
? ` · ${browser.i18n.getMessage("classifyMoreStored",
|
||||
[String(weitere)]) || `${weitere} Dateien abgelegt`}`
|
||||
: "");
|
||||
|
||||
$("titel").value = doc.title || "";
|
||||
$("erstellt").value = (doc.created || "").slice(0, 10);
|
||||
fuelleListe("korrListe", stamm.korr);
|
||||
fuelleListe("typListe", stamm.typen);
|
||||
$("korr").value = nameVon(stamm.korr, doc.correspondent);
|
||||
$("typ").value = nameVon(stamm.typen, doc.document_type);
|
||||
zeigeTags(doc.tags || []);
|
||||
} catch (err) {
|
||||
fehlerAnzeigen(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
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(() => window.close(), 900);
|
||||
} catch (err) {
|
||||
fehlerAnzeigen(err.message);
|
||||
$("speichern").disabled = false;
|
||||
}
|
||||
}
|
||||
129
ui/gemeinsam.css
@@ -1,12 +1,21 @@
|
||||
/* Gemeinsames Aussehen der Dialoge. Bewusst zurückhaltend, damit sie sich
|
||||
in die Oberfläche von Thunderbird einfügen statt sich abzuheben. */
|
||||
/* Gemeinsames Aussehen der Dialoge.
|
||||
Bewusst zurückhaltend, damit sie sich in die Oberfläche von
|
||||
Thunderbird einfügen statt sich abzuheben. */
|
||||
|
||||
:root {
|
||||
--gruen: #175420;
|
||||
--rand: #c8ccd0;
|
||||
--hell: #f6f6f4;
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
/* Muss VOR den Anzeigearten stehen und !important tragen: Eine Regel wie
|
||||
".wahl { display: flex }" überstimmt sonst das eingebaute
|
||||
"[hidden] { display: none }", und ausgeblendete Elemente erscheinen. */
|
||||
[hidden] { display: none !important; }
|
||||
|
||||
body {
|
||||
font: message-box;
|
||||
font-size: 13px;
|
||||
@@ -15,9 +24,16 @@ body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
/* Feste Höhe UND overflow:hidden. Mit min-height wuchs die Seite über
|
||||
das Fenster hinaus, und die Fußzeile lag unerreichbar darunter.
|
||||
Gestaucht wird stattdessen innen: Die Listen blättern selbst. */
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
h1 { font-size: 14px; margin: 0 0 2px; font-weight: 600; }
|
||||
|
||||
fieldset {
|
||||
border: 1px solid var(--rand);
|
||||
border-radius: 4px;
|
||||
@@ -26,7 +42,9 @@ fieldset {
|
||||
min-width: 0;
|
||||
}
|
||||
legend { padding: 0 4px; font-weight: 600; }
|
||||
|
||||
label { display: block; margin: 4px 0 2px; }
|
||||
|
||||
input[type=text], input[type=password], input[type=number], select {
|
||||
width: 100%;
|
||||
padding: 4px 6px;
|
||||
@@ -35,15 +53,30 @@ input[type=text], input[type=password], input[type=number], select {
|
||||
background: Field;
|
||||
color: FieldText;
|
||||
}
|
||||
|
||||
.zeile { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
||||
.zeile > input[type=text] { flex: 1; }
|
||||
|
||||
/* Kompakte Zahlenfelder mit vorangestellter Beschriftung. */
|
||||
.masse { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
|
||||
.mass { display: flex; gap: 5px; align-items: center; }
|
||||
.mass label { margin: 0; white-space: nowrap; }
|
||||
.mass input { width: 68px; }
|
||||
.zeile > input[type=text] { flex: 1; }
|
||||
|
||||
.wahl { display: flex; gap: 6px; align-items: center; margin: 3px 0; }
|
||||
.wahl input { width: auto; margin: 0; }
|
||||
|
||||
/* Dateiliste mit Wahl der Ablageart je Datei. */
|
||||
.dateizeile {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 3px 0; flex-wrap: wrap;
|
||||
}
|
||||
.dateizeile .name { flex: 1; min-width: 200px; }
|
||||
.dateizeile .art { display: flex; gap: 12px; align-items: center; }
|
||||
.dateizeile .art label { margin: 0; white-space: nowrap; font-size: 12px; }
|
||||
.dateizeile .art input { width: auto; margin: 0; }
|
||||
.dateizeile .art label.aus { color: GrayText; }
|
||||
|
||||
button {
|
||||
padding: 5px 14px;
|
||||
border: 1px solid var(--rand);
|
||||
@@ -52,18 +85,28 @@ button {
|
||||
color: ButtonText;
|
||||
cursor: pointer;
|
||||
}
|
||||
button.haupt { background: var(--gruen); color: #fff; border-color: var(--gruen); }
|
||||
button.haupt {
|
||||
background: var(--gruen); color: #fff; border-color: var(--gruen);
|
||||
}
|
||||
button:disabled { opacity: .5; cursor: default; }
|
||||
.fuss { display: flex; justify-content: flex-end; gap: 8px; margin-top: auto; }
|
||||
|
||||
.fuss {
|
||||
display: flex; justify-content: flex-end; gap: 8px; margin-top: auto;
|
||||
}
|
||||
|
||||
.liste {
|
||||
border: 1px solid var(--rand);
|
||||
border-radius: 3px;
|
||||
overflow: auto;
|
||||
background: Field;
|
||||
flex: 1;
|
||||
min-height: 90px;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%; border-collapse: collapse;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
table { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
|
||||
th, td { padding: 3px 8px; text-align: left; white-space: nowrap; }
|
||||
th {
|
||||
position: sticky; top: 0;
|
||||
@@ -74,18 +117,64 @@ th {
|
||||
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); }
|
||||
.hinweis { color: GrayText; font-size: 12px; }
|
||||
.fehler { color: #b32; }
|
||||
.wachsen { flex: 1; display: flex; flex-direction: column; gap: 6px; min-height: 0; }
|
||||
.trefferHervor { background: color-mix(in srgb, Mark 60%, transparent); }
|
||||
|
||||
.wachsen {
|
||||
flex: 1; display: flex; flex-direction: column; gap: 6px; min-height: 0;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------- Zweispaltig */
|
||||
|
||||
/* KEIN flex-wrap.
|
||||
Mit Umbruch bemisst sich die Hoehe einer Flexzeile nach ihrem INHALT,
|
||||
nicht nach dem Behaelter - die Spalten wachsen dann, statt sich zu
|
||||
stauchen, und die Liste laeuft wieder aus dem Fenster.
|
||||
Der urspruengliche Dialog hatte genau deshalb keinen Umbruch. Das
|
||||
Fenster ist ohnehin 980 px breit; zwei Spalten zu 340 px passen
|
||||
immer. */
|
||||
.spalten {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* KEIN fieldset für wachsende Bereiche.
|
||||
Ein fieldset taugt nicht als Flex-Behälter: Browser behandeln seinen
|
||||
inneren anonymen Kasten gesondert, height:100% löst sich darin nicht
|
||||
verlässlich auf, und eine Liste darin wächst über den Rahmen hinaus.
|
||||
Ein gewöhnliches Element verhält sich wie erwartet.
|
||||
Der einzige sichtbare Unterschied: Die Überschrift steht innen statt
|
||||
auf dem Rahmen. */
|
||||
.kasten {
|
||||
border: 1px solid var(--rand);
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
/* Grundbreite 0 statt 340 px: Bei nowrap wuerde eine Grundbreite die
|
||||
Spalten ueber das Fenster hinausschieben. Die Aufteilung ergibt
|
||||
sich aus flex-grow zu gleichen Teilen. */
|
||||
flex: 1 1 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
.kasten > h2 {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
|
||||
/* Die Liste füllt, was nach den festen Elementen übrig bleibt, und
|
||||
blättert selbst. Genau das war das Ziel - es scheiterte bisher am
|
||||
fieldset, nicht an der Vorgehensweise. */
|
||||
.kasten .liste {
|
||||
flex: 1;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
@@ -13,13 +13,33 @@
|
||||
*/
|
||||
let dauerverbindung = null;
|
||||
|
||||
let lebenszeichen = null;
|
||||
|
||||
export function verbindungHalten() {
|
||||
if (dauerverbindung) return;
|
||||
try {
|
||||
dauerverbindung = browser.runtime.connect({ name: "dialog" });
|
||||
dauerverbindung.onDisconnect.addListener(() => {
|
||||
dauerverbindung = null;
|
||||
if (lebenszeichen) {
|
||||
clearInterval(lebenszeichen);
|
||||
lebenszeichen = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Zusätzlich regelmäßig ein Lebenszeichen senden. Die offene
|
||||
// Verbindung allein hat sich als nicht ausreichend erwiesen: Bei
|
||||
// mehreren Dateien hintereinander wurde die Ereignisseite trotzdem
|
||||
// beendet. Ein tatsächliches Ereignis alle 15 Sekunden setzt die
|
||||
// Untätigkeitsuhr zuverlässig zurück.
|
||||
lebenszeichen = setInterval(() => {
|
||||
try {
|
||||
dauerverbindung?.postMessage({ befehl: "lebenszeichen" });
|
||||
} catch {
|
||||
clearInterval(lebenszeichen);
|
||||
lebenszeichen = null;
|
||||
}
|
||||
}, 15000);
|
||||
} catch (err) {
|
||||
console.warn("[paperless] Dauerverbindung nicht möglich:", err);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
<div class="wachsen">
|
||||
<label data-i18n="colVersions">Versionen</label>
|
||||
<div class="liste" style="min-height:70px">
|
||||
<div class="liste" >
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th class="zahl" data-i18n="colNo">Nr</th>
|
||||
@@ -68,6 +68,7 @@
|
||||
</div>
|
||||
<div class="hinweis" data-i18n="insertHintLink">
|
||||
Der Link ist ohne Anmeldung abrufbar. 0 Tage bedeutet unbegrenzt.
|
||||
Leeres Tag-Feld bedeutet: keine Kennzeichnung setzen.
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
|
||||
98
ui/schritt.html
Normal file
@@ -0,0 +1,98 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title data-i18n="stepTitle">In Paperless ablegen</title>
|
||||
<link rel="stylesheet" href="gemeinsam.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1 id="kopf">In Paperless ablegen</h1>
|
||||
<div id="quelle" class="hinweis"></div>
|
||||
|
||||
<div class="spalten">
|
||||
|
||||
<section class="kasten">
|
||||
<h2 data-i18n="storeTarget">Ziel</h2>
|
||||
|
||||
<div class="wahl">
|
||||
<input type="radio" name="ziel" id="zielNeu" value="neu" checked>
|
||||
<label for="zielNeu" data-i18n="storeTargetNew">
|
||||
Neues Dokument anlegen
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="wahl">
|
||||
<input type="radio" name="ziel" id="zielVorhanden" value="vorhanden">
|
||||
<label for="zielVorhanden" data-i18n="storeTargetExisting">
|
||||
Als neue Version an ein bestehendes anhängen
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="zeile" style="margin-top:4px">
|
||||
<input type="text" id="suche" disabled>
|
||||
<button id="suchen" disabled data-i18n="btnSearch">Suchen</button>
|
||||
</div>
|
||||
|
||||
<div class="liste">
|
||||
<table>
|
||||
<thead><tr>
|
||||
<th class="zahl" data-i18n="colAsn">ASN</th>
|
||||
<th data-i18n="colCreated">Erstellt</th>
|
||||
<th data-i18n="colTitle">Titel</th>
|
||||
</tr></thead>
|
||||
<tbody id="treffer"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="hinweis" data-i18n="stepHintExisting">
|
||||
Die Klassifizierung des Ziels wird übernommen und kann angepasst
|
||||
werden.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="kasten">
|
||||
<h2 data-i18n="classifyTitle">Klassifizierung</h2>
|
||||
|
||||
<label for="titel" data-i18n="fieldTitle">Titel</label>
|
||||
<input type="text" id="titel">
|
||||
|
||||
<div class="zeile">
|
||||
<span class="mass">
|
||||
<label for="erstellt" data-i18n="fieldCreated">Erstellt</label>
|
||||
<input type="text" id="erstellt" placeholder="JJJJ-MM-TT"
|
||||
style="width:110px">
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<label for="korr" data-i18n="fieldCorrespondent">Korrespondent</label>
|
||||
<input type="text" id="korr" list="korrListe" autocomplete="off">
|
||||
<datalist id="korrListe"></datalist>
|
||||
|
||||
<label for="typ" data-i18n="fieldDocType">Dokumenttyp</label>
|
||||
<input type="text" id="typ" list="typListe" autocomplete="off">
|
||||
<datalist id="typListe"></datalist>
|
||||
|
||||
<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="tags" style="padding:4px 8px"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="meldung" class="hinweis"></div>
|
||||
|
||||
<div class="fuss">
|
||||
<button id="abbrechen" data-i18n="btnCancel">Abbrechen</button>
|
||||
<button id="ueberspringen" data-i18n="btnSkip">Überspringen</button>
|
||||
<button id="ablegen" class="haupt" data-i18n="btnStore">Ablegen</button>
|
||||
</div>
|
||||
|
||||
<script type="module" src="schritt.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
357
ui/schritt.js
Normal file
@@ -0,0 +1,357 @@
|
||||
/**
|
||||
* schritt.js - Ziel und Klassifizierung, Datei für Datei.
|
||||
*
|
||||
* Der Ablauf ist bewusst in zwei Fenster geteilt:
|
||||
*
|
||||
* 1. Auswahl der abzulegenden Dateien (store.html)
|
||||
* 2. Je Datei: Ziel und Klassifizierung (dieses Fenster)
|
||||
*
|
||||
* Der 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.
|
||||
*
|
||||
* Beim Anhängen an ein bestehendes Dokument wird dessen Klassifizierung
|
||||
* übernommen und kann angepasst werden. Sie neu einzugeben wäre Arbeit
|
||||
* für nichts, und ein Tippfehler brächte die Version vom Original weg.
|
||||
*/
|
||||
|
||||
import { kurz, groesse } from "../lib/settings.js";
|
||||
import { uebersetzeSeite, ruf, fehlerAnzeigen, meldungAnzeigen }
|
||||
from "./gemeinsam.js";
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
let auftrag = null;
|
||||
let liste = []; // die abzulegenden Dateien
|
||||
let stelle = 0;
|
||||
let stamm = { tags: [], korr: [], typen: [] };
|
||||
|
||||
let treffer = [];
|
||||
let zielDokument = null;
|
||||
let gewaehlteTags = new Set();
|
||||
|
||||
const abgelegt = []; // für die Abschlussmeldung
|
||||
|
||||
document.addEventListener("DOMContentLoaded", start);
|
||||
|
||||
async function start() {
|
||||
uebersetzeSeite();
|
||||
const nummer = Number(new URLSearchParams(location.search).get("auftrag"));
|
||||
|
||||
try {
|
||||
auftrag = await ruf("auftrag", { nummer });
|
||||
if (!auftrag) throw new Error("Der Auftrag ist nicht mehr vorhanden.");
|
||||
liste = await ruf("dateiListe", { auftrag });
|
||||
stamm = await ruf("stammdaten");
|
||||
} catch (err) {
|
||||
fehlerAnzeigen(err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!liste.length) {
|
||||
fehlerAnzeigen(browser.i18n.getMessage("errNothingSelected")
|
||||
|| "Nichts ausgewählt.");
|
||||
return;
|
||||
}
|
||||
|
||||
fuelleListe("korrListe", stamm.korr);
|
||||
fuelleListe("typListe", stamm.typen);
|
||||
verdrahten();
|
||||
await anzeigen();
|
||||
}
|
||||
|
||||
function verdrahten() {
|
||||
for (const r of document.querySelectorAll('input[name=ziel]')) {
|
||||
r.addEventListener("change", zielGewechselt);
|
||||
}
|
||||
$("suchen").addEventListener("click", suchen);
|
||||
$("suche").addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") suchen();
|
||||
});
|
||||
$("tagSuche").addEventListener("input", zeichneTags);
|
||||
$("abbrechen").addEventListener("click", () => window.close());
|
||||
$("ueberspringen").addEventListener("click", () => weiter());
|
||||
$("ablegen").addEventListener("click", ablegen);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------ Datei anzeigen
|
||||
|
||||
async function anzeigen() {
|
||||
const eintrag = liste[stelle];
|
||||
|
||||
$("kopf").textContent = browser.i18n.getMessage("stepTitle")
|
||||
|| "In Paperless ablegen";
|
||||
|
||||
const zaehler = liste.length > 1
|
||||
? `${browser.i18n.getMessage("classifyProgress",
|
||||
[String(stelle + 1), String(liste.length)])
|
||||
|| `${stelle + 1} von ${liste.length}`} · `
|
||||
: "";
|
||||
const name = eintrag.art === "nachricht"
|
||||
? (browser.i18n.getMessage("stepWholeMessage") || "Die ganze Nachricht")
|
||||
: `${eintrag.name}${eintrag.groesse ? ` (${groesse(eintrag.groesse)})` : ""}`;
|
||||
$("quelle").textContent = `${zaehler}${name}`;
|
||||
|
||||
// Zurücksetzen für die neue Datei
|
||||
zielDokument = null;
|
||||
treffer = [];
|
||||
$("treffer").replaceChildren();
|
||||
$("suche").value = "";
|
||||
$("tagSuche").value = "";
|
||||
$("zielNeu").checked = true;
|
||||
gewaehlteTags = new Set();
|
||||
|
||||
$("titel").value = vorschlagTitel(eintrag);
|
||||
$("erstellt").value = auftrag.datum || "";
|
||||
$("korr").value = "";
|
||||
$("typ").value = "";
|
||||
|
||||
zielGewechselt();
|
||||
zeichneTags();
|
||||
meldungAnzeigen("");
|
||||
$("ablegen").disabled = false;
|
||||
$("ueberspringen").hidden = liste.length < 2;
|
||||
}
|
||||
|
||||
function vorschlagTitel(eintrag) {
|
||||
if (eintrag.art === "nachricht") return auftrag.titelVorschlag || "";
|
||||
// Endung weg: Sie steht schon im Dateinamen des Originals und trägt
|
||||
// im Titel nichts bei.
|
||||
return String(eintrag.name || "").replace(/\.[^.]{1,8}$/, "");
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------- Zielwahl
|
||||
|
||||
function zielGewechselt() {
|
||||
const vorhanden = $("zielVorhanden").checked;
|
||||
$("suche").disabled = !vorhanden;
|
||||
$("suchen").disabled = !vorhanden;
|
||||
|
||||
// Die Trefferliste abblenden statt ausblenden: Ein Feld, das
|
||||
// verschwindet und wieder auftaucht, lässt die Spalte springen.
|
||||
const liste = $("treffer").closest(".liste");
|
||||
if (liste) liste.style.opacity = vorhanden ? "1" : ".45";
|
||||
|
||||
if (vorhanden && !treffer.length) suchen();
|
||||
pruefen();
|
||||
}
|
||||
|
||||
async function suchen() {
|
||||
$("treffer").replaceChildren();
|
||||
zielDokument = null;
|
||||
try {
|
||||
treffer = await ruf("suchen", { query: $("suche").value.trim() });
|
||||
} catch (err) {
|
||||
fehlerAnzeigen(err.message);
|
||||
return;
|
||||
}
|
||||
for (const [i, r] of treffer.entries()) {
|
||||
const tr = document.createElement("tr");
|
||||
const a = document.createElement("td");
|
||||
a.className = "zahl";
|
||||
a.textContent = r.archive_serial_number ?? "—";
|
||||
const b = document.createElement("td");
|
||||
b.textContent = (r.created || "").slice(0, 10);
|
||||
const c = document.createElement("td");
|
||||
c.textContent = r.title || "";
|
||||
c.title = r.title || "";
|
||||
tr.append(a, b, c);
|
||||
tr.addEventListener("click", () => waehleZiel(i));
|
||||
$("treffer").append(tr);
|
||||
}
|
||||
if (!treffer.length) {
|
||||
meldungAnzeigen(browser.i18n.getMessage("msgNoHits") || "Keine Treffer.");
|
||||
}
|
||||
pruefen();
|
||||
}
|
||||
|
||||
/**
|
||||
* Zieldokument wählen und dessen Klassifizierung übernehmen.
|
||||
*
|
||||
* Eine neue Version gehört inhaltlich zum selben Vorgang wie das
|
||||
* Original. Die Angaben neu einzutippen wäre Arbeit für nichts - und ein
|
||||
* abweichender Korrespondent brächte die Version vom Original weg.
|
||||
*/
|
||||
async function waehleZiel(i) {
|
||||
zielDokument = treffer[i] || null;
|
||||
for (const [j, tr] of [...$("treffer").children].entries()) {
|
||||
tr.classList.toggle("gewaehlt", j === i);
|
||||
}
|
||||
pruefen();
|
||||
if (!zielDokument) return;
|
||||
|
||||
meldungAnzeigen(browser.i18n.getMessage("msgLoadingTarget")
|
||||
|| "Klassifizierung des Ziels wird geladen …");
|
||||
try {
|
||||
const doc = await ruf("dokument", { id: zielDokument.id });
|
||||
$("titel").value = doc.title || "";
|
||||
$("erstellt").value = (doc.created || "").slice(0, 10);
|
||||
$("korr").value = nameVon(stamm.korr, doc.correspondent);
|
||||
$("typ").value = nameVon(stamm.typen, doc.document_type);
|
||||
gewaehlteTags = new Set((doc.tags || []).map(Number));
|
||||
zeichneTags();
|
||||
meldungAnzeigen("");
|
||||
} catch (err) {
|
||||
fehlerAnzeigen(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function pruefen() {
|
||||
const ok = $("zielNeu").checked || Boolean(zielDokument);
|
||||
$("ablegen").disabled = !ok;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- Tags
|
||||
|
||||
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(rows, id) {
|
||||
return rows.find((x) => x.id === id)?.name || "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Tagliste zeichnen: Gewählte oben, dann die gefilterten übrigen.
|
||||
*
|
||||
* Gewählte bleiben sichtbar, auch wenn der Filter sie ausschliessen
|
||||
* würde - sonst verliert man beim Suchen den Überblick, was schon
|
||||
* angehakt ist.
|
||||
*/
|
||||
function zeichneTags() {
|
||||
const ziel = $("tags");
|
||||
ziel.replaceChildren();
|
||||
const filter = ($("tagSuche").value || "").trim().toLowerCase();
|
||||
|
||||
const oben = stamm.tags.filter((t) => gewaehlteTags.has(Number(t.id)));
|
||||
const unten = stamm.tags.filter(
|
||||
(t) => !gewaehlteTags.has(Number(t.id))
|
||||
&& (!filter || t.name.toLowerCase().includes(filter)));
|
||||
|
||||
for (const t of oben) ziel.append(tagZeile(t, true, ""));
|
||||
|
||||
if (oben.length && unten.length) {
|
||||
const tr = document.createElement("hr");
|
||||
tr.style.cssText = "border:0;border-top:1px solid var(--rand);margin:5px 0";
|
||||
ziel.append(tr);
|
||||
}
|
||||
|
||||
for (const t of unten) ziel.append(tagZeile(t, false, filter));
|
||||
|
||||
if (!oben.length && !unten.length) {
|
||||
const h = document.createElement("div");
|
||||
h.className = "hinweis";
|
||||
h.textContent = browser.i18n.getMessage("tagsNoMatch")
|
||||
|| "Keine Übereinstimmung.";
|
||||
ziel.append(h);
|
||||
}
|
||||
}
|
||||
|
||||
function tagZeile(tag, gewaehlt, filter) {
|
||||
const zeile = document.createElement("div");
|
||||
zeile.className = "wahl";
|
||||
const box = document.createElement("input");
|
||||
box.type = "checkbox";
|
||||
box.id = `tag_${tag.id}`;
|
||||
box.checked = gewaehlt;
|
||||
box.addEventListener("change", () => {
|
||||
if (box.checked) gewaehlteTags.add(Number(tag.id));
|
||||
else gewaehlteTags.delete(Number(tag.id));
|
||||
zeichneTags();
|
||||
});
|
||||
const bez = document.createElement("label");
|
||||
bez.htmlFor = box.id;
|
||||
if (gewaehlt) bez.style.fontWeight = "600";
|
||||
beschriften(bez, 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 -
|
||||
* wer ein Dokument per Mail einliefern kann, bestimmt Tagnamen mit.
|
||||
*/
|
||||
function beschriften(el, name, filter) {
|
||||
el.replaceChildren();
|
||||
const pos = filter ? name.toLowerCase().indexOf(filter) : -1;
|
||||
if (pos < 0) {
|
||||
el.textContent = name;
|
||||
return;
|
||||
}
|
||||
el.append(document.createTextNode(name.slice(0, pos)));
|
||||
const mark = document.createElement("span");
|
||||
mark.className = "trefferHervor";
|
||||
mark.textContent = name.slice(pos, pos + filter.length);
|
||||
el.append(mark, document.createTextNode(name.slice(pos + filter.length)));
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------- Ablauf
|
||||
|
||||
async function ablegen() {
|
||||
$("ablegen").disabled = true;
|
||||
$("ueberspringen").disabled = true;
|
||||
meldungAnzeigen(browser.i18n.getMessage("msgUploading")
|
||||
|| "Wird abgelegt …");
|
||||
|
||||
const eintrag = liste[stelle];
|
||||
const anVorhandenes = $("zielVorhanden").checked;
|
||||
|
||||
try {
|
||||
const ergebnis = await ruf("ablegenEine", {
|
||||
auftrag,
|
||||
eintrag,
|
||||
ziel: anVorhandenes ? "vorhanden" : "neu",
|
||||
zielId: zielDokument?.id,
|
||||
titel: $("titel").value.trim(),
|
||||
erstellt: $("erstellt").value.trim()
|
||||
});
|
||||
|
||||
// Klassifizierung anwenden. Beim Anhängen gilt sie für die Wurzel,
|
||||
// nicht für die neue Version.
|
||||
const zielId = ergebnis.art === "version" ? ergebnis.zielId : ergebnis.id;
|
||||
if (zielId) {
|
||||
meldungAnzeigen(browser.i18n.getMessage("msgSaving")
|
||||
|| "Klassifizierung wird gespeichert …");
|
||||
await ruf("klassifizieren", {
|
||||
id: zielId,
|
||||
payload: {
|
||||
title: $("titel").value.trim(),
|
||||
tags: [...gewaehlteTags],
|
||||
...($("erstellt").value.trim()
|
||||
? { created: $("erstellt").value.trim() } : {})
|
||||
},
|
||||
korrName: $("korr").value.trim(),
|
||||
typName: $("typ").value.trim()
|
||||
});
|
||||
}
|
||||
|
||||
abgelegt.push(ergebnis);
|
||||
await weiter();
|
||||
} catch (err) {
|
||||
console.error("[paperless] Ablegen gescheitert", err);
|
||||
fehlerAnzeigen(err.message);
|
||||
$("ablegen").disabled = false;
|
||||
$("ueberspringen").disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function weiter() {
|
||||
$("ueberspringen").disabled = false;
|
||||
stelle += 1;
|
||||
if (stelle >= liste.length) {
|
||||
meldungAnzeigen(browser.i18n.getMessage("msgStored",
|
||||
[String(abgelegt.length)]) || `${abgelegt.length} abgelegt.`);
|
||||
setTimeout(() => window.close(), 1200);
|
||||
return;
|
||||
}
|
||||
await anzeigen();
|
||||
}
|
||||
@@ -13,77 +13,17 @@
|
||||
<fieldset>
|
||||
<legend data-i18n="storeWhat">Was soll abgelegt werden?</legend>
|
||||
<div id="dateien"></div>
|
||||
<div class="wahl" id="pdfWahl" hidden>
|
||||
<input type="checkbox" id="alsPdf" checked>
|
||||
<label for="alsPdf" data-i18n="storeAsPdf">
|
||||
Als PDF aufbereiten, Original und Anhänge einbetten
|
||||
</label>
|
||||
</div>
|
||||
<div class="hinweis" data-i18n="storeHintPreselect">
|
||||
Kleine Bilder und Signaturen sind vorab abgewählt — sie sind meist
|
||||
Teil der Nachricht, keine eigenen Dokumente.
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="wachsen">
|
||||
<legend data-i18n="storeTarget">Ziel</legend>
|
||||
|
||||
<div class="wahl">
|
||||
<input type="radio" name="ziel" id="zielNeu" value="neu" checked>
|
||||
<label for="zielNeu" data-i18n="storeTargetNew">
|
||||
Neues Dokument anlegen
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="bereichNeu">
|
||||
<label for="titel" data-i18n="fieldTitle">Titel</label>
|
||||
<input type="text" id="titel">
|
||||
<div class="hinweis" data-i18n="storeHintTitleMulti">
|
||||
Bei mehreren Dateien wird der Titel um den Dateinamen ergänzt.
|
||||
</div>
|
||||
<label for="erstellt" data-i18n="fieldCreated">Erstellt</label>
|
||||
<input type="text" id="erstellt" placeholder="JJJJ-MM-TT">
|
||||
</div>
|
||||
|
||||
<div class="wahl">
|
||||
<input type="radio" name="ziel" id="zielVorhanden" value="vorhanden">
|
||||
<label for="zielVorhanden" data-i18n="storeTargetExisting">
|
||||
An bestehendes Dokument anhängen
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="bereichVorhanden" class="wachsen">
|
||||
<div class="zeile">
|
||||
<input type="text" id="suche" disabled>
|
||||
<button id="suchen" disabled data-i18n="btnSearch">Suchen</button>
|
||||
</div>
|
||||
<div class="liste">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="zahl" data-i18n="colAsn">ASN</th>
|
||||
<th data-i18n="colCreated">Erstellt</th>
|
||||
<th data-i18n="colTitle">Titel</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="treffer"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div class="wahl">
|
||||
<input type="checkbox" id="klassifizieren" checked>
|
||||
<label for="klassifizieren" data-i18n="storeClassifyAfter">
|
||||
Nach dem Ablegen die Klassifizierung bearbeiten
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="meldung" class="hinweis"></div>
|
||||
|
||||
<div class="fuss">
|
||||
<button id="abbrechen" data-i18n="btnCancel">Abbrechen</button>
|
||||
<button id="ablegen" class="haupt" data-i18n="btnStore">Ablegen</button>
|
||||
<button id="ablegen" class="haupt" data-i18n="btnNext">Weiter</button>
|
||||
</div>
|
||||
|
||||
<script type="module" src="store.js"></script>
|
||||
|
||||
260
ui/store.js
@@ -7,14 +7,14 @@
|
||||
*/
|
||||
|
||||
import { groesse, kurz, titelAusBetreff } from "../lib/settings.js";
|
||||
import { uebersetzeSeite, ruf, fehlerAnzeigen } from "./gemeinsam.js";
|
||||
import { umwandelbar } from "../lib/mailpdf.js";
|
||||
import { uebersetzeSeite, ruf, fehlerAnzeigen, meldungAnzeigen }
|
||||
from "./gemeinsam.js";
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
let auftrag = null;
|
||||
let einstellungen = null;
|
||||
let treffer = [];
|
||||
let gewaehlterTreffer = null;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", start);
|
||||
|
||||
@@ -31,7 +31,6 @@ async function start() {
|
||||
|
||||
quelleAnzeigen();
|
||||
dateienAnzeigen();
|
||||
vorbelegen();
|
||||
verdrahten();
|
||||
}
|
||||
|
||||
@@ -50,43 +49,35 @@ function quelleAnzeigen() {
|
||||
* Darstellung der Nachricht, nicht zum Inhalt - wer sie alle mit
|
||||
* archiviert, hat nach einem Jahr hunderte Logos im Bestand.
|
||||
*/
|
||||
/**
|
||||
* Dateien zur Auswahl anbieten, je Zeile mit der Wahl der Ablageart.
|
||||
*
|
||||
* Vorbelegt wird unterschiedlich, weil die Absicht unterschiedlich ist:
|
||||
*
|
||||
* Eine NACHRICHT wird als PDF aufbereitet - eine .eml sieht sich im
|
||||
* Archiv niemand an.
|
||||
*
|
||||
* Ein ANHANG wird im Original abgelegt. Eine Rechnung als PDF ist
|
||||
* bereits das Dokument; sie durch eine Textfassung zu ersetzen wäre
|
||||
* Unfug. Nur bei Textdateien - etwa einem HTML-Anhang - ist die
|
||||
* Aufbereitung sinnvoll, und dort steht sie zur Wahl.
|
||||
*/
|
||||
function dateienAnzeigen() {
|
||||
const ziel = $("dateien");
|
||||
ziel.replaceChildren();
|
||||
|
||||
if (auftrag.quelle === "nachricht") {
|
||||
// Die Wahl zwischen PDF und Rohformat gibt es nur hier. Bei einem
|
||||
// einzelnen Anhang waere sie sinnlos - dort wird die Datei so
|
||||
// abgelegt, wie sie ist.
|
||||
const wahl = $("pdfWahl");
|
||||
if (wahl && wahl.hidden) {
|
||||
wahl.hidden = false;
|
||||
$("alsPdf").checked = (einstellungen.mailFormat || "pdf") === "pdf";
|
||||
$("alsPdf").addEventListener("change", dateienAnzeigen);
|
||||
}
|
||||
|
||||
// Aufbau über createElement statt innerHTML. Hier stünde zwar nur
|
||||
// festes Markup, aber die Zusicherung "kein innerHTML" soll ohne
|
||||
// Ausnahme gelten - sonst muss man bei jeder Änderung neu prüfen, ob
|
||||
// sich inzwischen ein Fremdwert eingeschlichen hat.
|
||||
const zeile = document.createElement("div");
|
||||
zeile.className = "wahl";
|
||||
const box = document.createElement("input");
|
||||
box.type = "checkbox";
|
||||
box.id = "ganz";
|
||||
box.checked = true;
|
||||
box.disabled = true;
|
||||
const bez = document.createElement("label");
|
||||
bez.htmlFor = "ganz";
|
||||
const alsPdf = $("alsPdf") ? $("alsPdf").checked
|
||||
: (einstellungen.mailFormat || "pdf") === "pdf";
|
||||
bez.textContent = alsPdf
|
||||
? (browser.i18n.getMessage("storeWholePdf")
|
||||
|| "Die Nachricht als PDF, mit eingebettetem Original")
|
||||
: (browser.i18n.getMessage("storeWholeMessage")
|
||||
|| "Die vollständige Nachricht als .eml");
|
||||
zeile.append(box, bez);
|
||||
ziel.append(zeile);
|
||||
const name = `${titelAusBetreff(auftrag.betreff)}.eml`;
|
||||
ziel.append(dateiZeile({
|
||||
schluessel: "ganz",
|
||||
beschriftung: name,
|
||||
gewaehlt: true,
|
||||
festGewaehlt: true,
|
||||
pdfMoeglich: true,
|
||||
alsPdf: (einstellungen.mailFormat || "pdf") === "pdf",
|
||||
hinweisPdf: browser.i18n.getMessage("artMessagePdf")
|
||||
|| "als PDF aufbereiten"
|
||||
}));
|
||||
|
||||
if (auftrag.dateien?.length) {
|
||||
const h = document.createElement("div");
|
||||
@@ -96,6 +87,7 @@ function dateienAnzeigen() {
|
||||
|| `Die ${auftrag.dateien.length} Anhänge bleiben darin enthalten.`;
|
||||
ziel.append(h);
|
||||
}
|
||||
knopfPruefen();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -103,128 +95,118 @@ function dateienAnzeigen() {
|
||||
const uninteressant =
|
||||
einstellungen.ignorierteTypen.includes(d.contentType)
|
||||
|| (d.size || 0) < einstellungen.ignorierteKleiner;
|
||||
d.ausgewaehlt = !uninteressant;
|
||||
if (d.ausgewaehlt === undefined) d.ausgewaehlt = !uninteressant;
|
||||
if (d.alsPdf === undefined) d.alsPdf = false;
|
||||
|
||||
ziel.append(dateiZeile({
|
||||
schluessel: `d${i}`,
|
||||
beschriftung: `${d.name} (${groesse(d.size)}, ${d.contentType})`,
|
||||
gewaehlt: d.ausgewaehlt,
|
||||
pdfMoeglich: umwandelbar(d.contentType, d.name),
|
||||
alsPdf: d.alsPdf,
|
||||
beiWahl: (an) => { d.ausgewaehlt = an; },
|
||||
beiArt: (pdf) => { d.alsPdf = pdf; }
|
||||
}));
|
||||
}
|
||||
knopfPruefen();
|
||||
}
|
||||
|
||||
/**
|
||||
* Eine Zeile der Dateiliste: Häkchen, Name, und rechts die Wahl zwischen
|
||||
* Original und PDF.
|
||||
*/
|
||||
function dateiZeile(o) {
|
||||
const zeile = document.createElement("div");
|
||||
zeile.className = "wahl";
|
||||
zeile.className = "dateizeile";
|
||||
|
||||
const box = document.createElement("input");
|
||||
box.type = "checkbox";
|
||||
box.id = `d${i}`;
|
||||
box.checked = d.ausgewaehlt;
|
||||
box.id = o.schluessel;
|
||||
box.checked = o.gewaehlt;
|
||||
if (o.festGewaehlt) box.disabled = true;
|
||||
box.addEventListener("change", () => {
|
||||
d.ausgewaehlt = box.checked;
|
||||
o.beiWahl?.(box.checked);
|
||||
knopfPruefen();
|
||||
});
|
||||
const bez = document.createElement("label");
|
||||
bez.htmlFor = box.id;
|
||||
bez.textContent = `${d.name} (${groesse(d.size)}, ${d.contentType})`;
|
||||
zeile.append(box, bez);
|
||||
ziel.append(zeile);
|
||||
}
|
||||
}
|
||||
|
||||
function vorbelegen() {
|
||||
$("titel").value = titelAusBetreff(auftrag.betreff);
|
||||
if (auftrag.datum) $("erstellt").value = auftrag.datum;
|
||||
}
|
||||
const name = document.createElement("label");
|
||||
name.className = "name";
|
||||
name.htmlFor = box.id;
|
||||
name.textContent = o.beschriftung;
|
||||
|
||||
function verdrahten() {
|
||||
for (const r of document.querySelectorAll('input[name=ziel]')) {
|
||||
r.addEventListener("change", zielGewechselt);
|
||||
}
|
||||
$("suchen").addEventListener("click", suchen);
|
||||
$("suche").addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") suchen();
|
||||
zeile.append(box, name);
|
||||
|
||||
const art = document.createElement("div");
|
||||
art.className = "art";
|
||||
const gruppe = `art_${o.schluessel}`;
|
||||
|
||||
const machen = (wert, text, aktiv) => {
|
||||
const r = document.createElement("input");
|
||||
r.type = "radio";
|
||||
r.name = gruppe;
|
||||
r.id = `${gruppe}_${wert}`;
|
||||
r.value = wert;
|
||||
r.checked = aktiv;
|
||||
if (!o.pdfMoeglich && wert === "pdf") r.disabled = true;
|
||||
r.addEventListener("change", () => {
|
||||
if (r.checked) o.beiArt?.(wert === "pdf");
|
||||
});
|
||||
$("abbrechen").addEventListener("click", () => window.close());
|
||||
$("ablegen").addEventListener("click", ablegen);
|
||||
zielGewechselt();
|
||||
const l = document.createElement("label");
|
||||
l.htmlFor = r.id;
|
||||
l.textContent = text;
|
||||
if (r.disabled) l.className = "aus";
|
||||
art.append(r, l);
|
||||
};
|
||||
|
||||
machen("original", browser.i18n.getMessage("artOriginal")
|
||||
|| "Original ablegen", !o.alsPdf);
|
||||
machen("pdf", o.hinweisPdf || browser.i18n.getMessage("artPdf")
|
||||
|| "PDF aufbereiten", Boolean(o.alsPdf) && o.pdfMoeglich);
|
||||
|
||||
if (!o.pdfMoeglich) {
|
||||
art.title = browser.i18n.getMessage("artPdfUnavailable")
|
||||
|| "Für dieses Format nicht möglich - nur Textdateien lassen sich "
|
||||
+ "aufbereiten.";
|
||||
}
|
||||
|
||||
function zielGewechselt() {
|
||||
const neu = $("zielNeu").checked;
|
||||
for (const id of ["titel", "erstellt"]) $(id).disabled = !neu;
|
||||
for (const id of ["suche", "suchen"]) $(id).disabled = neu;
|
||||
if (!neu && !treffer.length) suchen();
|
||||
knopfPruefen();
|
||||
zeile.append(art);
|
||||
return zeile;
|
||||
}
|
||||
|
||||
async function suchen() {
|
||||
const liste = $("treffer");
|
||||
liste.replaceChildren();
|
||||
try {
|
||||
treffer = await ruf("suchen", { query: $("suche").value.trim() });
|
||||
} catch (err) {
|
||||
fehlerAnzeigen(err.message);
|
||||
return;
|
||||
}
|
||||
for (const [i, r] of treffer.entries()) {
|
||||
const tr = document.createElement("tr");
|
||||
const asn = document.createElement("td");
|
||||
asn.className = "zahl";
|
||||
asn.textContent = r.archive_serial_number ?? "—";
|
||||
const erstellt = document.createElement("td");
|
||||
erstellt.textContent = (r.created || "").slice(0, 10);
|
||||
const titel = document.createElement("td");
|
||||
titel.textContent = r.title || "";
|
||||
titel.title = r.title || "";
|
||||
tr.append(asn, erstellt, titel);
|
||||
tr.addEventListener("click", () => waehle(i));
|
||||
liste.append(tr);
|
||||
}
|
||||
if (treffer.length) waehle(0);
|
||||
else fehlerAnzeigen(browser.i18n.getMessage("msgNoHits") || "Keine Treffer.");
|
||||
}
|
||||
|
||||
function waehle(i) {
|
||||
gewaehlterTreffer = treffer[i] || null;
|
||||
for (const [j, tr] of [...$("treffer").children].entries()) {
|
||||
tr.classList.toggle("gewaehlt", j === i);
|
||||
}
|
||||
knopfPruefen();
|
||||
}
|
||||
|
||||
function knopfPruefen() {
|
||||
const etwasGewaehlt = auftrag.quelle === "nachricht"
|
||||
|| (auftrag.dateien || []).some((d) => d.ausgewaehlt);
|
||||
const zielOk = $("zielNeu").checked || Boolean(gewaehlterTreffer);
|
||||
$("ablegen").disabled = !(etwasGewaehlt && zielOk);
|
||||
}
|
||||
|
||||
async function ablegen() {
|
||||
/**
|
||||
* Weiter zum Schrittdialog.
|
||||
*
|
||||
* Die Auswahl wird über einen neuen Auftrag übergeben - das Fenster
|
||||
* schliesst sich, und der Schrittdialog geht die Dateien einzeln durch.
|
||||
*/
|
||||
async function weiter() {
|
||||
$("ablegen").disabled = true;
|
||||
$("meldung").className = "hinweis";
|
||||
$("meldung").textContent =
|
||||
browser.i18n.getMessage("msgUploading") || "Wird abgelegt …";
|
||||
|
||||
meldungAnzeigen(browser.i18n.getMessage("msgOpeningStep")
|
||||
|| "Wird vorbereitet …");
|
||||
try {
|
||||
const ergebnisse = await ruf("ablegen", {
|
||||
auftrag: { ...auftrag,
|
||||
// Nur mitgeben, wenn die Wahl überhaupt angeboten wurde.
|
||||
alsPdf: $("pdfWahl") && !$("pdfWahl").hidden
|
||||
? $("alsPdf").checked : undefined },
|
||||
ziel: $("zielNeu").checked ? "neu" : "vorhanden",
|
||||
zielId: gewaehlterTreffer?.id,
|
||||
titel: $("titel").value.trim(),
|
||||
erstellt: $("erstellt").value.trim()
|
||||
});
|
||||
|
||||
if ($("klassifizieren").checked && ergebnisse.length) {
|
||||
// Beim Anhängen an ein vorhandenes Dokument gilt dessen
|
||||
// Klassifizierung - dann ist die Wurzel gemeint, nicht die neue
|
||||
// Version.
|
||||
const erstes = ergebnisse[0];
|
||||
const id = erstes.art === "version" ? erstes.zielId : erstes.id;
|
||||
location.href = `classify.html?id=${id}&weiter=${ergebnisse.length}`;
|
||||
return;
|
||||
await ruf("schrittOeffnen", {
|
||||
auftrag: {
|
||||
...auftrag,
|
||||
titelVorschlag: titelAusBetreff(auftrag.betreff),
|
||||
alsPdf: auftrag.quelle === "nachricht"
|
||||
? $("art_ganz_pdf")?.checked ?? true
|
||||
: undefined
|
||||
}
|
||||
|
||||
$("meldung").textContent =
|
||||
browser.i18n.getMessage("msgStored", [String(ergebnisse.length)])
|
||||
|| `${ergebnisse.length} abgelegt.`;
|
||||
setTimeout(() => window.close(), 1200);
|
||||
});
|
||||
window.close();
|
||||
} catch (err) {
|
||||
fehlerAnzeigen(err.message);
|
||||
$("ablegen").disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function verdrahten() {
|
||||
$("abbrechen").addEventListener("click", () => window.close());
|
||||
$("ablegen").addEventListener("click", weiter);
|
||||
}
|
||||
|
||||
function knopfPruefen() {
|
||||
const etwasGewaehlt = auftrag.quelle === "nachricht"
|
||||
|| (auftrag.dateien || []).some((d) => d.ausgewaehlt);
|
||||
$("ablegen").disabled = !etwasGewaehlt;
|
||||
}
|
||||
|
||||