Merge remote-tracking branch 'upstream/dev' into feature/updated-nav-bar

This commit is contained in:
Michael Shamoon 2021-01-01 12:16:55 -08:00
commit a5ae056c9b
54 changed files with 1079 additions and 411 deletions

View File

@ -42,6 +42,7 @@ whoosh="~=2.7.4"
inotifyrecursive = "~=0.3.4" inotifyrecursive = "~=0.3.4"
ocrmypdf = "*" ocrmypdf = "*"
tqdm = "*" tqdm = "*"
tika = "*"
[dev-packages] [dev-packages]
coveralls = "*" coveralls = "*"

View File

@ -1,4 +1,4 @@
[![Build Status](https://travis-ci.org/jonaswinkler/paperless-ng.svg?branch=master)](https://travis-ci.org/jonaswinkler/paperless-ng) [![Build Status](https://travis-ci.com/jonaswinkler/paperless-ng.svg?branch=master)](https://travis-ci.com/jonaswinkler/paperless-ng)
[![Documentation Status](https://readthedocs.org/projects/paperless-ng/badge/?version=latest)](https://paperless-ng.readthedocs.io/en/latest/?badge=latest) [![Documentation Status](https://readthedocs.org/projects/paperless-ng/badge/?version=latest)](https://paperless-ng.readthedocs.io/en/latest/?badge=latest)
[![Gitter](https://badges.gitter.im/paperless-ng/community.svg)](https://gitter.im/paperless-ng/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Gitter](https://badges.gitter.im/paperless-ng/community.svg)](https://gitter.im/paperless-ng/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Docker Hub Pulls](https://img.shields.io/docker/pulls/jonaswinkler/paperless-ng.svg)](https://hub.docker.com/r/jonaswinkler/paperless-ng) [![Docker Hub Pulls](https://img.shields.io/docker/pulls/jonaswinkler/paperless-ng.svg)](https://hub.docker.com/r/jonaswinkler/paperless-ng)

View File

@ -1,4 +1,4 @@
bind = ['[::]:8000', 'localhost:8000'] bind = '0.0.0.0:8000'
backlog = 2048 backlog = 2048
workers = 3 workers = 3
worker_class = 'sync' worker_class = 'sync'

View File

@ -15,7 +15,7 @@ services:
POSTGRES_PASSWORD: paperless POSTGRES_PASSWORD: paperless
webserver: webserver:
image: jonaswinkler/paperless-ng:0.9.10 image: jonaswinkler/paperless-ng:0.9.11
restart: always restart: always
depends_on: depends_on:
- db - db

View File

@ -5,7 +5,7 @@ services:
restart: always restart: always
webserver: webserver:
image: jonaswinkler/paperless-ng:0.9.10 image: jonaswinkler/paperless-ng:0.9.11
restart: always restart: always
depends_on: depends_on:
- broker - broker

View File

@ -0,0 +1,43 @@
version: "3.4"
services:
broker:
image: redis:6.0
restart: always
webserver:
image: jonaswinkler/paperless-ng:0.9.9
restart: always
depends_on:
- broker
ports:
- 8000:8000
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000"]
interval: 30s
timeout: 10s
retries: 5
volumes:
- data:/usr/src/paperless/data
- media:/usr/src/paperless/media
- ./export:/usr/src/paperless/export
- ./consume:/usr/src/paperless/consume
env_file: docker-compose.env
environment:
PAPERLESS_REDIS: redis://broker:6379
PAPERLESS_TIKA_ENABLED: 1
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg:
image: thecodingmachine/gotenberg
restart: unless-stopped
environment:
DISABLE_GOOGLE_CHROME: 1
tika:
image: apache/tika
restart: unless-stopped
volumes:
data:
media:

View File

@ -0,0 +1,43 @@
version: "3.4"
services:
broker:
image: redis:6.0
restart: always
webserver:
build: .
restart: always
depends_on:
- broker
ports:
- 8000:8000
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000"]
interval: 30s
timeout: 10s
retries: 5
volumes:
- data:/usr/src/paperless/data
- media:/usr/src/paperless/media
- ./export:/usr/src/paperless/export
- ./consume:/usr/src/paperless/consume
env_file: docker-compose.env
environment:
PAPERLESS_REDIS: redis://broker:6379
PAPERLESS_TIKA_ENABLED: 1
PAPERLESS_TIKA_GOTENBERG_ENDPOINT: http://gotenberg:3000
PAPERLESS_TIKA_ENDPOINT: http://tika:9998
gotenberg:
image: thecodingmachine/gotenberg
restart: unless-stopped
environment:
DISABLE_GOOGLE_CHROME: 1
tika:
image: apache/tika
restart: unless-stopped
volumes:
data:
media:

View File

@ -5,6 +5,13 @@
Changelog Changelog
********* *********
paperless-ng 0.9.11
###################
* Fixed an issue with the docker image not starting at all due to a configuration change of the web server.
paperless-ng 0.9.10 paperless-ng 0.9.10
################### ###################
@ -15,6 +22,7 @@ paperless-ng 0.9.10
* Other changes and additions * Other changes and additions
* Thanks to `zjean`_, paperless now publishes a webmanifest, which is useful for adding the application to home screens on mobile devices.
* The Paperless-ng logo now navigates to the dashboard. * The Paperless-ng logo now navigates to the dashboard.
* Filter for documents that don't have any correspondents, types or tags assigned. * Filter for documents that don't have any correspondents, types or tags assigned.
* Tags, types and correspondents are now sorted case insensitive. * Tags, types and correspondents are now sorted case insensitive.
@ -25,6 +33,8 @@ paperless-ng 0.9.10
* Added missing dependencies for Raspberry Pi builds. * Added missing dependencies for Raspberry Pi builds.
* Fixed an issue with plain text file consumption: Thumbnail generation failed due to missing fonts. * Fixed an issue with plain text file consumption: Thumbnail generation failed due to missing fonts.
* An issue with the search index reporting missing documents after bulk deletes was fixed. * An issue with the search index reporting missing documents after bulk deletes was fixed.
* Issue with the tag selector not clearing input correctly.
* The consumer used to stop working when encountering an incomplete classifier model file.
.. note:: .. note::
@ -956,6 +966,7 @@ bulk of the work on this big change.
* Initial release * Initial release
.. _zjean: https://github.com/zjean
.. _rYR79435: https://github.com/rYR79435 .. _rYR79435: https://github.com/rYR79435
.. _Michael Shamoon: https://github.com/shamoon .. _Michael Shamoon: https://github.com/shamoon
.. _jayme-github: http://github.com/jayme-github .. _jayme-github: http://github.com/jayme-github

View File

@ -277,6 +277,35 @@ PAPERLESS_OCR_USER_ARG=<json>
{"deskew": true, "optimize": 3, "unpaper_args": "--pre-rotate 90"} {"deskew": true, "optimize": 3, "unpaper_args": "--pre-rotate 90"}
.. _configuration-tika:
Tika settings
#############
Paperless can make use of `Tika <https://tika.apache.org/>`_ and
`Gotenberg <https://thecodingmachine.github.io/gotenberg/>`_ for parsing and
converting "Office" documents (such as ".doc", ".xlsx" and ".odt"). If you
wish to use this, you must provide a Tika server and a Gotenberg server,
configure their endpoints, and enable the feature.
If you run paperless on docker, you can add those services to the docker-compose
file (see the examples provided).
PAPERLESS_TIKA_ENABLED=<bool>
Enable (or disable) the Tika parser.
Defaults to false.
PAPERLESS_TIKA_ENDPOINT=<url>
Set the endpoint URL were Paperless can reach your Tika server.
Defaults to "http://localhost:9998".
PAPERLESS_TIKA_GOTENBERG_ENDPOINT=<url>
Set the endpoint URL were Paperless can reach your Gotenberg server.
Defaults to "http://localhost:3000".
Software tweaks Software tweaks
############### ###############

View File

@ -39,7 +39,7 @@
#PAPERLESS_OCR_OUTPUT_TYPE=pdfa #PAPERLESS_OCR_OUTPUT_TYPE=pdfa
#PAPERLESS_OCR_PAGES=1 #PAPERLESS_OCR_PAGES=1
#PAPERLESS_OCR_IMAGE_DPI=300 #PAPERLESS_OCR_IMAGE_DPI=300
#PAPERLESS_OCR_USER_ARG={} #PAPERLESS_OCR_USER_ARGS={}
#PAPERLESS_CONVERT_MEMORY_LIMIT=0 #PAPERLESS_CONVERT_MEMORY_LIMIT=0
#PAPERLESS_CONVERT_TMPDIR=/var/tmp/paperless #PAPERLESS_CONVERT_TMPDIR=/var/tmp/paperless

View File

@ -1,133 +1,138 @@
{ {
"$schema": "./node_modules/@angular/cli/lib/config/schema.json", "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1, "version": 1,
"newProjectRoot": "projects", "newProjectRoot": "projects",
"projects": { "projects": {
"paperless-ui": { "paperless-ui": {
"projectType": "application", "projectType": "application",
"schematics": { "schematics": {
"@schematics/angular:component": { "@schematics/angular:component": {
"style": "scss" "style": "scss"
} }
}, },
"root": "", "root": "",
"sourceRoot": "src", "sourceRoot": "src",
"prefix": "app", "prefix": "app",
"architect": { "architect": {
"build": { "build": {
"builder": "@angular-devkit/build-angular:browser", "builder": "@angular-devkit/build-angular:browser",
"options": { "options": {
"outputPath": "dist/paperless-ui", "outputPath": "dist/paperless-ui",
"outputHashing": "none", "outputHashing": "none",
"index": "src/index.html", "index": "src/index.html",
"main": "src/main.ts", "main": "src/main.ts",
"polyfills": "src/polyfills.ts", "polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json", "tsConfig": "tsconfig.app.json",
"aot": true, "aot": true,
"assets": [ "assets": [
"src/favicon.ico", "src/favicon.ico",
"src/assets" "src/assets",
], "src/manifest.webmanifest"
"styles": [ ],
"src/styles.scss" "styles": [
], "src/styles.scss"
"scripts": [], ],
"allowedCommonJsDependencies": [ "scripts": [],
"ng2-pdf-viewer" "allowedCommonJsDependencies": [
] "ng2-pdf-viewer"
}, ]
"configurations": { },
"production": { "configurations": {
"fileReplacements": [ "production": {
{ "fileReplacements": [
"replace": "src/environments/environment.ts", {
"with": "src/environments/environment.prod.ts" "replace": "src/environments/environment.ts",
} "with": "src/environments/environment.prod.ts"
], }
"optimization": true, ],
"outputHashing": "none", "optimization": true,
"sourceMap": false, "outputHashing": "none",
"extractCss": true, "sourceMap": false,
"namedChunks": false, "extractCss": true,
"extractLicenses": true, "namedChunks": false,
"vendorChunk": false, "extractLicenses": true,
"buildOptimizer": true, "vendorChunk": false,
"budgets": [ "buildOptimizer": true,
{ "budgets": [
"type": "initial", {
"maximumWarning": "2mb", "type": "initial",
"maximumError": "5mb" "maximumWarning": "2mb",
}, "maximumError": "5mb"
{ },
"type": "anyComponentStyle", {
"maximumWarning": "6kb", "type": "anyComponentStyle",
"maximumError": "10kb" "maximumWarning": "6kb",
} "maximumError": "10kb"
] }
} ]
} }
}, }
"serve": { },
"builder": "@angular-devkit/build-angular:dev-server", "serve": {
"options": { "builder": "@angular-devkit/build-angular:dev-server",
"browserTarget": "paperless-ui:build" "options": {
}, "browserTarget": "paperless-ui:build"
"configurations": { },
"production": { "configurations": {
"browserTarget": "paperless-ui:build:production" "production": {
} "browserTarget": "paperless-ui:build:production"
} }
}, }
"extract-i18n": { },
"builder": "@angular-devkit/build-angular:extract-i18n", "extract-i18n": {
"options": { "builder": "@angular-devkit/build-angular:extract-i18n",
"browserTarget": "paperless-ui:build" "options": {
} "browserTarget": "paperless-ui:build"
}, }
"test": { },
"builder": "@angular-devkit/build-angular:karma", "test": {
"options": { "builder": "@angular-devkit/build-angular:karma",
"main": "src/test.ts", "options": {
"polyfills": "src/polyfills.ts", "main": "src/test.ts",
"tsConfig": "tsconfig.spec.json", "polyfills": "src/polyfills.ts",
"karmaConfig": "karma.conf.js", "tsConfig": "tsconfig.spec.json",
"assets": [ "karmaConfig": "karma.conf.js",
"src/favicon.ico", "assets": [
"src/assets" "src/favicon.ico",
], "src/assets",
"styles": [ "src/manifest.webmanifest"
"src/styles.scss" ],
], "styles": [
"scripts": [] "src/styles.scss"
} ],
}, "scripts": []
"lint": { }
"builder": "@angular-devkit/build-angular:tslint", },
"options": { "lint": {
"tsConfig": [ "builder": "@angular-devkit/build-angular:tslint",
"tsconfig.app.json", "options": {
"tsconfig.spec.json", "tsConfig": [
"e2e/tsconfig.json" "tsconfig.app.json",
], "tsconfig.spec.json",
"exclude": [ "e2e/tsconfig.json"
"**/node_modules/**" ],
] "exclude": [
} "**/node_modules/**"
}, ]
"e2e": { }
"builder": "@angular-devkit/build-angular:protractor", },
"options": { "e2e": {
"protractorConfig": "e2e/protractor.conf.js", "builder": "@angular-devkit/build-angular:protractor",
"devServerTarget": "paperless-ui:serve" "options": {
}, "protractorConfig": "e2e/protractor.conf.js",
"configurations": { "devServerTarget": "paperless-ui:serve"
"production": { },
"devServerTarget": "paperless-ui:serve:production" "configurations": {
} "production": {
} "devServerTarget": "paperless-ui:serve:production"
} }
} }
} }
}, }
"defaultProject": "paperless-ui" }
},
"defaultProject": "paperless-ui",
"cli": {
"analytics": "7c47c2bc-b97e-4014-85ae-b0c99b5750b4"
}
} }

View File

@ -149,8 +149,8 @@
<context context-type="linenumber">161</context> <context context-type="linenumber">161</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5277522254327902345" datatype="html"> <trans-unit id="5382975254277698192" datatype="html">
<source>Do you really want to delete document &apos;<x id="PH" equiv-text="this.document.title"/>&apos;?</source> <source>Do you really want to delete document &quot;<x id="PH" equiv-text="this.document.title"/>&quot;?</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context> <context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
<context context-type="linenumber">162</context> <context context-type="linenumber">162</context>
@ -1050,36 +1050,36 @@
<context context-type="linenumber">115</context> <context context-type="linenumber">115</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8965789168028339624" datatype="html"> <trans-unit id="6619516195038467207" datatype="html">
<source>This operation will add the tag &quot;<x id="PH" equiv-text="tag.name"/>&quot; to all <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source> <source>This operation will add the tag &quot;<x id="PH" equiv-text="tag.name"/>&quot; to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">118</context> <context context-type="linenumber">118</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4791265247184178563" datatype="html"> <trans-unit id="1894412783609570695" datatype="html">
<source>This operation will add the tags <x id="PH" equiv-text="this._localizeList(changedTags.itemsToAdd)"/> to all <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source> <source>This operation will add the tags <x id="PH" equiv-text="this._localizeList(changedTags.itemsToAdd)"/> to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">120</context> <context context-type="linenumber">120</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7567916006270093567" datatype="html"> <trans-unit id="7181166515756808573" datatype="html">
<source>This operation will remove the tag &quot;<x id="PH" equiv-text="tag.name"/>&quot; from all <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source> <source>This operation will remove the tag &quot;<x id="PH" equiv-text="tag.name"/>&quot; from <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">123</context> <context context-type="linenumber">123</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="247266594076352528" datatype="html"> <trans-unit id="3819792277998068944" datatype="html">
<source>This operation will remove the tags <x id="PH" equiv-text="this._localizeList(changedTags.itemsToRemove)"/> from all <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source> <source>This operation will remove the tags <x id="PH" equiv-text="this._localizeList(changedTags.itemsToRemove)"/> from <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">125</context> <context context-type="linenumber">125</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4286636723521919383" datatype="html"> <trans-unit id="2739066218579571288" datatype="html">
<source>This operation will add the tags <x id="PH" equiv-text="this._localizeList(changedTags.itemsToAdd)"/> and remove the tags <x id="PH_1" equiv-text="this._localizeList(changedTags.itemsToRemove)"/> on all <x id="PH_2" equiv-text="this.list.selected.size"/> selected document(s).</source> <source>This operation will add the tags <x id="PH" equiv-text="this._localizeList(changedTags.itemsToAdd)"/> and remove the tags <x id="PH_1" equiv-text="this._localizeList(changedTags.itemsToRemove)"/> on <x id="PH_2" equiv-text="this.list.selected.size"/> selected document(s).</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">127</context> <context context-type="linenumber">127</context>
@ -1092,15 +1092,15 @@
<context context-type="linenumber">157</context> <context context-type="linenumber">157</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3171547143349510312" datatype="html"> <trans-unit id="6900893559485781849" datatype="html">
<source>This operation will assign the correspondent &quot;<x id="PH" equiv-text="correspondent.name"/>&quot; to all <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source> <source>This operation will assign the correspondent &quot;<x id="PH" equiv-text="correspondent.name"/>&quot; to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">159</context> <context context-type="linenumber">159</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5197985579238314950" datatype="html"> <trans-unit id="1257522660364398440" datatype="html">
<source>This operation will remove the correspondent from all <x id="PH" equiv-text="this.list.selected.size"/> selected document(s).</source> <source>This operation will remove the correspondent from <x id="PH" equiv-text="this.list.selected.size"/> selected document(s).</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">161</context> <context context-type="linenumber">161</context>
@ -1113,15 +1113,15 @@
<context context-type="linenumber">190</context> <context context-type="linenumber">190</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5346233369873832070" datatype="html"> <trans-unit id="332180123895325027" datatype="html">
<source>This operation will assign the document type &quot;<x id="PH" equiv-text="documentType.name"/>&quot; to all <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source> <source>This operation will assign the document type &quot;<x id="PH" equiv-text="documentType.name"/>&quot; to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">192</context> <context context-type="linenumber">192</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6005206188202839923" datatype="html"> <trans-unit id="2236642492594872779" datatype="html">
<source>This operation will remove the document type from all <x id="PH" equiv-text="this.list.selected.size"/> selected document(s).</source> <source>This operation will remove the document type from <x id="PH" equiv-text="this.list.selected.size"/> selected document(s).</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">194</context> <context context-type="linenumber">194</context>
@ -1134,8 +1134,8 @@
<context context-type="linenumber">219</context> <context context-type="linenumber">219</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3928393581343272038" datatype="html"> <trans-unit id="4303174930844518780" datatype="html">
<source>This operation will permanently delete all <x id="PH" equiv-text="this.list.selected.size"/> selected document(s).</source> <source>This operation will permanently delete <x id="PH" equiv-text="this.list.selected.size"/> selected document(s).</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">220</context> <context context-type="linenumber">220</context>
@ -1281,8 +1281,8 @@
<context context-type="linenumber">5</context> <context context-type="linenumber">5</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="80e9f19d307b1611c56aa0bdddc930d85418734c" datatype="html"> <trans-unit id="ea8d9a9486d5639d1c38c012900b8d34d5e4135d" datatype="html">
<source>You can start uploading documents by dropping them in the file upload box to the right or by dropping them in the configured consumption folder and they&apos;ll start showing up in the documents list. After you&apos;ve added some metadata to your documents, use the filtering mechanisms of paperless to create custom views (such as &apos;Recently added&apos;, &apos;Tagged TODO&apos;) and have they will be shown on the dashboard instead of this message.</source> <source>You can start uploading documents by dropping them in the file upload box to the right or by dropping them in the configured consumption folder and they&apos;ll start showing up in the documents list. After you&apos;ve added some metadata to your documents, use the filtering mechanisms of paperless to create custom views (such as &apos;Recently added&apos;, &apos;Tagged TODO&apos;) and they will appear on the dashboard instead of this message.</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/dashboard/widgets/welcome-widget/welcome-widget.component.html</context> <context context-type="sourcefile">src/app/components/dashboard/widgets/welcome-widget/welcome-widget.component.html</context>
<context context-type="linenumber">6,7</context> <context context-type="linenumber">6,7</context>

View File

@ -1,4 +1,5 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { AppViewService } from './services/app-view.service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@ -7,7 +8,8 @@ import { Component } from '@angular/core';
}) })
export class AppComponent { export class AppComponent {
constructor () { constructor (appViewService: AppViewService) {
appViewService.updateDarkModeSettings()
} }

View File

@ -5,7 +5,9 @@
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<a class="navbar-brand col-auto col-md-3 col-lg-2 mr-0 px-3 py-3 order-sm-0" routerLink="/dashboard"> <a class="navbar-brand col-auto col-md-3 col-lg-2 mr-0 px-3 py-3 order-sm-0" routerLink="/dashboard">
<img src="assets/logo-dark-notext.svg" height="18px" class="mr-2"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.43 238.91" width="16px" class="mr-2" fill="currentColor">
<path d="M194.7,0C164.22,70.94,17.64,79.74,64.55,194.06c.58,1.47-10.85,17-18.47,29.9-1.76-6.45-3.81-13.48-3.52-14.07,38.11-45.14-27.26-70.65-30.78-107.58C-4.64,131.62-10.5,182.92,39,212.53c.3,0,2.64,11.14,3.81,16.71a58.55,58.55,0,0,0-2.93,6.45c-1.17,2.93,7.62,2.64,7.62,3.22.88-.29,21.7-36.93,22.28-37.23C187.67,174.72,208.48,68.6,194.7,0ZM134.61,74.75C79.5,124,70.12,160.64,71.88,178.53,53.41,134.85,107.64,86.77,134.61,74.75ZM28.2,145.11c10.55,9.67,28.14,39.28,13.19,56.57C44.91,193.77,46.08,175.89,28.2,145.11Z" transform="translate(0 0)"/>
</svg>
<ng-container i18n="app title">Paperless-ng</ng-container> <ng-container i18n="app title">Paperless-ng</ng-container>
</a> </a>
<div class="search-form-container flex-grow-1 py-2 pb-3 pb-sm-2 px-3 pl-md-4 mr-sm-auto order-3 order-sm-1"> <div class="search-form-container flex-grow-1 py-2 pb-3 pb-sm-2 px-3 pl-md-4 mr-sm-auto order-3 order-sm-1">

View File

@ -20,7 +20,7 @@
</div> </div>
</div> </div>
<div *ngIf="selectionModel.items" class="items"> <div *ngIf="selectionModel.items" class="items">
<ng-container *ngFor="let item of selectionModel.items | filter: filterText"> <ng-container *ngFor="let item of (editing ? selectionModel.itemsSorted : selectionModel.items) | filter: filterText">
<app-toggleable-dropdown-button *ngIf="allowSelectNone || item.id" [item]="item" [state]="selectionModel.get(item.id)" (toggle)="selectionModel.toggle(item.id)"></app-toggleable-dropdown-button> <app-toggleable-dropdown-button *ngIf="allowSelectNone || item.id" [item]="item" [state]="selectionModel.get(item.id)" (toggle)="selectionModel.toggle(item.id)"></app-toggleable-dropdown-button>
</ng-container> </ng-container>
</div> </div>

View File

@ -18,6 +18,18 @@ export class FilterableDropdownSelectionModel {
items: MatchingModel[] = [] items: MatchingModel[] = []
get itemsSorted(): MatchingModel[] {
return this.items.sort((a,b) => {
if (this.getNonTemporary(a.id) == ToggleableItemState.NotSelected && this.getNonTemporary(b.id) != ToggleableItemState.NotSelected) {
return 1
} else if (this.getNonTemporary(a.id) != ToggleableItemState.NotSelected && this.getNonTemporary(b.id) == ToggleableItemState.NotSelected) {
return -1
} else {
return a.name.localeCompare(b.name)
}
})
}
private selectionStates = new Map<number, ToggleableItemState>() private selectionStates = new Map<number, ToggleableItemState>()
private temporarySelectionStates = new Map<number, ToggleableItemState>() private temporarySelectionStates = new Map<number, ToggleableItemState>()
@ -69,6 +81,10 @@ export class FilterableDropdownSelectionModel {
} }
private getNonTemporary(id: number) {
return this.selectionStates.get(id) || ToggleableItemState.NotSelected
}
get(id: number) { get(id: number) {
return this.temporarySelectionStates.get(id) || ToggleableItemState.NotSelected return this.temporarySelectionStates.get(id) || ToggleableItemState.NotSelected
} }

View File

@ -5,7 +5,9 @@
<ng-select name="tags" [items]="tags" bindLabel="name" bindValue="id" [(ngModel)]="displayValue" <ng-select name="tags" [items]="tags" bindLabel="name" bindValue="id" [(ngModel)]="displayValue"
[multiple]="true" [multiple]="true"
[closeOnSelect]="false" [closeOnSelect]="false"
[clearSearchOnAdd]="true"
[disabled]="disabled" [disabled]="disabled"
[hideSelected]="true"
(change)="ngSelectChange()"> (change)="ngSelectChange()">
<ng-template ng-label-tmp let-item="item"> <ng-template ng-label-tmp let-item="item">

File diff suppressed because one or more lines are too long

View File

@ -4,7 +4,7 @@
<img src="assets/save-filter.png" class="float-right"> <img src="assets/save-filter.png" class="float-right">
<p i18n>Paperless is running! :)</p> <p i18n>Paperless is running! :)</p>
<p i18n>You can start uploading documents by dropping them in the file upload box to the right or by dropping them in the configured consumption folder and they'll start showing up in the documents list. <p i18n>You can start uploading documents by dropping them in the file upload box to the right or by dropping them in the configured consumption folder and they'll start showing up in the documents list.
After you've added some metadata to your documents, use the filtering mechanisms of paperless to create custom views (such as 'Recently added', 'Tagged TODO') and have they will be shown on the dashboard instead of this message.</p> After you've added some metadata to your documents, use the filtering mechanisms of paperless to create custom views (such as 'Recently added', 'Tagged TODO') and they will appear on the dashboard instead of this message.</p>
<p i18n>Paperless offers some more features that try to make your life easier:</p> <p i18n>Paperless offers some more features that try to make your life easier:</p>
<ul> <ul>
<li i18n>Once you've got a couple documents in paperless and added metadata to them, paperless can assign that metadata to new documents automatically.</li> <li i18n>Once you've got a couple documents in paperless and added metadata to them, paperless can assign that metadata to new documents automatically.</li>

View File

@ -159,7 +159,7 @@ export class DocumentDetailComponent implements OnInit {
delete() { delete() {
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'}) let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
modal.componentInstance.title = $localize`Confirm delete` modal.componentInstance.title = $localize`Confirm delete`
modal.componentInstance.messageBold = $localize`Do you really want to delete document '${this.document.title}'?` modal.componentInstance.messageBold = $localize`Do you really want to delete document "${this.document.title}"?`
modal.componentInstance.message = $localize`The files for this document will be deleted permanently. This operation cannot be undone.` modal.componentInstance.message = $localize`The files for this document will be deleted permanently. This operation cannot be undone.`
modal.componentInstance.btnClass = "btn-danger" modal.componentInstance.btnClass = "btn-danger"
modal.componentInstance.btnCaption = $localize`Delete document` modal.componentInstance.btnCaption = $localize`Delete document`

View File

@ -115,16 +115,16 @@ export class BulkEditorComponent {
modal.componentInstance.title = $localize`Confirm tags assignment` modal.componentInstance.title = $localize`Confirm tags assignment`
if (changedTags.itemsToAdd.length == 1 && changedTags.itemsToRemove.length == 0) { if (changedTags.itemsToAdd.length == 1 && changedTags.itemsToRemove.length == 0) {
let tag = changedTags.itemsToAdd[0] let tag = changedTags.itemsToAdd[0]
modal.componentInstance.message = $localize`This operation will add the tag "${tag.name}" to all ${this.list.selected.size} selected document(s).` modal.componentInstance.message = $localize`This operation will add the tag "${tag.name}" to ${this.list.selected.size} selected document(s).`
} else if (changedTags.itemsToAdd.length > 1 && changedTags.itemsToRemove.length == 0) { } else if (changedTags.itemsToAdd.length > 1 && changedTags.itemsToRemove.length == 0) {
modal.componentInstance.message = $localize`This operation will add the tags ${this._localizeList(changedTags.itemsToAdd)} to all ${this.list.selected.size} selected document(s).` modal.componentInstance.message = $localize`This operation will add the tags ${this._localizeList(changedTags.itemsToAdd)} to ${this.list.selected.size} selected document(s).`
} else if (changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length == 1) { } else if (changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length == 1) {
let tag = changedTags.itemsToRemove[0] let tag = changedTags.itemsToRemove[0]
modal.componentInstance.message = $localize`This operation will remove the tag "${tag.name}" from all ${this.list.selected.size} selected document(s).` modal.componentInstance.message = $localize`This operation will remove the tag "${tag.name}" from ${this.list.selected.size} selected document(s).`
} else if (changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length > 1) { } else if (changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length > 1) {
modal.componentInstance.message = $localize`This operation will remove the tags ${this._localizeList(changedTags.itemsToRemove)} from all ${this.list.selected.size} selected document(s).` modal.componentInstance.message = $localize`This operation will remove the tags ${this._localizeList(changedTags.itemsToRemove)} from ${this.list.selected.size} selected document(s).`
} else { } else {
modal.componentInstance.message = $localize`This operation will add the tags ${this._localizeList(changedTags.itemsToAdd)} and remove the tags ${this._localizeList(changedTags.itemsToRemove)} on all ${this.list.selected.size} selected document(s).` modal.componentInstance.message = $localize`This operation will add the tags ${this._localizeList(changedTags.itemsToAdd)} and remove the tags ${this._localizeList(changedTags.itemsToRemove)} on ${this.list.selected.size} selected document(s).`
} }
modal.componentInstance.btnClass = "btn-warning" modal.componentInstance.btnClass = "btn-warning"
@ -156,9 +156,9 @@ export class BulkEditorComponent {
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'}) let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
modal.componentInstance.title = $localize`Confirm correspondent assignment` modal.componentInstance.title = $localize`Confirm correspondent assignment`
if (correspondent) { if (correspondent) {
modal.componentInstance.message = $localize`This operation will assign the correspondent "${correspondent.name}" to all ${this.list.selected.size} selected document(s).` modal.componentInstance.message = $localize`This operation will assign the correspondent "${correspondent.name}" to ${this.list.selected.size} selected document(s).`
} else { } else {
modal.componentInstance.message = $localize`This operation will remove the correspondent from all ${this.list.selected.size} selected document(s).` modal.componentInstance.message = $localize`This operation will remove the correspondent from ${this.list.selected.size} selected document(s).`
} }
modal.componentInstance.btnClass = "btn-warning" modal.componentInstance.btnClass = "btn-warning"
modal.componentInstance.btnCaption = $localize`Confirm` modal.componentInstance.btnCaption = $localize`Confirm`
@ -189,9 +189,9 @@ export class BulkEditorComponent {
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'}) let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
modal.componentInstance.title = $localize`Confirm document type assignment` modal.componentInstance.title = $localize`Confirm document type assignment`
if (documentType) { if (documentType) {
modal.componentInstance.message = $localize`This operation will assign the document type "${documentType.name}" to all ${this.list.selected.size} selected document(s).` modal.componentInstance.message = $localize`This operation will assign the document type "${documentType.name}" to ${this.list.selected.size} selected document(s).`
} else { } else {
modal.componentInstance.message = $localize`This operation will remove the document type from all ${this.list.selected.size} selected document(s).` modal.componentInstance.message = $localize`This operation will remove the document type from ${this.list.selected.size} selected document(s).`
} }
modal.componentInstance.btnClass = "btn-warning" modal.componentInstance.btnClass = "btn-warning"
modal.componentInstance.btnCaption = $localize`Confirm` modal.componentInstance.btnCaption = $localize`Confirm`
@ -217,7 +217,7 @@ export class BulkEditorComponent {
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'}) let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
modal.componentInstance.delayConfirm(5) modal.componentInstance.delayConfirm(5)
modal.componentInstance.title = $localize`Delete confirm` modal.componentInstance.title = $localize`Delete confirm`
modal.componentInstance.messageBold = $localize`This operation will permanently delete all ${this.list.selected.size} selected document(s).` modal.componentInstance.messageBold = $localize`This operation will permanently delete ${this.list.selected.size} selected document(s).`
modal.componentInstance.message = $localize`This operation cannot be undone.` modal.componentInstance.message = $localize`This operation cannot be undone.`
modal.componentInstance.btnClass = "btn-danger" modal.componentInstance.btnClass = "btn-danger"
modal.componentInstance.btnCaption = $localize`Delete document(s)` modal.componentInstance.btnCaption = $localize`Delete document(s)`

View File

@ -1,7 +1,7 @@
<div class="card mb-3 bg-light shadow-sm" [class.card-selected]="selected" [class.document-card]="selectable"> <div class="card mb-3 shadow-sm" [class.card-selected]="selected" [class.document-card]="selectable">
<div class="row no-gutters"> <div class="row no-gutters">
<div class="col-md-2 d-none d-lg-block doc-img-background" [class.doc-img-background-selected]="selected"> <div class="col-md-2 d-none d-lg-block doc-img-background rounded-left" [class.doc-img-background-selected]="selected">
<img [src]="getThumbUrl()" class="card-img doc-img border-right" (click)="setSelected(selectable ? !selected : false)"> <img [src]="getThumbUrl()" class="card-img doc-img border-right rounded-left" (click)="setSelected(selectable ? !selected : false)">
<div style="top: 0; left: 0" class="position-absolute border-right border-bottom bg-light p-1" [class.document-card-check]="!selected"> <div style="top: 0; left: 0" class="position-absolute border-right border-bottom bg-light p-1" [class.document-card-check]="!selected">
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">
@ -12,7 +12,7 @@
</div> </div>
<div class="col"> <div class="col">
<div class="card-body"> <div class="card-body bg-light">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<h5 class="card-title"> <h5 class="card-title">

View File

@ -30,10 +30,6 @@
border-color: $primary; border-color: $primary;
} }
.doc-img-background {
background-color: white;
}
.doc-img-background-selected { .doc-img-background-selected {
background-color: $primaryFaded; background-color: $primaryFaded;
} }

View File

@ -1,7 +1,7 @@
<div class="col p-2 h-100"> <div class="col p-2 h-100">
<div class="card h-100 shadow-sm" [class.card-selected]="selected"> <div class="card h-100 shadow-sm document-card" [class.card-selected]="selected">
<div class="border-bottom" [class.doc-img-background-selected]="selected"> <div class="border-bottom doc-img-container" [class.doc-img-background-selected]="selected">
<img class="card-img doc-img" [src]="getThumbUrl()" (click)="setSelected(!selected)"> <img class="card-img doc-img rounded-top" [src]="getThumbUrl()" (click)="setSelected(!selected)">
<div class="border-right border-bottom bg-light p-1 rounded document-card-check"> <div class="border-right border-bottom bg-light p-1 rounded document-card-check">
<div class="custom-control custom-checkbox"> <div class="custom-control custom-checkbox">

View File

@ -78,8 +78,8 @@
</app-page-header> </app-page-header>
<div class="w-100 mb-2 mb-sm-4"> <div class="w-100 mb-2 mb-sm-4">
<app-filter-editor *ngIf="!isBulkEditing" [(filterRules)]="list.filterRules" #filterEditor></app-filter-editor> <app-filter-editor [hidden]="isBulkEditing" [(filterRules)]="list.filterRules" #filterEditor></app-filter-editor>
<app-bulk-editor *ngIf="isBulkEditing"></app-bulk-editor> <app-bulk-editor [hidden]="!isBulkEditing"></app-bulk-editor>
</div> </div>
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">

View File

@ -1,5 +1,4 @@
import { Component, OnInit } from '@angular/core'; import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { FILTER_CORRESPONDENT } from 'src/app/data/filter-rule-type'; import { FILTER_CORRESPONDENT } from 'src/app/data/filter-rule-type';
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'; import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent';
@ -16,7 +15,6 @@ import { CorrespondentEditDialogComponent } from './correspondent-edit-dialog/co
export class CorrespondentListComponent extends GenericListComponent<PaperlessCorrespondent> { export class CorrespondentListComponent extends GenericListComponent<PaperlessCorrespondent> {
constructor(correspondentsService: CorrespondentService, modalService: NgbModal, constructor(correspondentsService: CorrespondentService, modalService: NgbModal,
private router: Router,
private list: DocumentListViewService private list: DocumentListViewService
) { ) {
super(correspondentsService,modalService,CorrespondentEditDialogComponent) super(correspondentsService,modalService,CorrespondentEditDialogComponent)
@ -27,9 +25,6 @@ export class CorrespondentListComponent extends GenericListComponent<PaperlessCo
} }
filterDocuments(object: PaperlessCorrespondent) { filterDocuments(object: PaperlessCorrespondent) {
this.list.documentListView.filter_rules = [ this.list.quickFilter([{rule_type: FILTER_CORRESPONDENT, value: object.id.toString()}])
{rule_type: FILTER_CORRESPONDENT, value: object.id.toString()}
]
this.router.navigate(["documents"])
} }
} }

View File

@ -1,5 +1,4 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { FILTER_DOCUMENT_TYPE } from 'src/app/data/filter-rule-type'; import { FILTER_DOCUMENT_TYPE } from 'src/app/data/filter-rule-type';
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'; import { PaperlessDocumentType } from 'src/app/data/paperless-document-type';
@ -16,7 +15,6 @@ import { DocumentTypeEditDialogComponent } from './document-type-edit-dialog/doc
export class DocumentTypeListComponent extends GenericListComponent<PaperlessDocumentType> { export class DocumentTypeListComponent extends GenericListComponent<PaperlessDocumentType> {
constructor(service: DocumentTypeService, modalService: NgbModal, constructor(service: DocumentTypeService, modalService: NgbModal,
private router: Router,
private list: DocumentListViewService private list: DocumentListViewService
) { ) {
super(service, modalService, DocumentTypeEditDialogComponent) super(service, modalService, DocumentTypeEditDialogComponent)
@ -28,9 +26,6 @@ export class DocumentTypeListComponent extends GenericListComponent<PaperlessDoc
filterDocuments(object: PaperlessDocumentType) { filterDocuments(object: PaperlessDocumentType) {
this.list.documentListView.filter_rules = [ this.list.quickFilter([{rule_type: FILTER_DOCUMENT_TYPE, value: object.id.toString()}])
{rule_type: FILTER_DOCUMENT_TYPE, value: object.id.toString()}
]
this.router.navigate(["documents"])
} }
} }

View File

@ -10,7 +10,7 @@
<a ngbNavLink i18n>General settings</a> <a ngbNavLink i18n>General settings</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
<h4 i18n>Document list</h4> <h4 i18n>Appearance</h4>
<div class="form-row form-group"> <div class="form-row form-group">
<div class="col-md-3 col-form-label"> <div class="col-md-3 col-form-label">
@ -26,14 +26,29 @@
</select> </select>
</div> </div>
</div> </div>
<h4 i18n>Bulk editing</h4> <div class="form-row form-group">
<div class="col-md-3 col-form-label">
<span i18n>Dark mode</span>
</div>
<div class="col">
<app-input-check i18n-title title="Use system settings" formControlName="darkModeUseSystem" (change)="toggleDarkModeSetting()"></app-input-check>
<div class="custom-control custom-switch" *ngIf="!settingsForm.value.darkModeUseSystem">
<input type="checkbox" class="custom-control-input" id="darkModeEnabled" formControlName="darkModeEnabled" [checked]="settingsForm.value.darkModeEnabled">
<label class="custom-control-label" for="darkModeEnabled">Enabled</label>
</div>
</div>
</div>
<app-input-check i18n-title title="Show confirmation dialogs" formControlName="bulkEditConfirmationDialogs" i18n-hint hint="Deleting documents will always ask for confirmation."></app-input-check> <h4 class="mt-4" i18n>Bulk editing</h4>
<app-input-check i18n-title title="Apply on close" formControlName="bulkEditApplyOnClose"></app-input-check>
<div class="form-row form-group">
<div class="offset-md-3 col">
<app-input-check i18n-title title="Show confirmation dialogs" formControlName="bulkEditConfirmationDialogs" i18n-hint hint="Deleting documents will always ask for confirmation."></app-input-check>
<app-input-check i18n-title title="Apply on close" formControlName="bulkEditApplyOnClose"></app-input-check>
</div>
</div>
</ng-template> </ng-template>
</li> </li>

View File

@ -1,10 +1,11 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit, Renderer2 } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms'; import { FormControl, FormGroup } from '@angular/forms';
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'; import { PaperlessSavedView } from 'src/app/data/paperless-saved-view';
import { DocumentListViewService } from 'src/app/services/document-list-view.service'; import { DocumentListViewService } from 'src/app/services/document-list-view.service';
import { SavedViewService } from 'src/app/services/rest/saved-view.service'; import { SavedViewService } from 'src/app/services/rest/saved-view.service';
import { SettingsService, SETTINGS_KEYS } from 'src/app/services/settings.service'; import { SettingsService, SETTINGS_KEYS } from 'src/app/services/settings.service';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
import { AppViewService } from 'src/app/services/app-view.service';
@Component({ @Component({
selector: 'app-settings', selector: 'app-settings',
@ -19,18 +20,21 @@ export class SettingsComponent implements OnInit {
'bulkEditConfirmationDialogs': new FormControl(this.settings.get(SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS)), 'bulkEditConfirmationDialogs': new FormControl(this.settings.get(SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS)),
'bulkEditApplyOnClose': new FormControl(this.settings.get(SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE)), 'bulkEditApplyOnClose': new FormControl(this.settings.get(SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE)),
'documentListItemPerPage': new FormControl(this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)), 'documentListItemPerPage': new FormControl(this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)),
'darkModeUseSystem': new FormControl(this.settings.get(SETTINGS_KEYS.DARK_MODE_USE_SYSTEM)),
'darkModeEnabled': new FormControl(this.settings.get(SETTINGS_KEYS.DARK_MODE_ENABLED)),
'savedViews': this.savedViewGroup 'savedViews': this.savedViewGroup
}) })
savedViews: PaperlessSavedView[]
constructor( constructor(
public savedViewService: SavedViewService, public savedViewService: SavedViewService,
private documentListViewService: DocumentListViewService, private documentListViewService: DocumentListViewService,
private toastService: ToastService, private toastService: ToastService,
private settings: SettingsService private settings: SettingsService,
private appViewService: AppViewService
) { } ) { }
savedViews: PaperlessSavedView[]
ngOnInit() { ngOnInit() {
this.savedViewService.listAll().subscribe(r => { this.savedViewService.listAll().subscribe(r => {
this.savedViews = r.results this.savedViews = r.results
@ -53,11 +57,22 @@ export class SettingsComponent implements OnInit {
}) })
} }
toggleDarkModeSetting() {
if (this.settingsForm.value.darkModeUseSystem) {
(this.settingsForm.controls.darkModeEnabled as FormControl).disable()
} else {
(this.settingsForm.controls.darkModeEnabled as FormControl).enable()
}
}
private saveLocalSettings() { private saveLocalSettings() {
this.settings.set(SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE, this.settingsForm.value.bulkEditApplyOnClose) this.settings.set(SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE, this.settingsForm.value.bulkEditApplyOnClose)
this.settings.set(SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS, this.settingsForm.value.bulkEditConfirmationDialogs) this.settings.set(SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS, this.settingsForm.value.bulkEditConfirmationDialogs)
this.settings.set(SETTINGS_KEYS.DOCUMENT_LIST_SIZE, this.settingsForm.value.documentListItemPerPage) this.settings.set(SETTINGS_KEYS.DOCUMENT_LIST_SIZE, this.settingsForm.value.documentListItemPerPage)
this.settings.set(SETTINGS_KEYS.DARK_MODE_USE_SYSTEM, this.settingsForm.value.darkModeUseSystem)
this.settings.set(SETTINGS_KEYS.DARK_MODE_ENABLED, (this.settingsForm.value.darkModeEnabled == true).toString())
this.documentListViewService.updatePageSize() this.documentListViewService.updatePageSize()
this.appViewService.updateDarkModeSettings()
this.toastService.showInfo($localize`Settings saved successfully.`) this.toastService.showInfo($localize`Settings saved successfully.`)
} }

View File

@ -1,5 +1,4 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { FILTER_HAS_TAG } from 'src/app/data/filter-rule-type'; import { FILTER_HAS_TAG } from 'src/app/data/filter-rule-type';
import { TAG_COLOURS, PaperlessTag } from 'src/app/data/paperless-tag'; import { TAG_COLOURS, PaperlessTag } from 'src/app/data/paperless-tag';
@ -16,7 +15,6 @@ import { TagEditDialogComponent } from './tag-edit-dialog/tag-edit-dialog.compon
export class TagListComponent extends GenericListComponent<PaperlessTag> { export class TagListComponent extends GenericListComponent<PaperlessTag> {
constructor(tagService: TagService, modalService: NgbModal, constructor(tagService: TagService, modalService: NgbModal,
private router: Router,
private list: DocumentListViewService private list: DocumentListViewService
) { ) {
super(tagService, modalService, TagEditDialogComponent) super(tagService, modalService, TagEditDialogComponent)
@ -27,14 +25,11 @@ export class TagListComponent extends GenericListComponent<PaperlessTag> {
} }
getDeleteMessage(object: PaperlessTag) { getDeleteMessage(object: PaperlessTag) {
return $localize`Do you really want to delete the tag "${object.name}"?` return $localize`Do you really want to delete the tag "${object.name}"?`
} }
filterDocuments(object: PaperlessTag) { filterDocuments(object: PaperlessTag) {
this.list.documentListView.filter_rules = [ this.list.quickFilter([{rule_type: FILTER_HAS_TAG, value: object.id.toString()}])
{rule_type: FILTER_HAS_TAG, value: object.id.toString()}
]
this.router.navigate(["documents"])
} }
} }

View File

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { AppViewService } from './app-view.service';
describe('AppViewService', () => {
let service: AppViewService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(AppViewService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,35 @@
import { Inject, Injectable, Renderer2, RendererFactory2 } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { SettingsService, SETTINGS_KEYS } from './settings.service';
@Injectable({
providedIn: 'root'
})
export class AppViewService {
private renderer: Renderer2;
constructor(
private settings: SettingsService,
private rendererFactory: RendererFactory2,
@Inject(DOCUMENT) private document
) {
this.renderer = rendererFactory.createRenderer(null, null);
this.updateDarkModeSettings()
}
updateDarkModeSettings() {
let darkModeUseSystem = this.settings.get(SETTINGS_KEYS.DARK_MODE_USE_SYSTEM)
let darkModeEnabled = this.settings.get(SETTINGS_KEYS.DARK_MODE_ENABLED)
if (darkModeUseSystem) {
this.renderer.addClass(this.document.body, 'color-scheme-system')
this.renderer.removeClass(this.document.body, 'color-scheme-dark')
} else {
this.renderer.removeClass(this.document.body, 'color-scheme-system')
darkModeEnabled ? this.renderer.addClass(this.document.body, 'color-scheme-dark') : this.renderer.removeClass(this.document.body, 'color-scheme-dark')
}
}
}

View File

@ -1,4 +1,5 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { cloneFilterRules, FilterRule } from '../data/filter-rule'; import { cloneFilterRules, FilterRule } from '../data/filter-rule';
import { PaperlessDocument } from '../data/paperless-document'; import { PaperlessDocument } from '../data/paperless-document';
@ -155,6 +156,14 @@ export class DocumentListViewService {
sessionStorage.setItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG, JSON.stringify(this.documentListView)) sessionStorage.setItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG, JSON.stringify(this.documentListView))
} }
quickFilter(filterRules: FilterRule[]) {
this.savedView = null
this.view.filter_rules = filterRules
this.reduceSelectionToFilter()
this.saveDocumentListView()
this.router.navigate(["documents"])
}
getLastPage(): number { getLastPage(): number {
return Math.ceil(this.collectionSize / this.currentPageSize) return Math.ceil(this.collectionSize / this.currentPageSize)
} }
@ -240,7 +249,7 @@ export class DocumentListViewService {
} }
} }
constructor(private documentService: DocumentService, private settings: SettingsService) { constructor(private documentService: DocumentService, private settings: SettingsService, private router: Router) {
let documentListViewConfigJson = sessionStorage.getItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG) let documentListViewConfigJson = sessionStorage.getItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG)
if (documentListViewConfigJson) { if (documentListViewConfigJson) {
try { try {

View File

@ -10,12 +10,16 @@ export const SETTINGS_KEYS = {
BULK_EDIT_CONFIRMATION_DIALOGS: 'general-settings:bulk-edit:confirmation-dialogs', BULK_EDIT_CONFIRMATION_DIALOGS: 'general-settings:bulk-edit:confirmation-dialogs',
BULK_EDIT_APPLY_ON_CLOSE: 'general-settings:bulk-edit:apply-on-close', BULK_EDIT_APPLY_ON_CLOSE: 'general-settings:bulk-edit:apply-on-close',
DOCUMENT_LIST_SIZE: 'general-settings:documentListSize', DOCUMENT_LIST_SIZE: 'general-settings:documentListSize',
DARK_MODE_USE_SYSTEM: 'general-settings:dark-mode:use-system',
DARK_MODE_ENABLED: 'general-settings:dark-mode:enabled'
} }
const SETTINGS: PaperlessSettings[] = [ const SETTINGS: PaperlessSettings[] = [
{key: SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS, type: "boolean", default: true}, {key: SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS, type: "boolean", default: true},
{key: SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE, type: "boolean", default: false}, {key: SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE, type: "boolean", default: false},
{key: SETTINGS_KEYS.DOCUMENT_LIST_SIZE, type: "number", default: 50} {key: SETTINGS_KEYS.DOCUMENT_LIST_SIZE, type: "number", default: 50},
{key: SETTINGS_KEYS.DARK_MODE_USE_SYSTEM, type: "boolean", default: true},
{key: SETTINGS_KEYS.DARK_MODE_ENABLED, type: "boolean", default: false}
] ]
@Injectable({ @Injectable({

View File

@ -1,69 +1,19 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="utf-8"?>
<svg <!-- Generator: Adobe Illustrator 25.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
xmlns:dc="http://purl.org/dc/elements/1.1/" <svg version="1.1"
xmlns:cc="http://creativecommons.org/ns#" id="svg4812" inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)" sodipodi:docname="logo-dark-notext.svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 198.4 238.9"
xmlns:svg="http://www.w3.org/2000/svg" style="enable-background:new 0 0 198.4 238.9;" xml:space="preserve">
xmlns="http://www.w3.org/2000/svg" <sodipodi:namedview bordercolor="#666666" borderopacity="1.0" id="base" inkscape:current-layer="SvgjsG1020" inkscape:cx="328.04904" inkscape:cy="330.33332" inkscape:document-rotation="0" inkscape:document-units="mm" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:window-height="1016" inkscape:window-maximized="1" inkscape:window-width="1920" inkscape:window-x="1280" inkscape:window-y="27" inkscape:zoom="0.98994949" pagecolor="#ffffff" showgrid="false">
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" </sodipodi:namedview>
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" <g id="layer1" transform="translate(-9.9999792,-10.000082)" inkscape:groupmode="layer" inkscape:label="Layer 1">
width="69.999977mm" <g id="SvgjsG1020" transform="matrix(0.10341565,0,0,0.10341565,1.2287665,8.3453496)">
height="84.283669mm" <path id="path57" d="M1967.5,16C1672.7,702,255.4,787,709,1892.5c5.7,14.2-104.9,164.4-178.6,289.1c-17-62.4-36.9-130.4-34-136.1
viewBox="0 0 69.999977 84.283669" c368.5-436.5-263.6-683.1-297.6-1040.3C40,1288.7-16.7,1784.8,462.3,2071.1c2.8,0,25.5,107.7,36.9,161.6
version="1.1" c-11.3,22.7-22.7,45.4-28.3,62.4c-11.3,28.3,73.7,25.5,73.7,31.2c8.5-2.8,209.8-357.2,215.4-360
id="svg4812" C1899.5,1705.4,2100.8,679.3,1967.5,16z M1386.4,738.8C853.5,1215,762.8,1569.4,779.8,1742.3
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)" C601.2,1319.9,1125.7,855,1386.4,738.8z M357.5,1419.1c102,93.5,272.1,379.8,127.6,547.1C519,1889.7,530.4,1716.8,357.5,1419.1z"
sodipodi:docname="logo-dark-notext.svg"> />
<defs </g>
id="defs4806" /> </g>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.98994949"
inkscape:cx="328.04904"
inkscape:cy="330.33332"
inkscape:document-units="mm"
inkscape:current-layer="SvgjsG1020"
inkscape:document-rotation="0"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="1280"
inkscape:window-y="27"
inkscape:window-maximized="1" />
<metadata
id="metadata4809">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-9.9999792,-10.000082)">
<g
id="SvgjsG1020"
featureKey="symbol1"
fill="#ffffff"
transform="matrix(0.10341565,0,0,0.10341565,1.2287665,8.3453496)">
<path
id="path57"
style="fill:#ffffff;stroke-width:1.10017"
d="M 752.4375,82.365234 C 638.02019,348.60552 87.938206,381.6089 263.96484,810.67383 c 2.20034,5.50083 -40.70621,63.80947 -69.31054,112.21679 -6.601,-24.20366 -14.30329,-50.6063 -13.20313,-52.80664 C 324.47281,700.65835 79.135592,604.94324 65.933594,466.32227 4.3242706,576.33891 -17.678136,768.86756 168.25,879.98438 c 1.10017,-10e-6 9.90207,41.80777 14.30273,62.71093 -4.40066,8.80133 -8.80162,17.60213 -11.00195,24.20313 -4.40066,11.00166 28.60352,9.90123 28.60352,12.10156 3.3005,-1.10017 81.41295,-138.62054 83.61328,-139.7207 C 726.0345,738.06398 804.14532,339.80419 752.4375,82.365234 Z M 526.9043,362.90625 C 320.073,547.73422 284.86775,685.25508 291.46875,752.36523 222.15826,588.44043 425.68898,408.01308 526.9043,362.90625 Z M 127.54297,626.94727 c 39.60599,36.30549 105.6163,147.4222 49.50781,212.33203 13.202,-29.7045 17.60234,-96.81455 -49.50781,-212.33203 z"
transform="matrix(0.90895334,0,0,0.90895334,65.06894,-58.865357)" />
<defs
id="defs14302" />
</g>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="69.999977mm"
height="84.283669mm"
viewBox="0 0 69.999977 84.283669"
version="1.1"
id="svg4812"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)"
sodipodi:docname="logo-dark-notext.svg">
<defs
id="defs4806" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.98994949"
inkscape:cx="328.04904"
inkscape:cy="330.33332"
inkscape:document-units="mm"
inkscape:current-layer="SvgjsG1020"
inkscape:document-rotation="0"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="1280"
inkscape:window-y="27"
inkscape:window-maximized="1" />
<metadata
id="metadata4809">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-9.9999792,-10.000082)">
<g
id="SvgjsG1020"
featureKey="symbol1"
fill="#ffffff"
transform="matrix(0.10341565,0,0,0.10341565,1.2287665,8.3453496)">
<path
id="path57"
style="fill:#ffffff;stroke-width:1.10017"
d="M 752.4375,82.365234 C 638.02019,348.60552 87.938206,381.6089 263.96484,810.67383 c 2.20034,5.50083 -40.70621,63.80947 -69.31054,112.21679 -6.601,-24.20366 -14.30329,-50.6063 -13.20313,-52.80664 C 324.47281,700.65835 79.135592,604.94324 65.933594,466.32227 4.3242706,576.33891 -17.678136,768.86756 168.25,879.98438 c 1.10017,-10e-6 9.90207,41.80777 14.30273,62.71093 -4.40066,8.80133 -8.80162,17.60213 -11.00195,24.20313 -4.40066,11.00166 28.60352,9.90123 28.60352,12.10156 3.3005,-1.10017 81.41295,-138.62054 83.61328,-139.7207 C 726.0345,738.06398 804.14532,339.80419 752.4375,82.365234 Z M 526.9043,362.90625 C 320.073,547.73422 284.86775,685.25508 291.46875,752.36523 222.15826,588.44043 425.68898,408.01308 526.9043,362.90625 Z M 127.54297,626.94727 c 39.60599,36.30549 105.6163,147.4222 49.50781,212.33203 13.202,-29.7045 17.60234,-96.81455 -49.50781,-212.33203 z"
transform="matrix(0.90895334,0,0,0.90895334,65.06894,-58.865357)" />
<defs
id="defs14302" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -2,5 +2,5 @@ export const environment = {
production: true, production: true,
apiBaseUrl: "/api/", apiBaseUrl: "/api/",
appTitle: "Paperless-ng", appTitle: "Paperless-ng",
version: "0.9.10" version: "0.9.11"
}; };

View File

@ -4,7 +4,7 @@
export const environment = { export const environment = {
production: false, production: false,
apiBaseUrl: "http://localhost:8000/api/", apiBaseUrl: "http://10.0.1.26:8000/api/",
appTitle: "Paperless-ng", appTitle: "Paperless-ng",
version: "DEVELOPMENT" version: "DEVELOPMENT"
}; };

View File

@ -5,9 +5,12 @@
<title>Paperless-ng</title> <title>Paperless-ng</title>
<base href="/"> <base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="color-scheme" content="dark light">
<meta name="theme-color" content="#17541f" />
<link rel="manifest" href="manifest.webmanifest">
<link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="icon" type="image/x-icon" href="favicon.ico">
</head> </head>
<body> <body class="color-scheme-system">
<app-root></app-root> <app-root></app-root>
</body> </body>
</html> </html>

View File

@ -0,0 +1,14 @@
{
"background_color": "white",
"description": "A supercharged version of paperless: scan, index and archive all your physical documents",
"display": "fullscreen",
"icons": [
{
"src": "favicon.ico",
"sizes": "128x128"
}
],
"name": "Paperless NG",
"short_name": "Paperless NG",
"start_url": "/"
}

View File

@ -1,4 +1,5 @@
@import "theme"; @import "theme";
@import "theme_dark";
@import "node_modules/bootstrap/scss/bootstrap"; @import "node_modules/bootstrap/scss/bootstrap";
@import "~@ng-select/ng-select/themes/default.theme.css"; @import "~@ng-select/ng-select/themes/default.theme.css";

329
src-ui/src/theme_dark.scss Normal file
View File

@ -0,0 +1,329 @@
$primary-dark-mode: #346e2c;
$danger-dark-mode: #9c142a;
$bg-dark-mode: #161618;
$bg-light-dark-mode: #1c1c1f;
$text-color-dark-mode: #abb2bf;
$text-color-dark-mode-accent: lighten($text-color-dark-mode, 10%);
$border-color-dark-mode: #47494f;
* {
transition: background-color 0.3s ease, border-color 0.3s ease;
}
@mixin dark-mode {
background-color: $bg-dark-mode !important;
color: $text-color-dark-mode;
.navbar-brand {
color: $text-color-dark-mode;
}
svg.logo {
.leaf {
color: $primary-dark-mode !important;
}
.text {
fill: $text-color-dark-mode !important;
}
}
.bg-light {
background-color: $bg-light-dark-mode !important;
a,
div {
color: $text-color-dark-mode;
}
}
.border {
border-color: $border-color-dark-mode !important;
}
.border-right {
border-right: 1px solid $border-color-dark-mode !important;
}
.border-left {
border-left: 1px solid $border-color-dark-mode !important;
}
.border-bottom {
border-bottom: 1px solid $border-color-dark-mode !important;
}
.nav-link {
color: $text-color-dark-mode !important;
&.active {
background-color: $bg-dark-mode;
color: $text-color-dark-mode;
border-color: $border-color-dark-mode $border-color-dark-mode $bg-dark-mode;
}
&:hover {
color: $text-color-dark-mode-accent !important;
border-color: $border-color-dark-mode $border-color-dark-mode $bg-dark-mode;
}
}
.nav-tabs {
border-color: $border-color-dark-mode;
.nav-link {
color: $primary-dark-mode !important;
&.active {
color: $text-color-dark-mode !important;
}
}
}
.dropdown-menu {
background-color: $bg-dark-mode;
.dropdown-divider {
border-color: $border-color-dark-mode;
}
.dropdown-item {
color: $text-color-dark-mode;
&:hover {
background-color: $bg-light-dark-mode;
color: $text-color-dark-mode;
}
}
.dropdown-item.disabled {
color: darken($text-color-dark-mode, 20%);
}
}
.card {
background-color: $bg-light-dark-mode;
.card-text {
color: $text-color-dark-mode;
}
}
.text-dark {
color: $text-color-dark-mode !important;
}
.modal-content, .modal-header, .modal-body, .modal-footer {
background-color: $bg-light-dark-mode;
border-color: $border-color-dark-mode;
}
app-tag .badge {
filter: brightness(.8);
}
.badge-light {
background-color: darken($bg-dark-mode, 20%);
color: $text-color-dark-mode-accent;
}
.doc-img-container {
border: none !important;
border-top-left-radius: .25rem;
border-top-right-radius: .25rem;
overflow: hidden;
}
.doc-img {
mix-blend-mode: normal;
filter: invert(95%) hue-rotate(180deg);
border-radius: 0;
border-color: $bg-dark-mode;
&.border-right {
border-right: none !important;
}
}
.card-selected .doc-img {
mix-blend-mode: luminosity;
}
.toast {
background-color: opacify($bg-light-dark-mode, .85);
}
.toast-header {
background-color: opacify($bg-dark-mode, .85);
}
a {
color: $primary-dark-mode;
&:hover {
color: lighten($primary, 10%);
}
}
table {
background-color: $bg-light-dark-mode;
color: $text-color-dark-mode;
border-color: $border-color-dark-mode;
tr:hover {
background-color: $bg-light-dark-mode;
color: $text-color-dark-mode-accent;
}
}
.table td,
.table th {
border-color: $border-color-dark-mode;
}
.table-row-selected {
background-color: $bg-light-dark-mode;
}
.close {
color: $text-color-dark-mode;
text-shadow: 0 1px 0 #666;
}
.btn-outline-primary{
border-color: $primary-dark-mode;
color: $primary-dark-mode;
&:hover {
background-color: darken($primary-dark-mode, 10%);
color: $text-color-dark-mode-accent;
}
}
.btn-outline-secondary {
border-color: $text-color-dark-mode;
color: $text-color-dark-mode;
&:hover {
background-color: $bg-dark-mode;
}
}
.btn-outline-danger {
border-color: $danger-dark-mode;
color: $danger-dark-mode;
&:hover {
background-color: darken($danger-dark-mode, 10%);
color: $text-color-dark-mode-accent;
}
}
.btn-outline-dark {
border-color: $border-color-dark-mode;
color: $text-color-dark-mode;
&:hover {
color: $text-color-dark-mode-accent;
}
}
.btn-link:not(:disabled):not(.disabled) {
color: $primary-dark-mode;
}
.btn-link:hover,
.btn-outline-primary:not(:disabled):not(.disabled).active,
.btn-outline-primary:not(:disabled):not(.disabled):active,
.show > .btn-outline-primary.dropdown-toggle {
color: $text-color-dark-mode-accent;
}
button.bg-light:hover {
background-color: $bg-dark-mode !important;
}
.form-control,
input,
select,
textarea {
background-color: $bg-dark-mode;
color: $text-color-dark-mode;
border-color: $border-color-dark-mode;
&::placeholder {
color: $text-color-dark-mode;
}
&:focus {
background-color: $bg-light-dark-mode !important;
color: darken($text-color-dark-mode, 10%) !important;
}
}
.ng-select-container,
.ng-select.ng-select-opened > .ng-select-container,
.ng-dropdown-panel,
.ng-dropdown-panel .ng-dropdown-panel-items .ng-option {
background-color: $bg-dark-mode;
color: $text-color-dark-mode;
border-color: $border-color-dark-mode;
input:focus {
background-color: transparent !important;
}
}
.ng-dropdown-panel .ng-dropdown-panel-items .ng-option:hover {
background-color: $bg-light-dark-mode;
}
.custom-control-label:before {
background-color: $bg-dark-mode;
color: $text-color-dark-mode;
}
.custom-control-input:checked ~ .custom-control-label::before {
color: $text-color-dark-mode-accent;
}
.input-group-text {
color: $text-color-dark-mode;
background-color: $bg-light-dark-mode;
border-color: $border-color-dark-mode;
}
.list-group-item {
color: $text-color-dark-mode;
background-color: $bg-light-dark-mode;
border-color: $border-color-dark-mode;
}
.page-item.disabled .page-link {
background-color: $bg-dark-mode;
border-color: $border-color-dark-mode;
}
.list-group-item,
.page-link {
background-color: $bg-light-dark-mode;
border-color: $border-color-dark-mode;
}
.page-item.active .page-link {
border-color: $border-color-dark-mode;
color: $text-color-dark-mode-accent;
}
.progress {
background-color: $border-color-dark-mode;
}
}
body.color-scheme-dark {
@include dark-mode;
}
body.color-scheme-system {
@media (prefers-color-scheme: dark) {
@include dark-mode;
}
}

View File

@ -158,7 +158,7 @@ class Consumer(LoggingMixin):
try: try:
classifier = DocumentClassifier() classifier = DocumentClassifier()
classifier.reload() classifier.reload()
except (FileNotFoundError, IncompatibleClassifierVersionError) as e: except (OSError, EOFError, IncompatibleClassifierVersionError) as e:
self.log( self.log(
"warning", "warning",
f"Cannot classify documents: {e}.") f"Cannot classify documents: {e}.")

View File

@ -73,7 +73,7 @@ class Command(Renderable, BaseCommand):
classifier = DocumentClassifier() classifier = DocumentClassifier()
try: try:
classifier.reload() classifier.reload()
except (FileNotFoundError, IncompatibleClassifierVersionError) as e: except (OSError, EOFError, IncompatibleClassifierVersionError) as e:
logging.getLogger(__name__).warning( logging.getLogger(__name__).warning(
f"Cannot classify documents: {e}.") f"Cannot classify documents: {e}.")
classifier = None classifier = None

View File

@ -23,6 +23,7 @@ def _process_document(doc_in):
finally: finally:
parser.cleanup() parser.cleanup()
class Command(Renderable, BaseCommand): class Command(Renderable, BaseCommand):
help = """ help = """
@ -62,4 +63,6 @@ class Command(Renderable, BaseCommand):
db.connections.close_all() db.connections.close_all()
with multiprocessing.Pool() as pool: with multiprocessing.Pool() as pool:
list(tqdm.tqdm(pool.imap_unordered(_process_document, ids), total=len(ids))) list(tqdm.tqdm(
pool.imap_unordered(_process_document, ids), total=len(ids)
))

View File

@ -276,13 +276,6 @@ def update_filename_and_move_files(sender, instance, **kwargs):
Document.objects.filter(pk=instance.pk).update( Document.objects.filter(pk=instance.pk).update(
filename=new_filename) filename=new_filename)
logging.getLogger(__name__).debug(
f"Moved file {old_source_path} to {new_source_path}.")
if instance.archive_checksum:
logging.getLogger(__name__).debug(
f"Moved file {old_archive_path} to {new_archive_path}.")
except OSError as e: except OSError as e:
instance.filename = old_filename instance.filename = old_filename
# this happens when we can't move a file. If that's the case for # this happens when we can't move a file. If that's the case for

View File

@ -35,9 +35,9 @@ def train_classifier():
try: try:
# load the classifier, since we might not have to train it again. # load the classifier, since we might not have to train it again.
classifier.reload() classifier.reload()
except (FileNotFoundError, IncompatibleClassifierVersionError): except (OSError, EOFError, IncompatibleClassifierVersionError):
# This is what we're going to fix here. # This is what we're going to fix here.
pass classifier = DocumentClassifier()
try: try:
if classifier.train(): if classifier.train():
@ -94,7 +94,10 @@ def bulk_update_documents(document_ids):
documents = Document.objects.filter(id__in=document_ids) documents = Document.objects.filter(id__in=document_ids)
ix = index.open_index() ix = index.open_index()
for doc in documents:
post_save.send(Document, instance=doc, created=False)
with AsyncWriter(ix) as writer: with AsyncWriter(ix) as writer:
for doc in documents: for doc in documents:
index.update_document(writer, doc) index.update_document(writer, doc)
post_save.send(Document, instance=doc, created=False)

View File

@ -12,7 +12,9 @@
<meta name="full_name" content="{{full_name}}"> <meta name="full_name" content="{{full_name}}">
<meta name="cookie_prefix" content="{{cookie_prefix}}"> <meta name="cookie_prefix" content="{{cookie_prefix}}">
<link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="{% static 'frontend/styles.css' %}"></head> <link rel="manifest" href="{% static 'frontend/manifest.webmanifest' %}">
<link rel="stylesheet" href="{% static 'frontend/styles.css' %}">
</head>
<body> <body>
<app-root>Loading...</app-root> <app-root>Loading...</app-root>
<script src="{% static 'frontend/runtime.js' %}" defer></script> <script src="{% static 'frontend/runtime.js' %}" defer></script>

View File

@ -87,6 +87,7 @@ INSTALLED_APPS = [
"documents.apps.DocumentsConfig", "documents.apps.DocumentsConfig",
"paperless_tesseract.apps.PaperlessTesseractConfig", "paperless_tesseract.apps.PaperlessTesseractConfig",
"paperless_text.apps.PaperlessTextConfig", "paperless_text.apps.PaperlessTextConfig",
"paperless_tika.apps.PaperlessTikaConfig",
"paperless_mail.apps.PaperlessMailConfig", "paperless_mail.apps.PaperlessMailConfig",
"django.contrib.admin", "django.contrib.admin",
@ -424,3 +425,10 @@ for t in json.loads(os.getenv("PAPERLESS_FILENAME_PARSE_TRANSFORMS", "[]")):
PAPERLESS_FILENAME_FORMAT = os.getenv("PAPERLESS_FILENAME_FORMAT") PAPERLESS_FILENAME_FORMAT = os.getenv("PAPERLESS_FILENAME_FORMAT")
THUMBNAIL_FONT_NAME = os.getenv("PAPERLESS_THUMBNAIL_FONT_NAME", "/usr/share/fonts/liberation/LiberationSerif-Regular.ttf") THUMBNAIL_FONT_NAME = os.getenv("PAPERLESS_THUMBNAIL_FONT_NAME", "/usr/share/fonts/liberation/LiberationSerif-Regular.ttf")
# Tika settings
PAPERLESS_TIKA_ENABLED = __get_boolean("PAPERLESS_TIKA_ENABLED", "NO")
PAPERLESS_TIKA_ENDPOINT = os.getenv("PAPERLESS_TIKA_ENDPOINT", "http://localhost:9998")
PAPERLESS_TIKA_GOTENBERG_ENDPOINT = os.getenv(
"PAPERLESS_TIKA_GOTENBERG_ENDPOINT", "http://localhost:3000"
)

View File

@ -1 +1 @@
__version__ = (0, 9, 10) __version__ = (0, 9, 11)

View File

@ -0,0 +1,14 @@
from django.apps import AppConfig
from django.conf import settings
from paperless_tika.signals import tika_consumer_declaration
class PaperlessTikaConfig(AppConfig):
name = "paperless_tika"
def ready(self):
from documents.signals import document_consumer_declaration
if settings.PAPERLESS_TIKA_ENABLED:
document_consumer_declaration.connect(tika_consumer_declaration)
AppConfig.ready(self)

View File

@ -0,0 +1,115 @@
import os
import subprocess
import tika
import requests
import dateutil.parser
from PIL import ImageDraw, ImageFont, Image
from django.conf import settings
from documents.parsers import DocumentParser, ParseError, run_convert
from paperless_tesseract.parsers import RasterisedDocumentParser
from tika import parser
class TikaDocumentParser(DocumentParser):
"""
This parser sends documents to a local tika server
"""
def get_thumbnail(self, document_path, mime_type):
self.log("info", f"[TIKA_THUMB] Generating thumbnail for{document_path}")
archive_path = self.archive_path
out_path = os.path.join(self.tempdir, "convert.png")
# Run convert to get a decent thumbnail
try:
run_convert(
density=300,
scale="500x5000>",
alpha="remove",
strip=True,
trim=False,
input_file="{}[0]".format(archive_path),
output_file=out_path,
logging_group=self.logging_group,
)
except ParseError:
# if convert fails, fall back to extracting
# the first PDF page as a PNG using Ghostscript
self.log(
"warning",
"Thumbnail generation with ImageMagick failed, falling back "
"to ghostscript. Check your /etc/ImageMagick-x/policy.xml!",
)
gs_out_path = os.path.join(self.tempdir, "gs_out.png")
cmd = [
settings.GS_BINARY,
"-q",
"-sDEVICE=pngalpha",
"-o",
gs_out_path,
archive_path,
]
if not subprocess.Popen(cmd).wait() == 0:
raise ParseError("Thumbnail (gs) failed at {}".format(cmd))
# then run convert on the output from gs
run_convert(
density=300,
scale="500x5000>",
alpha="remove",
strip=True,
trim=False,
input_file=gs_out_path,
output_file=out_path,
logging_group=self.logging_group,
)
return out_path
def parse(self, document_path, mime_type):
self.log("info", f"[TIKA_PARSE] Sending {document_path} to Tika server")
tika_server = settings.PAPERLESS_TIKA_ENDPOINT
try:
parsed = parser.from_file(document_path, tika_server)
except requests.exceptions.HTTPError as err:
raise ParseError(
f"Could not parse {document_path} with tika server at {tika_server}: {err}"
)
try:
self.text = parsed["content"].strip()
except:
pass
try:
self.date = dateutil.parser.isoparse(parsed["metadata"]["Creation-Date"])
except:
pass
archive_path = os.path.join(self.tempdir, "convert.pdf")
convert_to_pdf(document_path, archive_path)
self.archive_path = archive_path
def convert_to_pdf(document_path, pdf_path):
pdf_path = os.path.join(self.tempdir, "convert.pdf")
gotenberg_server = settings.PAPERLESS_TIKA_GOTENBERG_ENDPOINT
url = gotenberg_server + "/convert/office"
self.log("info", f"[TIKA] Converting {document_path} to PDF as {pdf_path}")
files = {"files": open(document_path, "rb")}
headers = {}
try:
response = requests.post(url, files=files, headers=headers)
response.raise_for_status() # ensure we notice bad responses
except requests.exceptions.HTTPError as err:
raise ParseError(
f"Could not contact gotenberg server at {gotenberg_server}: {err}"
)
file = open(pdf_path, "wb")
file.write(response.content)
file.close()

View File

@ -0,0 +1,20 @@
from .parsers import TikaDocumentParser
def tika_consumer_declaration(sender, **kwargs):
return {
"parser": TikaDocumentParser,
"weight": 10,
"mime_types": {
"application/msword": ".doc",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
"application/vnd.ms-excel": ".xls",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
"application/vnd.ms-powerpoint": ".ppt",
"application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx",
"application/vnd.openxmlformats-officedocument.presentationml.slideshow": ".ppsx",
"application/vnd.oasis.opendocument.presentation": ".odp",
"application/vnd.oasis.opendocument.spreadsheet": ".ods",
"application/vnd.oasis.opendocument.text": ".odt",
},
}