diff --git a/Pipfile b/Pipfile index 48759307c..f6301b98f 100644 --- a/Pipfile +++ b/Pipfile @@ -42,6 +42,7 @@ whoosh="~=2.7.4" inotifyrecursive = "~=0.3.4" ocrmypdf = "*" tqdm = "*" +tika = "*" [dev-packages] coveralls = "*" diff --git a/README.md b/README.md index eea41ce05..5c5fa4a76 100644 --- a/README.md +++ b/README.md @@ -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) [![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) diff --git a/docker/gunicorn.conf.py b/docker/gunicorn.conf.py index 5c9c0eb37..edfb362d9 100644 --- a/docker/gunicorn.conf.py +++ b/docker/gunicorn.conf.py @@ -1,4 +1,4 @@ -bind = ['[::]:8000', 'localhost:8000'] +bind = '0.0.0.0:8000' backlog = 2048 workers = 3 worker_class = 'sync' diff --git a/docker/hub/docker-compose.postgres.yml b/docker/hub/docker-compose.postgres.yml index c2b599e52..0779cea22 100644 --- a/docker/hub/docker-compose.postgres.yml +++ b/docker/hub/docker-compose.postgres.yml @@ -15,7 +15,7 @@ services: POSTGRES_PASSWORD: paperless webserver: - image: jonaswinkler/paperless-ng:0.9.10 + image: jonaswinkler/paperless-ng:0.9.11 restart: always depends_on: - db diff --git a/docker/hub/docker-compose.sqlite.yml b/docker/hub/docker-compose.sqlite.yml index 429d42c06..3eed96cc3 100644 --- a/docker/hub/docker-compose.sqlite.yml +++ b/docker/hub/docker-compose.sqlite.yml @@ -5,7 +5,7 @@ services: restart: always webserver: - image: jonaswinkler/paperless-ng:0.9.10 + image: jonaswinkler/paperless-ng:0.9.11 restart: always depends_on: - broker diff --git a/docker/hub/docker-compose.tika.yml b/docker/hub/docker-compose.tika.yml new file mode 100644 index 000000000..af8f575a0 --- /dev/null +++ b/docker/hub/docker-compose.tika.yml @@ -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: diff --git a/docker/local/docker-compose.tika.yml b/docker/local/docker-compose.tika.yml new file mode 100644 index 000000000..889713908 --- /dev/null +++ b/docker/local/docker-compose.tika.yml @@ -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: diff --git a/docs/changelog.rst b/docs/changelog.rst index 4357a981b..70f5cf683 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,13 @@ 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 ################### @@ -15,6 +22,7 @@ paperless-ng 0.9.10 * 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. * Filter for documents that don't have any correspondents, types or tags assigned. * 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. * 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. + * Issue with the tag selector not clearing input correctly. + * The consumer used to stop working when encountering an incomplete classifier model file. .. note:: @@ -956,6 +966,7 @@ bulk of the work on this big change. * Initial release +.. _zjean: https://github.com/zjean .. _rYR79435: https://github.com/rYR79435 .. _Michael Shamoon: https://github.com/shamoon .. _jayme-github: http://github.com/jayme-github diff --git a/docs/configuration.rst b/docs/configuration.rst index 5ccb80b3a..49c95bff1 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -277,6 +277,35 @@ PAPERLESS_OCR_USER_ARG= {"deskew": true, "optimize": 3, "unpaper_args": "--pre-rotate 90"} +.. _configuration-tika: + +Tika settings +############# + +Paperless can make use of `Tika `_ and +`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= + Enable (or disable) the Tika parser. + + Defaults to false. + +PAPERLESS_TIKA_ENDPOINT= + Set the endpoint URL were Paperless can reach your Tika server. + + Defaults to "http://localhost:9998". + +PAPERLESS_TIKA_GOTENBERG_ENDPOINT= + Set the endpoint URL were Paperless can reach your Gotenberg server. + + Defaults to "http://localhost:3000". + Software tweaks ############### diff --git a/paperless.conf.example b/paperless.conf.example index 139453cf3..d9d0f5b06 100644 --- a/paperless.conf.example +++ b/paperless.conf.example @@ -39,7 +39,7 @@ #PAPERLESS_OCR_OUTPUT_TYPE=pdfa #PAPERLESS_OCR_PAGES=1 #PAPERLESS_OCR_IMAGE_DPI=300 -#PAPERLESS_OCR_USER_ARG={} +#PAPERLESS_OCR_USER_ARGS={} #PAPERLESS_CONVERT_MEMORY_LIMIT=0 #PAPERLESS_CONVERT_TMPDIR=/var/tmp/paperless diff --git a/src-ui/angular.json b/src-ui/angular.json index 79233eeda..f081430c7 100644 --- a/src-ui/angular.json +++ b/src-ui/angular.json @@ -1,133 +1,138 @@ { - "$schema": "./node_modules/@angular/cli/lib/config/schema.json", - "version": 1, - "newProjectRoot": "projects", - "projects": { - "paperless-ui": { - "projectType": "application", - "schematics": { - "@schematics/angular:component": { - "style": "scss" - } - }, - "root": "", - "sourceRoot": "src", - "prefix": "app", - "architect": { - "build": { - "builder": "@angular-devkit/build-angular:browser", - "options": { - "outputPath": "dist/paperless-ui", - "outputHashing": "none", - "index": "src/index.html", - "main": "src/main.ts", - "polyfills": "src/polyfills.ts", - "tsConfig": "tsconfig.app.json", - "aot": true, - "assets": [ - "src/favicon.ico", - "src/assets" - ], - "styles": [ - "src/styles.scss" - ], - "scripts": [], - "allowedCommonJsDependencies": [ - "ng2-pdf-viewer" - ] - }, - "configurations": { - "production": { - "fileReplacements": [ - { - "replace": "src/environments/environment.ts", - "with": "src/environments/environment.prod.ts" - } - ], - "optimization": true, - "outputHashing": "none", - "sourceMap": false, - "extractCss": true, - "namedChunks": false, - "extractLicenses": true, - "vendorChunk": false, - "buildOptimizer": true, - "budgets": [ - { - "type": "initial", - "maximumWarning": "2mb", - "maximumError": "5mb" - }, - { - "type": "anyComponentStyle", - "maximumWarning": "6kb", - "maximumError": "10kb" - } - ] - } - } - }, - "serve": { - "builder": "@angular-devkit/build-angular:dev-server", - "options": { - "browserTarget": "paperless-ui:build" - }, - "configurations": { - "production": { - "browserTarget": "paperless-ui:build:production" - } - } - }, - "extract-i18n": { - "builder": "@angular-devkit/build-angular:extract-i18n", - "options": { - "browserTarget": "paperless-ui:build" - } - }, - "test": { - "builder": "@angular-devkit/build-angular:karma", - "options": { - "main": "src/test.ts", - "polyfills": "src/polyfills.ts", - "tsConfig": "tsconfig.spec.json", - "karmaConfig": "karma.conf.js", - "assets": [ - "src/favicon.ico", - "src/assets" - ], - "styles": [ - "src/styles.scss" - ], - "scripts": [] - } - }, - "lint": { - "builder": "@angular-devkit/build-angular:tslint", - "options": { - "tsConfig": [ - "tsconfig.app.json", - "tsconfig.spec.json", - "e2e/tsconfig.json" - ], - "exclude": [ - "**/node_modules/**" - ] - } - }, - "e2e": { - "builder": "@angular-devkit/build-angular:protractor", - "options": { - "protractorConfig": "e2e/protractor.conf.js", - "devServerTarget": "paperless-ui:serve" - }, - "configurations": { - "production": { - "devServerTarget": "paperless-ui:serve:production" - } - } - } - } - } - }, - "defaultProject": "paperless-ui" -} + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "paperless-ui": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "dist/paperless-ui", + "outputHashing": "none", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "tsconfig.app.json", + "aot": true, + "assets": [ + "src/favicon.ico", + "src/assets", + "src/manifest.webmanifest" + ], + "styles": [ + "src/styles.scss" + ], + "scripts": [], + "allowedCommonJsDependencies": [ + "ng2-pdf-viewer" + ] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "optimization": true, + "outputHashing": "none", + "sourceMap": false, + "extractCss": true, + "namedChunks": false, + "extractLicenses": true, + "vendorChunk": false, + "buildOptimizer": true, + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "5mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb", + "maximumError": "10kb" + } + ] + } + } + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "options": { + "browserTarget": "paperless-ui:build" + }, + "configurations": { + "production": { + "browserTarget": "paperless-ui:build:production" + } + } + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "paperless-ui:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "tsconfig.spec.json", + "karmaConfig": "karma.conf.js", + "assets": [ + "src/favicon.ico", + "src/assets", + "src/manifest.webmanifest" + ], + "styles": [ + "src/styles.scss" + ], + "scripts": [] + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": [ + "tsconfig.app.json", + "tsconfig.spec.json", + "e2e/tsconfig.json" + ], + "exclude": [ + "**/node_modules/**" + ] + } + }, + "e2e": { + "builder": "@angular-devkit/build-angular:protractor", + "options": { + "protractorConfig": "e2e/protractor.conf.js", + "devServerTarget": "paperless-ui:serve" + }, + "configurations": { + "production": { + "devServerTarget": "paperless-ui:serve:production" + } + } + } + } + } + }, + "defaultProject": "paperless-ui", + "cli": { + "analytics": "7c47c2bc-b97e-4014-85ae-b0c99b5750b4" + } +} \ No newline at end of file diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index 18eeec0ef..5f12c504c 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -149,8 +149,8 @@ 161 - - Do you really want to delete document ''? + + Do you really want to delete document ""? src/app/components/document-detail/document-detail.component.ts 162 @@ -1050,36 +1050,36 @@ 115 - - This operation will add the tag "" to all selected document(s). + + This operation will add the tag "" to selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts 118 - - This operation will add the tags to all selected document(s). + + This operation will add the tags to selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts 120 - - This operation will remove the tag "" from all selected document(s). + + This operation will remove the tag "" from selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts 123 - - This operation will remove the tags from all selected document(s). + + This operation will remove the tags from selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts 125 - - This operation will add the tags and remove the tags on all selected document(s). + + This operation will add the tags and remove the tags on selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts 127 @@ -1092,15 +1092,15 @@ 157 - - This operation will assign the correspondent "" to all selected document(s). + + This operation will assign the correspondent "" to selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts 159 - - This operation will remove the correspondent from all selected document(s). + + This operation will remove the correspondent from selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts 161 @@ -1113,15 +1113,15 @@ 190 - - This operation will assign the document type "" to all selected document(s). + + This operation will assign the document type "" to selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts 192 - - This operation will remove the document type from all selected document(s). + + This operation will remove the document type from selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts 194 @@ -1134,8 +1134,8 @@ 219 - - This operation will permanently delete all selected document(s). + + This operation will permanently delete selected document(s). src/app/components/document-list/bulk-editor/bulk-editor.component.ts 220 @@ -1281,8 +1281,8 @@ 5 - - 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. + + 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 they will appear on the dashboard instead of this message. src/app/components/dashboard/widgets/welcome-widget/welcome-widget.component.html 6,7 diff --git a/src-ui/src/app/app.component.ts b/src-ui/src/app/app.component.ts index 84c173a18..9cd9fb3ec 100644 --- a/src-ui/src/app/app.component.ts +++ b/src-ui/src/app/app.component.ts @@ -1,4 +1,5 @@ import { Component } from '@angular/core'; +import { AppViewService } from './services/app-view.service'; @Component({ selector: 'app-root', @@ -6,8 +7,9 @@ import { Component } from '@angular/core'; styleUrls: ['./app.component.scss'] }) export class AppComponent { - - constructor () { + + constructor (appViewService: AppViewService) { + appViewService.updateDarkModeSettings() } diff --git a/src-ui/src/app/components/app-frame/app-frame.component.html b/src-ui/src/app/components/app-frame/app-frame.component.html index a05b691af..36408dce5 100644 --- a/src-ui/src/app/components/app-frame/app-frame.component.html +++ b/src-ui/src/app/components/app-frame/app-frame.component.html @@ -5,7 +5,9 @@ - + + + Paperless-ng
diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html index 015269c17..a0e833c98 100644 --- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html +++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html @@ -20,7 +20,7 @@
- +
diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts index e1aa6a06a..b51923c27 100644 --- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts +++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts @@ -18,6 +18,18 @@ export class FilterableDropdownSelectionModel { 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() private temporarySelectionStates = new Map() @@ -69,6 +81,10 @@ export class FilterableDropdownSelectionModel { } + private getNonTemporary(id: number) { + return this.selectionStates.get(id) || ToggleableItemState.NotSelected + } + get(id: number) { return this.temporarySelectionStates.get(id) || ToggleableItemState.NotSelected } diff --git a/src-ui/src/app/components/common/input/tags/tags.component.html b/src-ui/src/app/components/common/input/tags/tags.component.html index 8a5dbc4f2..01b3fe755 100644 --- a/src-ui/src/app/components/common/input/tags/tags.component.html +++ b/src-ui/src/app/components/common/input/tags/tags.component.html @@ -5,7 +5,9 @@ diff --git a/src-ui/src/app/components/dashboard/dashboard.component.html b/src-ui/src/app/components/dashboard/dashboard.component.html index 5b76dd242..50bd44a4e 100644 --- a/src-ui/src/app/components/dashboard/dashboard.component.html +++ b/src-ui/src/app/components/dashboard/dashboard.component.html @@ -1,5 +1,8 @@ - +
diff --git a/src-ui/src/app/components/dashboard/widgets/welcome-widget/welcome-widget.component.html b/src-ui/src/app/components/dashboard/widgets/welcome-widget/welcome-widget.component.html index 7a2bbcb3c..6320189cc 100644 --- a/src-ui/src/app/components/dashboard/widgets/welcome-widget/welcome-widget.component.html +++ b/src-ui/src/app/components/dashboard/widgets/welcome-widget/welcome-widget.component.html @@ -4,7 +4,7 @@

Paperless is running! :)

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.

+ 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.

Paperless offers some more features that try to make your life easier:

  • Once you've got a couple documents in paperless and added metadata to them, paperless can assign that metadata to new documents automatically.
  • diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts index 2efd32f27..053258f34 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.ts @@ -159,7 +159,7 @@ export class DocumentDetailComponent implements OnInit { delete() { let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'}) 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.btnClass = "btn-danger" modal.componentInstance.btnCaption = $localize`Delete document` diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts index 1347cb138..e69ab241b 100644 --- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts +++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts @@ -115,16 +115,16 @@ export class BulkEditorComponent { modal.componentInstance.title = $localize`Confirm tags assignment` if (changedTags.itemsToAdd.length == 1 && changedTags.itemsToRemove.length == 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) { - 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) { 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) { - 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 { - 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" @@ -156,9 +156,9 @@ export class BulkEditorComponent { let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'}) modal.componentInstance.title = $localize`Confirm correspondent assignment` 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 { - 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.btnCaption = $localize`Confirm` @@ -189,9 +189,9 @@ export class BulkEditorComponent { let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'}) modal.componentInstance.title = $localize`Confirm document type assignment` 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 { - 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.btnCaption = $localize`Confirm` @@ -217,7 +217,7 @@ export class BulkEditorComponent { let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'}) modal.componentInstance.delayConfirm(5) 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.btnClass = "btn-danger" modal.componentInstance.btnCaption = $localize`Delete document(s)` diff --git a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html index c1757eb35..56047fc1e 100644 --- a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html +++ b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html @@ -1,7 +1,7 @@ -
    +
    -
    - +
    +
    @@ -12,7 +12,7 @@
    -
    +
    @@ -55,16 +55,16 @@  Download - +
    Score: - + Created: {{document.created | date}}
    - +
    diff --git a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.scss b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.scss index eb744d2af..0b1604d90 100644 --- a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.scss +++ b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.scss @@ -30,10 +30,6 @@ border-color: $primary; } -.doc-img-background { - background-color: white; -} - .doc-img-background-selected { background-color: $primaryFaded; -} \ No newline at end of file +} diff --git a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html index e0d8d28e1..83ba8c274 100644 --- a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html +++ b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html @@ -1,7 +1,7 @@
    -
    -
    - +
    +
    +
    diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index bc1047ba9..e627c428d 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -78,8 +78,8 @@
    - - + +
    diff --git a/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.ts b/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.ts index d3b71b4ab..37b6fa66e 100644 --- a/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.ts +++ b/src-ui/src/app/components/manage/correspondent-list/correspondent-list.component.ts @@ -1,5 +1,4 @@ -import { Component, OnInit } from '@angular/core'; -import { Router } from '@angular/router'; +import { Component } from '@angular/core'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { FILTER_CORRESPONDENT } from 'src/app/data/filter-rule-type'; import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'; @@ -16,7 +15,6 @@ import { CorrespondentEditDialogComponent } from './correspondent-edit-dialog/co export class CorrespondentListComponent extends GenericListComponent { constructor(correspondentsService: CorrespondentService, modalService: NgbModal, - private router: Router, private list: DocumentListViewService ) { super(correspondentsService,modalService,CorrespondentEditDialogComponent) @@ -27,9 +25,6 @@ export class CorrespondentListComponent extends GenericListComponent { constructor(service: DocumentTypeService, modalService: NgbModal, - private router: Router, private list: DocumentListViewService ) { super(service, modalService, DocumentTypeEditDialogComponent) @@ -28,9 +26,6 @@ export class DocumentTypeListComponent extends GenericListComponentGeneral settings -

    Document list

    - +

    Appearance

    +
    Items per page
    - + - -
    - +
    -

    Bulk editing

    +
    +
    + Dark mode +
    +
    + +
    + + +
    +
    +
    - - +

    Bulk editing

    + +
    +
    + + +
    +
    @@ -42,7 +57,7 @@
    - +
    @@ -68,7 +83,7 @@
    No saved views defined.
    - +
    @@ -78,4 +93,4 @@
    - \ No newline at end of file + diff --git a/src-ui/src/app/components/manage/settings/settings.component.ts b/src-ui/src/app/components/manage/settings/settings.component.ts index c26c63384..df3015e21 100644 --- a/src-ui/src/app/components/manage/settings/settings.component.ts +++ b/src-ui/src/app/components/manage/settings/settings.component.ts @@ -1,10 +1,11 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, Renderer2 } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'; import { DocumentListViewService } from 'src/app/services/document-list-view.service'; import { SavedViewService } from 'src/app/services/rest/saved-view.service'; import { SettingsService, SETTINGS_KEYS } from 'src/app/services/settings.service'; import { ToastService } from 'src/app/services/toast.service'; +import { AppViewService } from 'src/app/services/app-view.service'; @Component({ selector: 'app-settings', @@ -19,18 +20,21 @@ export class SettingsComponent implements OnInit { '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)), '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: PaperlessSavedView[] + constructor( public savedViewService: SavedViewService, private documentListViewService: DocumentListViewService, private toastService: ToastService, - private settings: SettingsService + private settings: SettingsService, + private appViewService: AppViewService ) { } - savedViews: PaperlessSavedView[] - ngOnInit() { this.savedViewService.listAll().subscribe(r => { 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() { 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.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.appViewService.updateDarkModeSettings() this.toastService.showInfo($localize`Settings saved successfully.`) } diff --git a/src-ui/src/app/components/manage/tag-list/tag-list.component.ts b/src-ui/src/app/components/manage/tag-list/tag-list.component.ts index 3d1ccd778..2c70ffef1 100644 --- a/src-ui/src/app/components/manage/tag-list/tag-list.component.ts +++ b/src-ui/src/app/components/manage/tag-list/tag-list.component.ts @@ -1,5 +1,4 @@ import { Component } from '@angular/core'; -import { Router } from '@angular/router'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { FILTER_HAS_TAG } from 'src/app/data/filter-rule-type'; 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 { constructor(tagService: TagService, modalService: NgbModal, - private router: Router, private list: DocumentListViewService ) { super(tagService, modalService, TagEditDialogComponent) @@ -27,14 +25,11 @@ export class TagListComponent extends GenericListComponent { } getDeleteMessage(object: PaperlessTag) { - return $localize`Do you really want to delete the tag "${object.name}"?` } filterDocuments(object: PaperlessTag) { - this.list.documentListView.filter_rules = [ - {rule_type: FILTER_HAS_TAG, value: object.id.toString()} - ] - this.router.navigate(["documents"]) + this.list.quickFilter([{rule_type: FILTER_HAS_TAG, value: object.id.toString()}]) + } } diff --git a/src-ui/src/app/services/app-view.service.spec.ts b/src-ui/src/app/services/app-view.service.spec.ts new file mode 100644 index 000000000..fc44ed3a4 --- /dev/null +++ b/src-ui/src/app/services/app-view.service.spec.ts @@ -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(); + }); +}); diff --git a/src-ui/src/app/services/app-view.service.ts b/src-ui/src/app/services/app-view.service.ts new file mode 100644 index 000000000..6af2e43af --- /dev/null +++ b/src-ui/src/app/services/app-view.service.ts @@ -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') + } + + } + +} diff --git a/src-ui/src/app/services/document-list-view.service.ts b/src-ui/src/app/services/document-list-view.service.ts index dfcf9c0c5..eb69439ec 100644 --- a/src-ui/src/app/services/document-list-view.service.ts +++ b/src-ui/src/app/services/document-list-view.service.ts @@ -1,4 +1,5 @@ import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; import { Observable } from 'rxjs'; import { cloneFilterRules, FilterRule } from '../data/filter-rule'; 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)) } + quickFilter(filterRules: FilterRule[]) { + this.savedView = null + this.view.filter_rules = filterRules + this.reduceSelectionToFilter() + this.saveDocumentListView() + this.router.navigate(["documents"]) + } + getLastPage(): number { 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) if (documentListViewConfigJson) { try { diff --git a/src-ui/src/app/services/settings.service.ts b/src-ui/src/app/services/settings.service.ts index 00e6ff639..7b1cfe9e3 100644 --- a/src-ui/src/app/services/settings.service.ts +++ b/src-ui/src/app/services/settings.service.ts @@ -10,12 +10,16 @@ export const SETTINGS_KEYS = { BULK_EDIT_CONFIRMATION_DIALOGS: 'general-settings:bulk-edit:confirmation-dialogs', BULK_EDIT_APPLY_ON_CLOSE: 'general-settings:bulk-edit:apply-on-close', 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[] = [ {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.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({ diff --git a/src-ui/src/assets/logo-dark-notext.svg b/src-ui/src/assets/logo-dark-notext.svg index 38ca9e700..74eb142c8 100644 --- a/src-ui/src/assets/logo-dark-notext.svg +++ b/src-ui/src/assets/logo-dark-notext.svg @@ -1,69 +1,19 @@ - - - - - - - - image/svg+xml - - - - - - - - - - - + + + + + + + + + + diff --git a/src-ui/src/assets/logo-dark.svg b/src-ui/src/assets/logo-dark.svg index bc8ba2b80..111d8d32f 100644 --- a/src-ui/src/assets/logo-dark.svg +++ b/src-ui/src/assets/logo-dark.svg @@ -1,93 +1,5 @@ - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - + + + + diff --git a/src-ui/src/assets/logo-white-notext.svg b/src-ui/src/assets/logo-white-notext.svg new file mode 100644 index 000000000..38ca9e700 --- /dev/null +++ b/src-ui/src/assets/logo-white-notext.svg @@ -0,0 +1,69 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/src-ui/src/environments/environment.prod.ts b/src-ui/src/environments/environment.prod.ts index 7b707f014..1ac69bc39 100644 --- a/src-ui/src/environments/environment.prod.ts +++ b/src-ui/src/environments/environment.prod.ts @@ -2,5 +2,5 @@ export const environment = { production: true, apiBaseUrl: "/api/", appTitle: "Paperless-ng", - version: "0.9.10" + version: "0.9.11" }; diff --git a/src-ui/src/environments/environment.ts b/src-ui/src/environments/environment.ts index 29a8f3af6..7c5b29e82 100644 --- a/src-ui/src/environments/environment.ts +++ b/src-ui/src/environments/environment.ts @@ -4,7 +4,7 @@ export const environment = { production: false, - apiBaseUrl: "http://localhost:8000/api/", + apiBaseUrl: "http://10.0.1.26:8000/api/", appTitle: "Paperless-ng", version: "DEVELOPMENT" }; diff --git a/src-ui/src/index.html b/src-ui/src/index.html index f82399ce6..b05f9123b 100644 --- a/src-ui/src/index.html +++ b/src-ui/src/index.html @@ -5,9 +5,12 @@ Paperless-ng + + + - + diff --git a/src-ui/src/manifest.webmanifest b/src-ui/src/manifest.webmanifest new file mode 100644 index 000000000..60151bb5c --- /dev/null +++ b/src-ui/src/manifest.webmanifest @@ -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": "/" +} diff --git a/src-ui/src/styles.scss b/src-ui/src/styles.scss index 7e9a9377a..8cf4a93f6 100644 --- a/src-ui/src/styles.scss +++ b/src-ui/src/styles.scss @@ -1,4 +1,5 @@ @import "theme"; +@import "theme_dark"; @import "node_modules/bootstrap/scss/bootstrap"; @import "~@ng-select/ng-select/themes/default.theme.css"; diff --git a/src-ui/src/theme_dark.scss b/src-ui/src/theme_dark.scss new file mode 100644 index 000000000..a122f1d86 --- /dev/null +++ b/src-ui/src/theme_dark.scss @@ -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; + } +} diff --git a/src/documents/consumer.py b/src/documents/consumer.py index 4bcb6d1d9..5a06194b7 100755 --- a/src/documents/consumer.py +++ b/src/documents/consumer.py @@ -158,7 +158,7 @@ class Consumer(LoggingMixin): try: classifier = DocumentClassifier() classifier.reload() - except (FileNotFoundError, IncompatibleClassifierVersionError) as e: + except (OSError, EOFError, IncompatibleClassifierVersionError) as e: self.log( "warning", f"Cannot classify documents: {e}.") diff --git a/src/documents/management/commands/document_retagger.py b/src/documents/management/commands/document_retagger.py index cf014dc6f..0fb9782c1 100755 --- a/src/documents/management/commands/document_retagger.py +++ b/src/documents/management/commands/document_retagger.py @@ -73,7 +73,7 @@ class Command(Renderable, BaseCommand): classifier = DocumentClassifier() try: classifier.reload() - except (FileNotFoundError, IncompatibleClassifierVersionError) as e: + except (OSError, EOFError, IncompatibleClassifierVersionError) as e: logging.getLogger(__name__).warning( f"Cannot classify documents: {e}.") classifier = None diff --git a/src/documents/management/commands/document_thumbnails.py b/src/documents/management/commands/document_thumbnails.py index 41ddec86e..01df14624 100644 --- a/src/documents/management/commands/document_thumbnails.py +++ b/src/documents/management/commands/document_thumbnails.py @@ -23,6 +23,7 @@ def _process_document(doc_in): finally: parser.cleanup() + class Command(Renderable, BaseCommand): help = """ @@ -62,4 +63,6 @@ class Command(Renderable, BaseCommand): db.connections.close_all() 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) + )) diff --git a/src/documents/signals/handlers.py b/src/documents/signals/handlers.py index 586897585..f2743c212 100755 --- a/src/documents/signals/handlers.py +++ b/src/documents/signals/handlers.py @@ -276,13 +276,6 @@ def update_filename_and_move_files(sender, instance, **kwargs): Document.objects.filter(pk=instance.pk).update( 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: instance.filename = old_filename # this happens when we can't move a file. If that's the case for diff --git a/src/documents/tasks.py b/src/documents/tasks.py index f9937c177..38ff532b5 100644 --- a/src/documents/tasks.py +++ b/src/documents/tasks.py @@ -35,9 +35,9 @@ def train_classifier(): try: # load the classifier, since we might not have to train it again. classifier.reload() - except (FileNotFoundError, IncompatibleClassifierVersionError): + except (OSError, EOFError, IncompatibleClassifierVersionError): # This is what we're going to fix here. - pass + classifier = DocumentClassifier() try: if classifier.train(): @@ -94,7 +94,10 @@ def bulk_update_documents(document_ids): documents = Document.objects.filter(id__in=document_ids) ix = index.open_index() + + for doc in documents: + post_save.send(Document, instance=doc, created=False) + with AsyncWriter(ix) as writer: for doc in documents: index.update_document(writer, doc) - post_save.send(Document, instance=doc, created=False) diff --git a/src/documents/templates/index.html b/src/documents/templates/index.html index 47a352cd5..83544b5e4 100644 --- a/src/documents/templates/index.html +++ b/src/documents/templates/index.html @@ -12,7 +12,9 @@ - + + + Loading... diff --git a/src/paperless/settings.py b/src/paperless/settings.py index 5af1be85e..caa1b9b18 100644 --- a/src/paperless/settings.py +++ b/src/paperless/settings.py @@ -87,6 +87,7 @@ INSTALLED_APPS = [ "documents.apps.DocumentsConfig", "paperless_tesseract.apps.PaperlessTesseractConfig", "paperless_text.apps.PaperlessTextConfig", + "paperless_tika.apps.PaperlessTikaConfig", "paperless_mail.apps.PaperlessMailConfig", "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") 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" +) diff --git a/src/paperless/version.py b/src/paperless/version.py index facb097fc..e1ba14cb4 100644 --- a/src/paperless/version.py +++ b/src/paperless/version.py @@ -1 +1 @@ -__version__ = (0, 9, 10) +__version__ = (0, 9, 11) diff --git a/src/paperless_tika/apps.py b/src/paperless_tika/apps.py new file mode 100644 index 000000000..5cab21427 --- /dev/null +++ b/src/paperless_tika/apps.py @@ -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) diff --git a/src/paperless_tika/parsers.py b/src/paperless_tika/parsers.py new file mode 100644 index 000000000..81f213a6b --- /dev/null +++ b/src/paperless_tika/parsers.py @@ -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() diff --git a/src/paperless_tika/signals.py b/src/paperless_tika/signals.py new file mode 100644 index 000000000..409daebe2 --- /dev/null +++ b/src/paperless_tika/signals.py @@ -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", + }, + }