Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e9d62cf95 | |||
| 8380dc1584 |
95
README.md
@@ -15,6 +15,7 @@ WebExtensions, LibreOffice UNO.
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Kontextmenü am Anhang | Anhang in Paperless ablegen | ausgewählte Anhänge als Dokument |
|
| 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` |
|
| 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 Nachrichtenfenster | dasselbe für die angezeigte Nachricht | |
|
||||||
| Schaltfläche im Verfassenfenster | Aus Paperless einfügen | als Anhang oder als Freigabelink |
|
| 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.
|
Darstellung nicht originalgetreu ist.
|
||||||
|
|
||||||
Wer lieber die Rohfassung will, stellt das in den Einstellungen um oder
|
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 —
|
**Titel aus dem Betreff.** Präfixe wie `AW:` oder `Fwd:` werden entfernt —
|
||||||
sie tragen im Archiv nichts bei und stören beim Sortieren.
|
sie tragen im Archiv nichts bei und stören beim Sortieren.
|
||||||
|
|
||||||
**Klassifizierung direkt danach.** Sonst trägt ein neues Dokument nur den
|
**Zwei Schritte, dann je Datei einer.**
|
||||||
Titel, und nachträglich macht man es erfahrungsgemäß nicht mehr.
|
|
||||||
|
```
|
||||||
|
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
|
### Warum kein originalgetreues Abbild
|
||||||
|
|
||||||
@@ -173,7 +206,7 @@ Dauerhaft:
|
|||||||
|
|
||||||
```
|
```
|
||||||
Add-ons-Verwaltung → Zahnrad → Add-on aus Datei installieren
|
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:
|
werden — der heikelste Punkt entfällt. Die wichtigsten Maßnahmen:
|
||||||
|
|
||||||
- Zugriffsrecht nur für die eingetragene Adresse, zur Laufzeit erbeten
|
- 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
|
im ersten Entwurf enthalten, ohne je benutzt zu werden, und ist entfernt
|
||||||
- Nur `http` und `https` als Basisadresse
|
- Nur `http` und `https` als Basisadresse
|
||||||
- Dateinamen aus Anhängen werden bereinigt — sie stammen vom Absender
|
- Dateinamen aus Anhängen werden bereinigt — sie stammen vom Absender
|
||||||
@@ -248,9 +281,11 @@ background.js Menüs, Schaltflächen, Vermittlung
|
|||||||
lib/
|
lib/
|
||||||
client.js REST-Client (fetch, FormData)
|
client.js REST-Client (fetch, FormData)
|
||||||
settings.js Einstellungen, Dateinamen, Hilfsmittel
|
settings.js Einstellungen, Dateinamen, Hilfsmittel
|
||||||
|
pdf.js PDF-Erzeuger ohne Fremdbibliothek
|
||||||
|
mailpdf.js Aufbereitung einer Nachricht
|
||||||
ui/
|
ui/
|
||||||
store.html/.js Ablegen: Anhänge oder ganze Nachricht
|
store.html/.js Schritt 1: welche Dateien
|
||||||
classify.html/.js Klassifizierung direkt danach
|
schritt.html/.js Schritt 2: Ziel und Klassifizierung je Datei
|
||||||
insert.html/.js Einfügen als Anhang oder Link
|
insert.html/.js Einfügen als Anhang oder Link
|
||||||
options.html/.js Verbindung einrichten
|
options.html/.js Verbindung einrichten
|
||||||
gemeinsam.js/.css geteilte Hilfsmittel
|
gemeinsam.js/.css geteilte Hilfsmittel
|
||||||
@@ -259,11 +294,28 @@ icons/
|
|||||||
```
|
```
|
||||||
|
|
||||||
Die Dialoge sprechen **nicht** selbst mit Paperless, sondern lassen die
|
Die Dialoge sprechen **nicht** selbst mit Paperless, sondern lassen die
|
||||||
Ereignisseite arbeiten. Dort hängt die zur Laufzeit erteilte Berechtigung;
|
Ereignisseite arbeiten. So liegen Token und Verbindungslogik an einer
|
||||||
ein Dialogfenster müsste sie sonst erneut erbitten.
|
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
|
## Vor einer Veröffentlichung
|
||||||
|
|
||||||
| Datei | Was |
|
| 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
|
Die Kennung sollte einer Domain folgen, die dir gehört. Aktuell steht dort
|
||||||
`paperless@example.org`.
|
`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
|
## Lizenz
|
||||||
@@ -288,6 +362,3 @@ schließt jede Gewährleistung aus.
|
|||||||
|
|
||||||
Vor dem ersten produktiven Einsatz: eine Testnachricht ablegen und in
|
Vor dem ersten produktiven Einsatz: eine Testnachricht ablegen und in
|
||||||
Paperless nachsehen, ob Titel, Datum und Anhänge stimmen.
|
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": {
|
"storeHintPreselect": {
|
||||||
"message": "Kleine Bilder und Signaturen sind vorab abgewählt — sie sind meist Teil der Nachricht, keine eigenen Dokumente."
|
"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": {
|
"storeAttachmentsInside": {
|
||||||
"message": "Die $1 Anhänge bleiben darin enthalten.",
|
"message": "Die $1 Anhänge bleiben darin enthalten.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -46,26 +43,9 @@
|
|||||||
"storeTargetExisting": {
|
"storeTargetExisting": {
|
||||||
"message": "Als neue Version an ein bestehendes anhängen"
|
"message": "Als neue Version an ein bestehendes anhängen"
|
||||||
},
|
},
|
||||||
"storeHintTitleMulti": {
|
|
||||||
"message": "Bei mehreren Dateien wird der Titel um den Dateinamen ergänzt."
|
|
||||||
},
|
|
||||||
"storeClassifyAfter": {
|
|
||||||
"message": "Nach dem Ablegen die Klassifizierung bearbeiten"
|
|
||||||
},
|
|
||||||
"classifyTitle": {
|
"classifyTitle": {
|
||||||
"message": "Klassifizierung bearbeiten"
|
"message": "Klassifizierung bearbeiten"
|
||||||
},
|
},
|
||||||
"classifyHintNew": {
|
|
||||||
"message": "Unbekannte Korrespondenten und Dokumenttypen werden angelegt."
|
|
||||||
},
|
|
||||||
"classifyMoreStored": {
|
|
||||||
"message": "$1 Dateien abgelegt",
|
|
||||||
"placeholders": {
|
|
||||||
"1": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"insertTitle": {
|
"insertTitle": {
|
||||||
"message": "Aus Paperless einfügen"
|
"message": "Aus Paperless einfügen"
|
||||||
},
|
},
|
||||||
@@ -235,9 +215,6 @@
|
|||||||
"errNoPermission": {
|
"errNoPermission": {
|
||||||
"message": "Ohne Zugriffsrecht auf die Adresse kann die Erweiterung nicht arbeiten."
|
"message": "Ohne Zugriffsrecht auf die Adresse kann die Erweiterung nicht arbeiten."
|
||||||
},
|
},
|
||||||
"linkLabel": {
|
|
||||||
"message": "Datei"
|
|
||||||
},
|
|
||||||
"linkValidUntil": {
|
"linkValidUntil": {
|
||||||
"message": "Link gültig bis $1",
|
"message": "Link gültig bis $1",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -279,12 +256,6 @@
|
|||||||
"optionsHintMailFormat": {
|
"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."
|
"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": {
|
"fieldPdfFontSize": {
|
||||||
"message": "Schriftgröße"
|
"message": "Schriftgröße"
|
||||||
},
|
},
|
||||||
@@ -321,9 +292,6 @@
|
|||||||
"artPdfUnavailable": {
|
"artPdfUnavailable": {
|
||||||
"message": "Für dieses Format nicht möglich - nur Textdateien lassen sich aufbereiten."
|
"message": "Für dieses Format nicht möglich - nur Textdateien lassen sich aufbereiten."
|
||||||
},
|
},
|
||||||
"btnSkipAll": {
|
|
||||||
"message": "Alle überspringen"
|
|
||||||
},
|
|
||||||
"classifyProgress": {
|
"classifyProgress": {
|
||||||
"message": "$1 von $2",
|
"message": "$1 von $2",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -335,17 +303,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"msgUploadingOf": {
|
|
||||||
"message": "Wird abgelegt … $1 von $2",
|
|
||||||
"placeholders": {
|
|
||||||
"1": {
|
|
||||||
"content": "$1"
|
|
||||||
},
|
|
||||||
"2": {
|
|
||||||
"content": "$2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"stepTitle": {
|
"stepTitle": {
|
||||||
"message": "In Paperless ablegen"
|
"message": "In Paperless ablegen"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,9 +26,6 @@
|
|||||||
"storeHintPreselect": {
|
"storeHintPreselect": {
|
||||||
"message": "Small images and signatures are deselected — they usually belong to the message, not to the content."
|
"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": {
|
"storeAttachmentsInside": {
|
||||||
"message": "The $1 attachments remain inside it.",
|
"message": "The $1 attachments remain inside it.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -46,26 +43,9 @@
|
|||||||
"storeTargetExisting": {
|
"storeTargetExisting": {
|
||||||
"message": "Append as a new version to an existing document"
|
"message": "Append as a new version to an existing document"
|
||||||
},
|
},
|
||||||
"storeHintTitleMulti": {
|
|
||||||
"message": "With several files the title is extended by the file name."
|
|
||||||
},
|
|
||||||
"storeClassifyAfter": {
|
|
||||||
"message": "Edit the classification after storing"
|
|
||||||
},
|
|
||||||
"classifyTitle": {
|
"classifyTitle": {
|
||||||
"message": "Edit classification"
|
"message": "Edit classification"
|
||||||
},
|
},
|
||||||
"classifyHintNew": {
|
|
||||||
"message": "Unknown correspondents and document types will be created."
|
|
||||||
},
|
|
||||||
"classifyMoreStored": {
|
|
||||||
"message": "$1 files stored",
|
|
||||||
"placeholders": {
|
|
||||||
"1": {
|
|
||||||
"content": "$1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"insertTitle": {
|
"insertTitle": {
|
||||||
"message": "Insert from Paperless"
|
"message": "Insert from Paperless"
|
||||||
},
|
},
|
||||||
@@ -235,9 +215,6 @@
|
|||||||
"errNoPermission": {
|
"errNoPermission": {
|
||||||
"message": "Without access permission for the address the extension cannot work."
|
"message": "Without access permission for the address the extension cannot work."
|
||||||
},
|
},
|
||||||
"linkLabel": {
|
|
||||||
"message": "File"
|
|
||||||
},
|
|
||||||
"linkValidUntil": {
|
"linkValidUntil": {
|
||||||
"message": "Link valid until $1",
|
"message": "Link valid until $1",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -279,12 +256,6 @@
|
|||||||
"optionsHintMailFormat": {
|
"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."
|
"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": {
|
"fieldPdfFontSize": {
|
||||||
"message": "Font size"
|
"message": "Font size"
|
||||||
},
|
},
|
||||||
@@ -321,9 +292,6 @@
|
|||||||
"artPdfUnavailable": {
|
"artPdfUnavailable": {
|
||||||
"message": "Not possible for this format - only text files can be prepared."
|
"message": "Not possible for this format - only text files can be prepared."
|
||||||
},
|
},
|
||||||
"btnSkipAll": {
|
|
||||||
"message": "Skip all"
|
|
||||||
},
|
|
||||||
"classifyProgress": {
|
"classifyProgress": {
|
||||||
"message": "$1 of $2",
|
"message": "$1 of $2",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -335,17 +303,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"msgUploadingOf": {
|
|
||||||
"message": "Storing … $1 of $2",
|
|
||||||
"placeholders": {
|
|
||||||
"1": {
|
|
||||||
"content": "$1"
|
|
||||||
},
|
|
||||||
"2": {
|
|
||||||
"content": "$2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"stepTitle": {
|
"stepTitle": {
|
||||||
"message": "Store in Paperless"
|
"message": "Store in Paperless"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,9 +26,6 @@
|
|||||||
"storeHintPreselect": {
|
"storeHintPreselect": {
|
||||||
"message": "Les petites images et les signatures sont décochées — elles font partie du message, pas du contenu."
|
"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": {
|
"storeAttachmentsInside": {
|
||||||
"message": "Les $1 pièces jointes y restent incluses.",
|
"message": "Les $1 pièces jointes y restent incluses.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -46,26 +43,9 @@
|
|||||||
"storeTargetExisting": {
|
"storeTargetExisting": {
|
||||||
"message": "Ajouter comme nouvelle version à un document existant"
|
"message": "Ajouter comme nouvelle version à un document existant"
|
||||||
},
|
},
|
||||||
"storeHintTitleMulti": {
|
|
||||||
"message": "Avec plusieurs fichiers, le titre est complété par le nom du fichier."
|
|
||||||
},
|
|
||||||
"storeClassifyAfter": {
|
|
||||||
"message": "Modifier le classement après l'enregistrement"
|
|
||||||
},
|
|
||||||
"classifyTitle": {
|
"classifyTitle": {
|
||||||
"message": "Modifier le classement"
|
"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": {
|
"insertTitle": {
|
||||||
"message": "Insérer depuis Paperless"
|
"message": "Insérer depuis Paperless"
|
||||||
},
|
},
|
||||||
@@ -235,9 +215,6 @@
|
|||||||
"errNoPermission": {
|
"errNoPermission": {
|
||||||
"message": "Sans droit d'accès à l'adresse, l'extension ne peut pas fonctionner."
|
"message": "Sans droit d'accès à l'adresse, l'extension ne peut pas fonctionner."
|
||||||
},
|
},
|
||||||
"linkLabel": {
|
|
||||||
"message": "Fichier"
|
|
||||||
},
|
|
||||||
"linkValidUntil": {
|
"linkValidUntil": {
|
||||||
"message": "Lien valable jusqu'au $1",
|
"message": "Lien valable jusqu'au $1",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -279,12 +256,6 @@
|
|||||||
"optionsHintMailFormat": {
|
"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."
|
"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": {
|
"fieldPdfFontSize": {
|
||||||
"message": "Taille de police"
|
"message": "Taille de police"
|
||||||
},
|
},
|
||||||
@@ -321,9 +292,6 @@
|
|||||||
"artPdfUnavailable": {
|
"artPdfUnavailable": {
|
||||||
"message": "Impossible pour ce format - seuls les fichiers texte peuvent être préparés."
|
"message": "Impossible pour ce format - seuls les fichiers texte peuvent être préparés."
|
||||||
},
|
},
|
||||||
"btnSkipAll": {
|
|
||||||
"message": "Tout ignorer"
|
|
||||||
},
|
|
||||||
"classifyProgress": {
|
"classifyProgress": {
|
||||||
"message": "$1 sur $2",
|
"message": "$1 sur $2",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -335,17 +303,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"msgUploadingOf": {
|
|
||||||
"message": "Enregistrement … $1 sur $2",
|
|
||||||
"placeholders": {
|
|
||||||
"1": {
|
|
||||||
"content": "$1"
|
|
||||||
},
|
|
||||||
"2": {
|
|
||||||
"content": "$2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"stepTitle": {
|
"stepTitle": {
|
||||||
"message": "Enregistrer dans Paperless"
|
"message": "Enregistrer dans Paperless"
|
||||||
},
|
},
|
||||||
|
|||||||
112
background.js
@@ -315,81 +315,81 @@ browser.runtime.onConnect.addListener((port) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
browser.runtime.onMessage.addListener(async (nachricht, absender) => {
|
/*
|
||||||
// Nur Nachrichten aus dieser Erweiterung annehmen. Thunderbird lässt
|
* Befehle aus den Dialogen.
|
||||||
// fremde Absender hier zwar ohnehin nicht durch, solange kein
|
*
|
||||||
// externally_connectable gesetzt ist - die Prüfung kostet nichts und
|
* Der Listener selbst ist bewusst NICHT async: Ein async-Listener liefert
|
||||||
// hält die Zusicherung explizit.
|
* immer ein Promise zurück und beansprucht damit jede Nachricht - auch
|
||||||
if (absender?.id && absender.id !== browser.runtime.id) {
|
* solche, die gar nicht für ihn bestimmt sind. Stattdessen prüft er
|
||||||
return { ok: false, fehler: "Fremder Absender" };
|
* synchron, ob die Nachricht aus dieser Erweiterung stammt und einen
|
||||||
}
|
* bekannten Befehl trägt, und gibt nur dann das Promise des Handlers
|
||||||
try {
|
* zurück. Alles andere lehnt er mit false ab, sodass andere Empfänger
|
||||||
switch (nachricht?.befehl) {
|
* antworten können.
|
||||||
case "auftrag":
|
* Siehe https://webextension-api.thunderbird.net/en/mv3/guides/runtimeMessaging.html
|
||||||
return { ok: true, daten: auftraege.get(nachricht.nummer) || null };
|
*/
|
||||||
|
const BEFEHLE = {
|
||||||
|
auftrag: async (n) => auftraege.get(n.nummer) || null,
|
||||||
|
|
||||||
case "einstellungen":
|
einstellungen: async () => ladeEinstellungen(),
|
||||||
return { ok: true, daten: await ladeEinstellungen() };
|
|
||||||
|
|
||||||
case "berechtigungPruefen":
|
berechtigungPruefen: async () => berechtigungPruefen(),
|
||||||
return { ok: true, daten: await 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();
|
const c = await client();
|
||||||
return { ok: true, daten: await c.search(nachricht.query) };
|
return c.versions(await c.document(n.id));
|
||||||
}
|
},
|
||||||
|
|
||||||
case "dokument": {
|
stammdaten: async () => {
|
||||||
const c = await client();
|
|
||||||
return { ok: true, daten: await c.document(nachricht.id) };
|
|
||||||
}
|
|
||||||
|
|
||||||
case "versionen": {
|
|
||||||
const c = await client();
|
|
||||||
const doc = await c.document(nachricht.id);
|
|
||||||
return { ok: true, daten: await c.versions(doc) };
|
|
||||||
}
|
|
||||||
|
|
||||||
case "stammdaten": {
|
|
||||||
const c = await client();
|
const c = await client();
|
||||||
const [tags, korr, typen] = await Promise.all(
|
const [tags, korr, typen] = await Promise.all(
|
||||||
[c.tags(), c.correspondents(), c.documentTypes()]);
|
[c.tags(), c.correspondents(), c.documentTypes()]);
|
||||||
return { ok: true, daten: { tags, korr, typen } };
|
return { tags, korr, typen };
|
||||||
}
|
},
|
||||||
|
|
||||||
case "schrittOeffnen":
|
schrittOeffnen: async (n) => {
|
||||||
await dialogOeffnen("schritt", nachricht.auftrag);
|
await dialogOeffnen("schritt", n.auftrag);
|
||||||
return { ok: true };
|
return undefined;
|
||||||
|
},
|
||||||
|
|
||||||
case "dateiListe":
|
dateiListe: async (n) => dateiListe(n.auftrag),
|
||||||
return { ok: true, daten: dateiListe(nachricht.auftrag) };
|
|
||||||
|
|
||||||
case "ablegenEine":
|
ablegenEine: async (n) => ablegenEine(n),
|
||||||
return { ok: true, daten: await ablegenEine(nachricht) };
|
|
||||||
|
|
||||||
case "klassifizieren": {
|
klassifizieren: async (n) => {
|
||||||
const c = await client();
|
const c = await client();
|
||||||
const payload = { ...nachricht.payload };
|
const payload = { ...n.payload };
|
||||||
payload.correspondent = await aufloesen(
|
payload.correspondent = await aufloesen(c, "correspondents/", n.korrName);
|
||||||
c, "correspondents/", nachricht.korrName);
|
payload.document_type = await aufloesen(c, "document_types/", n.typName);
|
||||||
payload.document_type = await aufloesen(
|
await c.patchDocument(n.id, payload);
|
||||||
c, "document_types/", nachricht.typName);
|
return undefined;
|
||||||
await c.patchDocument(nachricht.id, payload);
|
},
|
||||||
return { ok: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
case "anhaengen":
|
anhaengen: async (n) => anhaengenAnEntwurf(n),
|
||||||
return { ok: true, daten: await anhaengenAnEntwurf(nachricht) };
|
|
||||||
|
|
||||||
case "freigabelink":
|
freigabelink: async (n) => freigabelink(n),
|
||||||
return { ok: true, daten: await freigabelink(nachricht) };
|
};
|
||||||
|
|
||||||
default:
|
async function befehlAusfuehren(handler, nachricht) {
|
||||||
return { ok: false, fehler: `Unbekannter Befehl: ${nachricht?.befehl}` };
|
try {
|
||||||
}
|
const daten = await handler(nachricht);
|
||||||
|
return daten === undefined ? { ok: true } : { ok: true, daten };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return { ok: false, fehler: String(err?.message || 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() {
|
async function client() {
|
||||||
|
|||||||
67
build.sh
@@ -11,18 +11,45 @@ NAME="paperless-connector"
|
|||||||
VERSION="$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' manifest.json | head -1)"
|
VERSION="$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' manifest.json | head -1)"
|
||||||
OUT="${NAME}-${VERSION}.xpi"
|
OUT="${NAME}-${VERSION}.xpi"
|
||||||
|
|
||||||
# Vollständigkeit prüfen. zip überspringt fehlende Pfade stillschweigend -
|
# Paketinhalt als feste Liste. Nur was hier steht, landet in der .xpi -
|
||||||
# ohne diese Prüfung entstünde ein Paket, das sich nicht laden lässt.
|
# so geraten keine Altlasten (alte Dialoge, ungenutzte Symbole, Quell-SVGs
|
||||||
|
# aus make_icons.py) ins Paket. addons.thunderbird.net lehnt Pakete mit
|
||||||
|
# ungenutzten Dateien ab.
|
||||||
|
DATEIEN="
|
||||||
|
manifest.json
|
||||||
|
background.js
|
||||||
|
LICENSE
|
||||||
|
lib/client.js
|
||||||
|
lib/settings.js
|
||||||
|
lib/pdf.js
|
||||||
|
lib/mailpdf.js
|
||||||
|
ui/gemeinsam.css
|
||||||
|
ui/gemeinsam.js
|
||||||
|
ui/store.html
|
||||||
|
ui/store.js
|
||||||
|
ui/schritt.html
|
||||||
|
ui/schritt.js
|
||||||
|
ui/insert.html
|
||||||
|
ui/insert.js
|
||||||
|
ui/options.html
|
||||||
|
ui/options.js
|
||||||
|
_locales/de/messages.json
|
||||||
|
_locales/en/messages.json
|
||||||
|
_locales/fr/messages.json
|
||||||
|
icons/paperless-logo_16.png
|
||||||
|
icons/paperless-logo_32.png
|
||||||
|
icons/paperless-logo_64.png
|
||||||
|
icons/paperless-store_16.png
|
||||||
|
icons/paperless-store_32.png
|
||||||
|
icons/paperless-insert_16.png
|
||||||
|
icons/paperless-insert_32.png
|
||||||
|
"
|
||||||
|
|
||||||
|
# Vollständigkeit prüfen. zip überspringt fehlende Pfade nicht immer
|
||||||
|
# lautstark - ohne diese Prüfung entstünde ein Paket, das sich nicht
|
||||||
|
# laden lässt.
|
||||||
FEHLT=""
|
FEHLT=""
|
||||||
for f in manifest.json background.js \
|
for f in $DATEIEN; do
|
||||||
lib/client.js lib/settings.js lib/pdf.js lib/mailpdf.js \
|
|
||||||
ui/store.html ui/store.js ui/schritt.html ui/schritt.js \
|
|
||||||
ui/insert.html ui/insert.js ui/options.html ui/options.js \
|
|
||||||
ui/gemeinsam.js ui/gemeinsam.css \
|
|
||||||
_locales/de/messages.json _locales/en/messages.json \
|
|
||||||
_locales/fr/messages.json \
|
|
||||||
icons/paperless-logo_64.png
|
|
||||||
do
|
|
||||||
[ -e "$f" ] || FEHLT="$FEHLT $f\n"
|
[ -e "$f" ] || FEHLT="$FEHLT $f\n"
|
||||||
done
|
done
|
||||||
if [ -n "$FEHLT" ]; then
|
if [ -n "$FEHLT" ]; then
|
||||||
@@ -32,14 +59,18 @@ if [ -n "$FEHLT" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
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" \
|
rm -f "$OUT"
|
||||||
manifest.json background.js \
|
# shellcheck disable=SC2086
|
||||||
lib ui icons _locales \
|
zip -q -X "$OUT" $DATEIEN
|
||||||
LICENSE \
|
|
||||||
-x "*.oxt" "*.xpi" "build.sh" "README.md" "SICHERHEIT.md" \
|
|
||||||
"make_icons.py" "*/.*"
|
|
||||||
|
|
||||||
if ! unzip -l "$OUT" | grep -q 'manifest.json'; then
|
if ! unzip -l "$OUT" | grep -q 'manifest.json'; then
|
||||||
echo "ABBRUCH: manifest.json fehlt im erzeugten Paket." >&2
|
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;
|
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) {
|
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({
|
const params = new URLSearchParams({
|
||||||
page_size: String(limit),
|
page_size: String(limit),
|
||||||
ordering: "-added",
|
// Ohne diese Einschränkung liefert Paperless je Treffer auch den
|
||||||
fields: "id,title,created,added,archive_serial_number,mime_type," +
|
// vollständigen Inhalt - bei fünfzig Treffern mehrere Megabyte.
|
||||||
"original_file_name"
|
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}`);
|
const data = await this.#json(`documents/?${params}`);
|
||||||
return data?.results || [];
|
return data?.results || [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document(id) {
|
document(id) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "__MSG_extName__",
|
"name": "__MSG_extName__",
|
||||||
"description": "__MSG_extDescription__",
|
"description": "__MSG_extDescription__",
|
||||||
"version": "1.14.3",
|
"version": "1.16.1",
|
||||||
"author": "Marco Morath",
|
"author": "Marco Morath",
|
||||||
"homepage_url": "https://gitea.cloud.mmorath.de/marco.morath/thunderbird-paperless-connector",
|
"homepage_url": "https://gitea.cloud.mmorath.de/marco.morath/thunderbird-paperless-connector",
|
||||||
"default_locale": "de",
|
"default_locale": "de",
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
"storage",
|
"storage",
|
||||||
"menus",
|
"menus",
|
||||||
"messagesRead",
|
"messagesRead",
|
||||||
|
"accountsRead",
|
||||||
"compose",
|
"compose",
|
||||||
"notifications"
|
"notifications"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,54 +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="abbrechenAlle" data-i18n="btnSkipAll">Alle überspringen</button>
|
|
||||||
<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>
|
|
||||||
262
ui/classify.js
@@ -1,262 +0,0 @@
|
|||||||
/**
|
|
||||||
* classify.js - Klassifizierung eines gerade abgelegten Dokuments.
|
|
||||||
*
|
|
||||||
* Wird unmittelbar nach dem Ablegen aufgerufen. Ohne diesen Schritt trägt
|
|
||||||
* ein neues Dokument nur den Titel, und nachträglich macht man es
|
|
||||||
* erfahrungsgemäß nicht mehr.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { kurz } from "../lib/settings.js";
|
|
||||||
import { uebersetzeSeite, ruf, fehlerAnzeigen, meldungAnzeigen }
|
|
||||||
from "./gemeinsam.js";
|
|
||||||
|
|
||||||
const $ = (id) => document.getElementById(id);
|
|
||||||
let dokumentId = null;
|
|
||||||
let alleTags = [];
|
|
||||||
|
|
||||||
// Bei mehreren abgelegten Dateien wird jede einzeln klassifiziert.
|
|
||||||
// Andernfalls trüge nur die erste eine sinnvolle Zuordnung, und die
|
|
||||||
// übrigen fielen später als unklassifiziert auf - wenn überhaupt.
|
|
||||||
let liste = [];
|
|
||||||
let stelle = 0;
|
|
||||||
|
|
||||||
// Gewählte Tags als Menge. Bewusst getrennt vom angezeigten Zustand der
|
|
||||||
// Kontrollkästchen: Der Filter blendet Zeilen aus, die Auswahl darf davon
|
|
||||||
// nicht berührt werden.
|
|
||||||
const gewaehlt = new Set();
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", start);
|
|
||||||
|
|
||||||
async function start() {
|
|
||||||
uebersetzeSeite();
|
|
||||||
const p = new URLSearchParams(location.search);
|
|
||||||
|
|
||||||
// "ids" für mehrere, "id" für den Einzelfall - letzteres bleibt für
|
|
||||||
// Aufrufe aus anderen Stellen erhalten.
|
|
||||||
const roh = p.get("ids") || p.get("id") || "";
|
|
||||||
liste = roh.split(",").map((x) => Number(x)).filter(Boolean);
|
|
||||||
stelle = 0;
|
|
||||||
|
|
||||||
$("ueberspringen").addEventListener("click", weiter);
|
|
||||||
$("abbrechenAlle").addEventListener("click", () => window.close());
|
|
||||||
$("speichern").addEventListener("click", speichern);
|
|
||||||
|
|
||||||
if (!liste.length) {
|
|
||||||
fehlerAnzeigen("Kein Dokument angegeben.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const stamm = await ruf("stammdaten");
|
|
||||||
alleTags = stamm.tags;
|
|
||||||
fuelleListe("korrListe", stamm.korr);
|
|
||||||
fuelleListe("typListe", stamm.typen);
|
|
||||||
zustand.korr = stamm.korr;
|
|
||||||
zustand.typen = stamm.typen;
|
|
||||||
} catch (err) {
|
|
||||||
fehlerAnzeigen(err.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await anzeigen();
|
|
||||||
}
|
|
||||||
|
|
||||||
const zustand = { korr: [], typen: [] };
|
|
||||||
|
|
||||||
/** Das Dokument an der aktuellen Stelle laden und darstellen. */
|
|
||||||
async function anzeigen() {
|
|
||||||
dokumentId = liste[stelle];
|
|
||||||
$("speichern").disabled = false;
|
|
||||||
$("ueberspringen").disabled = false;
|
|
||||||
meldungAnzeigen("");
|
|
||||||
|
|
||||||
// Der Knopf zum Abbrechen aller weiteren ist nur sinnvoll, wenn noch
|
|
||||||
// welche folgen.
|
|
||||||
$("abbrechenAlle").hidden = liste.length < 2;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const doc = await ruf("dokument", { id: dokumentId });
|
|
||||||
|
|
||||||
const zaehler = liste.length > 1
|
|
||||||
? ` · ${browser.i18n.getMessage("classifyProgress",
|
|
||||||
[String(stelle + 1), String(liste.length)])
|
|
||||||
|| `${stelle + 1} von ${liste.length}`}`
|
|
||||||
: "";
|
|
||||||
$("quelle").textContent = `#${dokumentId} · ${kurz(doc.title, 60)}`
|
|
||||||
+ zaehler;
|
|
||||||
|
|
||||||
$("titel").value = doc.title || "";
|
|
||||||
$("erstellt").value = (doc.created || "").slice(0, 10);
|
|
||||||
$("korr").value = nameVon(zustand.korr, doc.correspondent);
|
|
||||||
$("typ").value = nameVon(zustand.typen, doc.document_type);
|
|
||||||
zeigeTags(doc.tags || []);
|
|
||||||
if ($("tagSuche")) $("tagSuche").value = "";
|
|
||||||
} catch (err) {
|
|
||||||
fehlerAnzeigen(err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Zum nächsten Dokument, oder schliessen. */
|
|
||||||
async function weiter() {
|
|
||||||
stelle += 1;
|
|
||||||
if (stelle >= liste.length) {
|
|
||||||
window.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await anzeigen();
|
|
||||||
}
|
|
||||||
|
|
||||||
function fuelleListe(id, eintraege) {
|
|
||||||
const dl = $(id);
|
|
||||||
dl.replaceChildren();
|
|
||||||
for (const e of eintraege) {
|
|
||||||
const o = document.createElement("option");
|
|
||||||
o.value = e.name;
|
|
||||||
dl.append(o);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function nameVon(liste, id) {
|
|
||||||
return liste.find((x) => x.id === id)?.name || "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function zeigeTags(gesetzt) {
|
|
||||||
for (const id of gesetzt) gewaehlt.add(Number(id));
|
|
||||||
const feld = $("tagSuche");
|
|
||||||
if (feld) {
|
|
||||||
feld.addEventListener("input", zeichneTags);
|
|
||||||
// Eingabetaste soll den Dialog nicht abschicken, sondern beim
|
|
||||||
// einzigen Treffer diesen umschalten - das ist beim Tippen der
|
|
||||||
// erwartete Ablauf.
|
|
||||||
feld.addEventListener("keydown", (e) => {
|
|
||||||
if (e.key !== "Enter") return;
|
|
||||||
e.preventDefault();
|
|
||||||
const treffer = gefilterteTags();
|
|
||||||
if (treffer.length === 1) {
|
|
||||||
umschalten(treffer[0].id);
|
|
||||||
feld.value = "";
|
|
||||||
zeichneTags();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
zeichneTags();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Tags, die zum aktuellen Filter passen und noch nicht gewählt sind. */
|
|
||||||
function gefilterteTags() {
|
|
||||||
const filter = ($("tagSuche")?.value || "").trim().toLowerCase();
|
|
||||||
return alleTags.filter((t) => {
|
|
||||||
if (gewaehlt.has(Number(t.id))) return false;
|
|
||||||
if (!filter) return true;
|
|
||||||
return String(t.name).toLowerCase().includes(filter);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function umschalten(id) {
|
|
||||||
const n = Number(id);
|
|
||||||
if (gewaehlt.has(n)) gewaehlt.delete(n);
|
|
||||||
else gewaehlt.add(n);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tagliste zeichnen.
|
|
||||||
*
|
|
||||||
* Zwei Bereiche: oben die gewählten, darunter die übrigen. Die gewählten
|
|
||||||
* bleiben immer sichtbar, auch wenn der Filter sie ausschliesst - sonst
|
|
||||||
* verliert man beim Suchen den Überblick, was bereits angehakt ist.
|
|
||||||
*/
|
|
||||||
function zeichneTags() {
|
|
||||||
const filter = ($("tagSuche")?.value || "").trim().toLowerCase();
|
|
||||||
|
|
||||||
const oben = $("tagsGewaehlt");
|
|
||||||
oben.replaceChildren();
|
|
||||||
const gewaehlteListe = alleTags
|
|
||||||
.filter((t) => gewaehlt.has(Number(t.id)))
|
|
||||||
.sort((a, b) => String(a.name).localeCompare(String(b.name), "de"));
|
|
||||||
for (const tag of gewaehlteListe) {
|
|
||||||
oben.append(tagZeile(tag, true, ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
const unten = $("tags");
|
|
||||||
unten.replaceChildren();
|
|
||||||
const rest = gefilterteTags()
|
|
||||||
.sort((a, b) => String(a.name).localeCompare(String(b.name), "de"));
|
|
||||||
for (const tag of rest) {
|
|
||||||
unten.append(tagZeile(tag, false, filter));
|
|
||||||
}
|
|
||||||
|
|
||||||
const leer = $("tagsLeer");
|
|
||||||
if (leer) leer.style.display = (rest.length === 0 && filter) ? "" : "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
function tagZeile(tag, istGewaehlt, filter) {
|
|
||||||
const zeile = document.createElement("div");
|
|
||||||
zeile.className = "wahl";
|
|
||||||
|
|
||||||
const box = document.createElement("input");
|
|
||||||
box.type = "checkbox";
|
|
||||||
box.id = `t${tag.id}`;
|
|
||||||
box.value = String(tag.id);
|
|
||||||
box.checked = istGewaehlt;
|
|
||||||
box.addEventListener("change", () => {
|
|
||||||
umschalten(tag.id);
|
|
||||||
zeichneTags();
|
|
||||||
});
|
|
||||||
|
|
||||||
const bez = document.createElement("label");
|
|
||||||
bez.htmlFor = box.id;
|
|
||||||
beschriftungSetzen(bez, String(tag.name), filter);
|
|
||||||
|
|
||||||
zeile.append(box, bez);
|
|
||||||
return zeile;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Beschriftung mit hervorgehobenem Treffer.
|
|
||||||
*
|
|
||||||
* Aufbau über createElement und textContent, nicht über innerHTML - ein
|
|
||||||
* Tagname stammt aus Paperless und damit mittelbar aus fremden Quellen.
|
|
||||||
*/
|
|
||||||
function beschriftungSetzen(el, name, filter) {
|
|
||||||
el.replaceChildren();
|
|
||||||
if (!filter) {
|
|
||||||
el.textContent = name;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const pos = name.toLowerCase().indexOf(filter);
|
|
||||||
if (pos < 0) {
|
|
||||||
el.textContent = name;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
el.append(document.createTextNode(name.slice(0, pos)));
|
|
||||||
const treffer = document.createElement("span");
|
|
||||||
treffer.className = "trefferHervor";
|
|
||||||
treffer.textContent = name.slice(pos, pos + filter.length);
|
|
||||||
el.append(treffer);
|
|
||||||
el.append(document.createTextNode(name.slice(pos + filter.length)));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function speichern() {
|
|
||||||
$("speichern").disabled = true;
|
|
||||||
meldungAnzeigen(browser.i18n.getMessage("msgSaving") || "Wird gespeichert …");
|
|
||||||
try {
|
|
||||||
// Aus der Menge lesen, nicht aus den Kästchen: Gefilterte Zeilen
|
|
||||||
// sind gar nicht im Dokument, ihre Auswahl ginge sonst verloren.
|
|
||||||
const tags = [...gewaehlt];
|
|
||||||
const payload = { title: $("titel").value.trim(), tags };
|
|
||||||
const erstellt = $("erstellt").value.trim();
|
|
||||||
if (erstellt) payload.created = erstellt;
|
|
||||||
|
|
||||||
await ruf("klassifizieren", {
|
|
||||||
id: dokumentId,
|
|
||||||
payload,
|
|
||||||
korrName: $("korr").value.trim(),
|
|
||||||
typName: $("typ").value.trim()
|
|
||||||
});
|
|
||||||
meldungAnzeigen(browser.i18n.getMessage("msgSaved") || "Gespeichert.");
|
|
||||||
setTimeout(weiter, 600);
|
|
||||||
} catch (err) {
|
|
||||||
fehlerAnzeigen(err.message);
|
|
||||||
$("speichern").disabled = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
|
|
||||||
<div class="wachsen">
|
<div class="wachsen">
|
||||||
<label data-i18n="colVersions">Versionen</label>
|
<label data-i18n="colVersions">Versionen</label>
|
||||||
<div class="liste" style="min-height:70px">
|
<div class="liste" >
|
||||||
<table>
|
<table>
|
||||||
<thead><tr>
|
<thead><tr>
|
||||||
<th class="zahl" data-i18n="colNo">Nr</th>
|
<th class="zahl" data-i18n="colNo">Nr</th>
|
||||||
@@ -68,6 +68,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="hinweis" data-i18n="insertHintLink">
|
<div class="hinweis" data-i18n="insertHintLink">
|
||||||
Der Link ist ohne Anmeldung abrufbar. 0 Tage bedeutet unbegrenzt.
|
Der Link ist ohne Anmeldung abrufbar. 0 Tage bedeutet unbegrenzt.
|
||||||
|
Leeres Tag-Feld bedeutet: keine Kennzeichnung setzen.
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
|||||||