mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-11-03 03:16:10 -06:00 
			
		
		
		
	Merge branch 'dev' into feature-localization
This commit is contained in:
		
							
								
								
									
										1
									
								
								Pipfile
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								Pipfile
									
									
									
									
									
								
							@@ -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 = "*"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										57
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										57
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							@@ -1,7 +1,7 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
    "_meta": {
 | 
					    "_meta": {
 | 
				
			||||||
        "hash": {
 | 
					        "hash": {
 | 
				
			||||||
            "sha256": "3d576f289958226a7583e4c471c7f8c11bff6933bf093185f623cfb381a92412"
 | 
					            "sha256": "993e362c31af6b8094693075f614270a820cf0b557369d66d674e1a107b7bd31"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "pipfile-spec": 6,
 | 
					        "pipfile-spec": 6,
 | 
				
			||||||
        "requires": {
 | 
					        "requires": {
 | 
				
			||||||
@@ -44,6 +44,13 @@
 | 
				
			|||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "version": "==1.17.12"
 | 
					            "version": "==1.17.12"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        "certifi": {
 | 
				
			||||||
 | 
					            "hashes": [
 | 
				
			||||||
 | 
					                "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
 | 
				
			||||||
 | 
					                "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            "version": "==2020.12.5"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        "cffi": {
 | 
					        "cffi": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e",
 | 
					                "sha256:00a1ba5e2e95684448de9b89888ccd02c98d512064b4cb987d48f4b40aa0421e",
 | 
				
			||||||
@@ -229,6 +236,15 @@
 | 
				
			|||||||
            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
 | 
					            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
 | 
				
			||||||
            "version": "==9.0"
 | 
					            "version": "==9.0"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        "idna": {
 | 
				
			||||||
 | 
					            "hashes": [
 | 
				
			||||||
 | 
					                "sha256:4a57a6379512ade94fa99e2fa46d3cd0f2f553040548d0e2958c6ed90ee48226",
 | 
				
			||||||
 | 
					                "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
 | 
				
			||||||
 | 
					                "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
 | 
				
			||||||
 | 
					            "version": "==2.10"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        "imap-tools": {
 | 
					        "imap-tools": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:72bf46dc135b039a5d5b59f4e079242ac15eac02a30038e8cb2dec7b153cab65",
 | 
					                "sha256:72bf46dc135b039a5d5b59f4e079242ac15eac02a30038e8cb2dec7b153cab65",
 | 
				
			||||||
@@ -683,6 +699,14 @@
 | 
				
			|||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "version": "==3.5.56"
 | 
					            "version": "==3.5.56"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        "requests": {
 | 
				
			||||||
 | 
					            "hashes": [
 | 
				
			||||||
 | 
					                "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
 | 
				
			||||||
 | 
					                "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
 | 
				
			||||||
 | 
					            "version": "==2.25.1"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        "scikit-learn": {
 | 
					        "scikit-learn": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:090bbf144fd5823c1f2efa3e1a9bf180295b24294ca8f478e75b40ed54f8036e",
 | 
					                "sha256:090bbf144fd5823c1f2efa3e1a9bf180295b24294ca8f478e75b40ed54f8036e",
 | 
				
			||||||
@@ -769,6 +793,14 @@
 | 
				
			|||||||
            "markers": "python_version >= '3.5'",
 | 
					            "markers": "python_version >= '3.5'",
 | 
				
			||||||
            "version": "==2.1.0"
 | 
					            "version": "==2.1.0"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        "tika": {
 | 
				
			||||||
 | 
					            "hashes": [
 | 
				
			||||||
 | 
					                "sha256:c2c50f405622f74531841104f9e85c17511aede11de8e5385eab1a29a31f191b",
 | 
				
			||||||
 | 
					                "sha256:d1f2eddb93caa9a2857569486aa2bc0320d0bf1796cdbe03066954cbc4b4bf62"
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            "index": "pypi",
 | 
				
			||||||
 | 
					            "version": "==1.24"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        "tqdm": {
 | 
					        "tqdm": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:38b658a3e4ecf9b4f6f8ff75ca16221ae3378b2e175d846b6b33ea3a20852cf5",
 | 
					                "sha256:38b658a3e4ecf9b4f6f8ff75ca16221ae3378b2e175d846b6b33ea3a20852cf5",
 | 
				
			||||||
@@ -777,6 +809,15 @@
 | 
				
			|||||||
            "index": "pypi",
 | 
					            "index": "pypi",
 | 
				
			||||||
            "version": "==4.54.1"
 | 
					            "version": "==4.54.1"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        "typing-extensions": {
 | 
				
			||||||
 | 
					            "hashes": [
 | 
				
			||||||
 | 
					                "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918",
 | 
				
			||||||
 | 
					                "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c",
 | 
				
			||||||
 | 
					                "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            "markers": "python_version < '3.8'",
 | 
				
			||||||
 | 
					            "version": "==3.7.4.3"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        "tzlocal": {
 | 
					        "tzlocal": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:643c97c5294aedc737780a49d9df30889321cbe1204eac2c2ec6134035a92e44",
 | 
					                "sha256:643c97c5294aedc737780a49d9df30889321cbe1204eac2c2ec6134035a92e44",
 | 
				
			||||||
@@ -784,6 +825,14 @@
 | 
				
			|||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "version": "==2.1"
 | 
					            "version": "==2.1"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					        "urllib3": {
 | 
				
			||||||
 | 
					            "hashes": [
 | 
				
			||||||
 | 
					                "sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08",
 | 
				
			||||||
 | 
					                "sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
 | 
				
			||||||
 | 
					            "version": "==1.26.2"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        "watchdog": {
 | 
					        "watchdog": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:3caefdcc8f06a57fdc5ef2d22aa7c0bfda4f55e71a0bee74cbf3176d97536ef3",
 | 
					                "sha256:3caefdcc8f06a57fdc5ef2d22aa7c0bfda4f55e71a0bee74cbf3176d97536ef3",
 | 
				
			||||||
@@ -1197,11 +1246,11 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "requests": {
 | 
					        "requests": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8",
 | 
					                "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
 | 
				
			||||||
                "sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998"
 | 
					                "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
 | 
					            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
 | 
				
			||||||
            "version": "==2.25.0"
 | 
					            "version": "==2.25.1"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "six": {
 | 
					        "six": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										43
									
								
								docker/hub/docker-compose.tika.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								docker/hub/docker-compose.tika.yml
									
									
									
									
									
										Normal 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:
 | 
				
			||||||
							
								
								
									
										43
									
								
								docker/local/docker-compose.tika.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								docker/local/docker-compose.tika.yml
									
									
									
									
									
										Normal 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:
 | 
				
			||||||
@@ -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
 | 
				
			||||||
###############
 | 
					###############
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,2 +1,4 @@
 | 
				
			|||||||
docker run -p 5432:5432 -v paperless_pgdata:/var/lib/postgresql/data -d postgres:13
 | 
					docker run -p 5432:5432 -v paperless_pgdata:/var/lib/postgresql/data -d postgres:13
 | 
				
			||||||
docker run -d -p 6379:6379 redis:latest
 | 
					docker run -d -p 6379:6379 redis:latest
 | 
				
			||||||
 | 
					docker run -p 3000:3000 -d thecodingmachine/gotenberg
 | 
				
			||||||
 | 
					docker run -p 9998:9998 -d apache/tika
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -34,7 +34,11 @@
 | 
				
			|||||||
						"assets": [
 | 
											"assets": [
 | 
				
			||||||
							"src/favicon.ico",
 | 
												"src/favicon.ico",
 | 
				
			||||||
							"src/assets",
 | 
												"src/assets",
 | 
				
			||||||
							"src/manifest.webmanifest"
 | 
												"src/manifest.webmanifest", {
 | 
				
			||||||
 | 
													"glob": "pdf.worker.min.js",
 | 
				
			||||||
 | 
													"input": "node_modules/pdfjs-dist/build/",
 | 
				
			||||||
 | 
													"output": "/assets/js/"
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
						],
 | 
											],
 | 
				
			||||||
						"styles": [
 | 
											"styles": [
 | 
				
			||||||
							"src/styles.scss"
 | 
												"src/styles.scss"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -377,7 +377,7 @@
 | 
				
			|||||||
        <source>Do you really want to delete the tag "<x id="PH" equiv-text="object.name"/>"?</source>
 | 
					        <source>Do you really want to delete the tag "<x id="PH" equiv-text="object.name"/>"?</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.ts</context>
 | 
					          <context context-type="sourcefile">src/app/components/manage/tag-list/tag-list.component.ts</context>
 | 
				
			||||||
          <context context-type="linenumber">31</context>
 | 
					          <context context-type="linenumber">28</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="70a67e04629f6d412db0a12d51820b480788d795" datatype="html">
 | 
					      <trans-unit id="70a67e04629f6d412db0a12d51820b480788d795" datatype="html">
 | 
				
			||||||
@@ -440,7 +440,7 @@
 | 
				
			|||||||
        <source>Do you really want to delete the document type "<x id="PH" equiv-text="object.name"/>"?</source>
 | 
					        <source>Do you really want to delete the document type "<x id="PH" equiv-text="object.name"/>"?</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/manage/document-type-list/document-type-list.component.ts</context>
 | 
					          <context context-type="sourcefile">src/app/components/manage/document-type-list/document-type-list.component.ts</context>
 | 
				
			||||||
          <context context-type="linenumber">26</context>
 | 
					          <context context-type="linenumber">24</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="bc000b39af12c0925c424f4cb85f0c31c0f8eca8" datatype="html">
 | 
					      <trans-unit id="bc000b39af12c0925c424f4cb85f0c31c0f8eca8" datatype="html">
 | 
				
			||||||
@@ -468,21 +468,21 @@
 | 
				
			|||||||
        <source>Saved view "<x id="PH" equiv-text="savedView.name"/> deleted.</source>
 | 
					        <source>Saved view "<x id="PH" equiv-text="savedView.name"/> deleted.</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
 | 
					          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
 | 
				
			||||||
          <context context-type="linenumber">52</context>
 | 
					          <context context-type="linenumber">54</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="5647210819299459618" datatype="html">
 | 
					      <trans-unit id="5647210819299459618" datatype="html">
 | 
				
			||||||
        <source>Settings saved successfully.</source>
 | 
					        <source>Settings saved successfully.</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
 | 
					          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
 | 
				
			||||||
          <context context-type="linenumber">61</context>
 | 
					          <context context-type="linenumber">74</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="8488620293789898901" datatype="html">
 | 
					      <trans-unit id="8488620293789898901" datatype="html">
 | 
				
			||||||
        <source>Error while storing settings on server: <x id="PH" equiv-text="JSON.stringify(error.error)"/></source>
 | 
					        <source>Error while storing settings on server: <x id="PH" equiv-text="JSON.stringify(error.error)"/></source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
 | 
					          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.ts</context>
 | 
				
			||||||
          <context context-type="linenumber">73</context>
 | 
					          <context context-type="linenumber">86</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="11ebd254cc9294717105c5982eb0cd2af30a446d" datatype="html">
 | 
					      <trans-unit id="11ebd254cc9294717105c5982eb0cd2af30a446d" datatype="html">
 | 
				
			||||||
@@ -496,11 +496,11 @@
 | 
				
			|||||||
        <source>Saved views</source>
 | 
					        <source>Saved views</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
 | 
					          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
 | 
				
			||||||
          <context context-type="linenumber">41</context>
 | 
					          <context context-type="linenumber">56</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="0d8ceb153aa715eb905da0710cc0b2ac73159abc" datatype="html">
 | 
					      <trans-unit id="bbe41ac2ea4a6c00ea941a41b33105048f8e9f13" datatype="html">
 | 
				
			||||||
        <source>Document list</source>
 | 
					        <source>Appearance</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
 | 
					          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
 | 
				
			||||||
          <context context-type="linenumber">13</context>
 | 
					          <context context-type="linenumber">13</context>
 | 
				
			||||||
@@ -513,60 +513,74 @@
 | 
				
			|||||||
          <context context-type="linenumber">17</context>
 | 
					          <context context-type="linenumber">17</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
 | 
					      <trans-unit id="9ee5d1cbfd6ee168dae37aaba2b59b50bcabb2ff" datatype="html">
 | 
				
			||||||
 | 
					        <source>Dark mode</source>
 | 
				
			||||||
 | 
					        <context-group purpose="location">
 | 
				
			||||||
 | 
					          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
 | 
				
			||||||
 | 
					          <context context-type="linenumber">33</context>
 | 
				
			||||||
 | 
					        </context-group>
 | 
				
			||||||
 | 
					      </trans-unit>
 | 
				
			||||||
 | 
					      <trans-unit id="f8cb5506e70fd71fddc9bb71cee18bfff7b29637" datatype="html">
 | 
				
			||||||
 | 
					        <source>Use system settings</source>
 | 
				
			||||||
 | 
					        <context-group purpose="location">
 | 
				
			||||||
 | 
					          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
 | 
				
			||||||
 | 
					          <context context-type="linenumber">36</context>
 | 
				
			||||||
 | 
					        </context-group>
 | 
				
			||||||
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="3863a86cd9e69a61d143d3daf51df44203df4a82" datatype="html">
 | 
					      <trans-unit id="3863a86cd9e69a61d143d3daf51df44203df4a82" datatype="html">
 | 
				
			||||||
        <source>Bulk editing</source>
 | 
					        <source>Bulk editing</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
 | 
					          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
 | 
				
			||||||
          <context context-type="linenumber">33</context>
 | 
					          <context context-type="linenumber">44</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="c0ac61661c6c326d6e0e00c231b95cf2ac0c6586" datatype="html">
 | 
					      <trans-unit id="c0ac61661c6c326d6e0e00c231b95cf2ac0c6586" datatype="html">
 | 
				
			||||||
        <source>Show confirmation dialogs</source>
 | 
					        <source>Show confirmation dialogs</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
 | 
					          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
 | 
				
			||||||
          <context context-type="linenumber">35</context>
 | 
					          <context context-type="linenumber">48</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="291bbe56ecbe945dcf05580a57d679fa7bd1e06a" datatype="html">
 | 
					      <trans-unit id="291bbe56ecbe945dcf05580a57d679fa7bd1e06a" datatype="html">
 | 
				
			||||||
        <source>Deleting documents will always ask for confirmation.</source>
 | 
					        <source>Deleting documents will always ask for confirmation.</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
 | 
					          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
 | 
				
			||||||
          <context context-type="linenumber">35</context>
 | 
					          <context context-type="linenumber">48</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="8cfddc13e04f5545ac63f419ef363505d6f78c2e" datatype="html">
 | 
					      <trans-unit id="8cfddc13e04f5545ac63f419ef363505d6f78c2e" datatype="html">
 | 
				
			||||||
        <source>Apply on close</source>
 | 
					        <source>Apply on close</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
 | 
					          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
 | 
				
			||||||
          <context context-type="linenumber">36</context>
 | 
					          <context context-type="linenumber">49</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="8cb90334f5dfd7fc67205085f59381e2a334ccfc" datatype="html">
 | 
					      <trans-unit id="8cb90334f5dfd7fc67205085f59381e2a334ccfc" datatype="html">
 | 
				
			||||||
        <source>Appears on</source>
 | 
					        <source>Appears on</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
 | 
					          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
 | 
				
			||||||
          <context context-type="linenumber">53</context>
 | 
					          <context context-type="linenumber">68</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="6717cf1acf04728fc2b7c39f6d3297f8ff15fde5" datatype="html">
 | 
					      <trans-unit id="6717cf1acf04728fc2b7c39f6d3297f8ff15fde5" datatype="html">
 | 
				
			||||||
        <source>Show on dashboard</source>
 | 
					        <source>Show on dashboard</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
 | 
					          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
 | 
				
			||||||
          <context context-type="linenumber">56</context>
 | 
					          <context context-type="linenumber">71</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="541bfc5b123b3f8867fd681eaceefb663a811973" datatype="html">
 | 
					      <trans-unit id="541bfc5b123b3f8867fd681eaceefb663a811973" datatype="html">
 | 
				
			||||||
        <source>Show in sidebar</source>
 | 
					        <source>Show in sidebar</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
 | 
					          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
 | 
				
			||||||
          <context context-type="linenumber">60</context>
 | 
					          <context context-type="linenumber">75</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="abba764a7a595d04dc8c3b26e04b3780d4fdb540" datatype="html">
 | 
					      <trans-unit id="abba764a7a595d04dc8c3b26e04b3780d4fdb540" datatype="html">
 | 
				
			||||||
        <source>No saved views defined.</source>
 | 
					        <source>No saved views defined.</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
 | 
					          <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context>
 | 
				
			||||||
          <context context-type="linenumber">70</context>
 | 
					          <context context-type="linenumber">85</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="ef60a738a565f498b858e903e42bc5ffc3cc1299" datatype="html">
 | 
					      <trans-unit id="ef60a738a565f498b858e903e42bc5ffc3cc1299" datatype="html">
 | 
				
			||||||
@@ -580,7 +594,7 @@
 | 
				
			|||||||
        <source>Do you really want to delete the correspondent "<x id="PH" equiv-text="object.name"/>"?</source>
 | 
					        <source>Do you really want to delete the correspondent "<x id="PH" equiv-text="object.name"/>"?</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/manage/correspondent-list/correspondent-list.component.ts</context>
 | 
					          <context context-type="sourcefile">src/app/components/manage/correspondent-list/correspondent-list.component.ts</context>
 | 
				
			||||||
          <context context-type="linenumber">26</context>
 | 
					          <context context-type="linenumber">24</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="c3f3334de899327bf3ec8999236e10798ff76e72" datatype="html">
 | 
					      <trans-unit id="c3f3334de899327bf3ec8999236e10798ff76e72" datatype="html">
 | 
				
			||||||
@@ -639,8 +653,8 @@
 | 
				
			|||||||
          <context context-type="linenumber">11</context>
 | 
					          <context context-type="linenumber">11</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="180092a6b8a6151a05f4a7552a2fb75fd159dfa8" datatype="html">
 | 
					      <trans-unit id="eab7fc7cf2d663e54de934b779fce4275a303f0f" datatype="html">
 | 
				
			||||||
        <source>Match</source>
 | 
					        <source>Matching pattern</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.html</context>
 | 
					          <context context-type="sourcefile">src/app/components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component.html</context>
 | 
				
			||||||
          <context context-type="linenumber">12</context>
 | 
					          <context context-type="linenumber">12</context>
 | 
				
			||||||
@@ -776,26 +790,33 @@
 | 
				
			|||||||
        <source>Paperless-ng</source>
 | 
					        <source>Paperless-ng</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
 | 
					          <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
 | 
				
			||||||
          <context context-type="linenumber">4</context>
 | 
					          <context context-type="linenumber">11</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
        <note priority="1" from="description">app title</note>
 | 
					        <note priority="1" from="description">app title</note>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="8d667444401ef6380fd262e4fe4795f261a427b1" datatype="html">
 | 
					      <trans-unit id="069566c6ed4f051b5b5617ef1935837226585dad" datatype="html">
 | 
				
			||||||
        <source>Search for documents</source>
 | 
					        <source>Search documents</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
 | 
					          <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
 | 
				
			||||||
          <context context-type="linenumber">12</context>
 | 
					          <context context-type="linenumber">15</context>
 | 
				
			||||||
        </context-group>
 | 
					 | 
				
			||||||
      </trans-unit>
 | 
					 | 
				
			||||||
      <trans-unit id="68949525c4d9a901e0cd15a94e3fc8d2711e9918" datatype="html">
 | 
					 | 
				
			||||||
        <source>Manage</source>
 | 
					 | 
				
			||||||
        <context-group purpose="location">
 | 
					 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
 | 
					 | 
				
			||||||
          <context context-type="linenumber">77</context>
 | 
					 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="121cc5391cd2a5115bc2b3160379ee5b36cd7716" datatype="html">
 | 
					      <trans-unit id="121cc5391cd2a5115bc2b3160379ee5b36cd7716" datatype="html">
 | 
				
			||||||
        <source>Settings</source>
 | 
					        <source>Settings</source>
 | 
				
			||||||
 | 
					        <context-group purpose="location">
 | 
				
			||||||
 | 
					          <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
 | 
				
			||||||
 | 
					          <context context-type="linenumber">40</context>
 | 
				
			||||||
 | 
					        </context-group>
 | 
				
			||||||
 | 
					      </trans-unit>
 | 
				
			||||||
 | 
					      <trans-unit id="bb694b49d408265c91c62799c2b3a7e3151c824d" datatype="html">
 | 
				
			||||||
 | 
					        <source>Logout</source>
 | 
				
			||||||
 | 
					        <context-group purpose="location">
 | 
				
			||||||
 | 
					          <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
 | 
				
			||||||
 | 
					          <context context-type="linenumber">45</context>
 | 
				
			||||||
 | 
					        </context-group>
 | 
				
			||||||
 | 
					      </trans-unit>
 | 
				
			||||||
 | 
					      <trans-unit id="68949525c4d9a901e0cd15a94e3fc8d2711e9918" datatype="html">
 | 
				
			||||||
 | 
					        <source>Manage</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
 | 
					          <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
 | 
				
			||||||
          <context context-type="linenumber">112</context>
 | 
					          <context context-type="linenumber">112</context>
 | 
				
			||||||
@@ -805,70 +826,91 @@
 | 
				
			|||||||
        <source>Admin</source>
 | 
					        <source>Admin</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
 | 
					          <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
 | 
				
			||||||
          <context context-type="linenumber">119</context>
 | 
					          <context context-type="linenumber">147</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="46aa32e581922d6d2c3d7bc4c87209ad5808b029" datatype="html">
 | 
					      <trans-unit id="46aa32e581922d6d2c3d7bc4c87209ad5808b029" datatype="html">
 | 
				
			||||||
        <source>Misc</source>
 | 
					        <source>Misc</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
 | 
					          <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
 | 
				
			||||||
          <context context-type="linenumber">125</context>
 | 
					          <context context-type="linenumber">153</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="fcfd4675b4c90f08d18d3abede9a9a4dff4cfdc7" datatype="html">
 | 
					      <trans-unit id="fcfd4675b4c90f08d18d3abede9a9a4dff4cfdc7" datatype="html">
 | 
				
			||||||
        <source>Documentation</source>
 | 
					        <source>Documentation</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
 | 
					          <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
 | 
				
			||||||
          <context context-type="linenumber">132</context>
 | 
					          <context context-type="linenumber">160</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="355a222236bc01b9a8cd3cb9ecf76891125aed69" datatype="html">
 | 
					      <trans-unit id="355a222236bc01b9a8cd3cb9ecf76891125aed69" datatype="html">
 | 
				
			||||||
        <source>GitHub</source>
 | 
					        <source>GitHub</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
 | 
					          <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
 | 
				
			||||||
          <context context-type="linenumber">139</context>
 | 
					          <context context-type="linenumber">167</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="bb694b49d408265c91c62799c2b3a7e3151c824d" datatype="html">
 | 
					      <trans-unit id="af665f8de8fabe306aaf27443957e69bcbbce63c" datatype="html">
 | 
				
			||||||
        <source>Logout</source>
 | 
					        <source>Logged in as <x id="INTERPOLATION" equiv-text="{{displayName}}"/></source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
 | 
					          <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
 | 
				
			||||||
          <context context-type="linenumber">146</context>
 | 
					          <context context-type="linenumber">34</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="4f55b670f49d927c6026bb614c7c62b1f2a394c0" datatype="html">
 | 
					      <trans-unit id="4f55b670f49d927c6026bb614c7c62b1f2a394c0" datatype="html">
 | 
				
			||||||
        <source>Open documents</source>
 | 
					        <source>Open documents</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
 | 
					          <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
 | 
				
			||||||
          <context context-type="linenumber">57</context>
 | 
					          <context context-type="linenumber">92</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="dca5bf9344a759fa5a07f1b21f50286ec242ba44" datatype="html">
 | 
					      <trans-unit id="dca5bf9344a759fa5a07f1b21f50286ec242ba44" datatype="html">
 | 
				
			||||||
        <source>Close all</source>
 | 
					        <source>Close all</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
 | 
					          <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
 | 
				
			||||||
          <context context-type="linenumber">71</context>
 | 
					          <context context-type="linenumber">106</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="5195932016807797291" datatype="html">
 | 
					      <trans-unit id="5195932016807797291" datatype="html">
 | 
				
			||||||
        <source>Correspondent: <x id="PH" equiv-text="this.correspondents.find(c => c.id == +rule.value)?.name"/></source>
 | 
					        <source>Correspondent: <x id="PH" equiv-text="this.correspondents.find(c => c.id == +rule.value)?.name"/></source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
 | 
					          <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
 | 
				
			||||||
          <context context-type="linenumber">28</context>
 | 
					          <context context-type="linenumber">29</context>
 | 
				
			||||||
 | 
					        </context-group>
 | 
				
			||||||
 | 
					      </trans-unit>
 | 
				
			||||||
 | 
					      <trans-unit id="8170755470576301659" datatype="html">
 | 
				
			||||||
 | 
					        <source>Without correspondent</source>
 | 
				
			||||||
 | 
					        <context-group purpose="location">
 | 
				
			||||||
 | 
					          <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
 | 
				
			||||||
 | 
					          <context context-type="linenumber">31</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="8705701325879965907" datatype="html">
 | 
					      <trans-unit id="8705701325879965907" datatype="html">
 | 
				
			||||||
        <source>Type: <x id="PH" equiv-text="this.documentTypes.find(dt => dt.id == +rule.value)?.name"/></source>
 | 
					        <source>Type: <x id="PH" equiv-text="this.documentTypes.find(dt => dt.id == +rule.value)?.name"/></source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
 | 
					          <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
 | 
				
			||||||
          <context context-type="linenumber">31</context>
 | 
					          <context context-type="linenumber">36</context>
 | 
				
			||||||
 | 
					        </context-group>
 | 
				
			||||||
 | 
					      </trans-unit>
 | 
				
			||||||
 | 
					      <trans-unit id="4362173610367509215" datatype="html">
 | 
				
			||||||
 | 
					        <source>Without document type</source>
 | 
				
			||||||
 | 
					        <context-group purpose="location">
 | 
				
			||||||
 | 
					          <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
 | 
				
			||||||
 | 
					          <context context-type="linenumber">38</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="8180755793012580465" datatype="html">
 | 
					      <trans-unit id="8180755793012580465" datatype="html">
 | 
				
			||||||
        <source>Tag: <x id="PH" equiv-text="this.tags.find(t => t.id == +rule.value)?.name"/></source>
 | 
					        <source>Tag: <x id="PH" equiv-text="this.tags.find(t => t.id == +rule.value)?.name"/></source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
 | 
					          <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
 | 
				
			||||||
          <context context-type="linenumber">34</context>
 | 
					          <context context-type="linenumber">42</context>
 | 
				
			||||||
 | 
					        </context-group>
 | 
				
			||||||
 | 
					      </trans-unit>
 | 
				
			||||||
 | 
					      <trans-unit id="6494566478302448576" datatype="html">
 | 
				
			||||||
 | 
					        <source>Without any tag</source>
 | 
				
			||||||
 | 
					        <context-group purpose="location">
 | 
				
			||||||
 | 
					          <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
 | 
				
			||||||
 | 
					          <context context-type="linenumber">46</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="ddb40946e790522301687ecddb9ce1cb8ad40dd1" datatype="html">
 | 
					      <trans-unit id="ddb40946e790522301687ecddb9ce1cb8ad40dd1" datatype="html">
 | 
				
			||||||
@@ -910,7 +952,7 @@
 | 
				
			|||||||
        <source>Not assigned</source>
 | 
					        <source>Not assigned</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts</context>
 | 
					          <context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts</context>
 | 
				
			||||||
          <context context-type="linenumber">145</context>
 | 
					          <context context-type="linenumber">161</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
        <note priority="1" from="description">Filter drop down element to filter for documents with no correspondent/type/tag assigned</note>
 | 
					        <note priority="1" from="description">Filter drop down element to filter for documents with no correspondent/type/tag assigned</note>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
@@ -1561,22 +1603,43 @@
 | 
				
			|||||||
          <context context-type="linenumber">97</context>
 | 
					          <context context-type="linenumber">97</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="3184700926171002527" datatype="html">
 | 
					      <trans-unit id="5851669019930456395" datatype="html">
 | 
				
			||||||
        <source>Any</source>
 | 
					        <source>Any word</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/data/matching-model.ts</context>
 | 
					          <context context-type="sourcefile">src/app/data/matching-model.ts</context>
 | 
				
			||||||
          <context context-type="linenumber">12</context>
 | 
					          <context context-type="linenumber">12</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="1616102757855967475" datatype="html">
 | 
					      <trans-unit id="7517655726614958140" datatype="html">
 | 
				
			||||||
        <source>All</source>
 | 
					        <source>Any: Document contains any of these words (space separated)</source>
 | 
				
			||||||
 | 
					        <context-group purpose="location">
 | 
				
			||||||
 | 
					          <context context-type="sourcefile">src/app/data/matching-model.ts</context>
 | 
				
			||||||
 | 
					          <context context-type="linenumber">12</context>
 | 
				
			||||||
 | 
					        </context-group>
 | 
				
			||||||
 | 
					      </trans-unit>
 | 
				
			||||||
 | 
					      <trans-unit id="700315718208181326" datatype="html">
 | 
				
			||||||
 | 
					        <source>All words</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/data/matching-model.ts</context>
 | 
					          <context context-type="sourcefile">src/app/data/matching-model.ts</context>
 | 
				
			||||||
          <context context-type="linenumber">13</context>
 | 
					          <context context-type="linenumber">13</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="1968183742008490888" datatype="html">
 | 
					      <trans-unit id="111914402588955480" datatype="html">
 | 
				
			||||||
        <source>Literal</source>
 | 
					        <source>All: Document contains all of these words (space separated)</source>
 | 
				
			||||||
 | 
					        <context-group purpose="location">
 | 
				
			||||||
 | 
					          <context context-type="sourcefile">src/app/data/matching-model.ts</context>
 | 
				
			||||||
 | 
					          <context context-type="linenumber">13</context>
 | 
				
			||||||
 | 
					        </context-group>
 | 
				
			||||||
 | 
					      </trans-unit>
 | 
				
			||||||
 | 
					      <trans-unit id="9180173992399180575" datatype="html">
 | 
				
			||||||
 | 
					        <source>Exact match</source>
 | 
				
			||||||
 | 
					        <context-group purpose="location">
 | 
				
			||||||
 | 
					          <context context-type="sourcefile">src/app/data/matching-model.ts</context>
 | 
				
			||||||
 | 
					          <context context-type="linenumber">14</context>
 | 
				
			||||||
 | 
					        </context-group>
 | 
				
			||||||
 | 
					      </trans-unit>
 | 
				
			||||||
 | 
					      <trans-unit id="7109184332944610787" datatype="html">
 | 
				
			||||||
 | 
					        <source>Exact: Document contains this string</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/data/matching-model.ts</context>
 | 
					          <context context-type="sourcefile">src/app/data/matching-model.ts</context>
 | 
				
			||||||
          <context context-type="linenumber">14</context>
 | 
					          <context context-type="linenumber">14</context>
 | 
				
			||||||
@@ -1589,15 +1652,29 @@
 | 
				
			|||||||
          <context context-type="linenumber">15</context>
 | 
					          <context context-type="linenumber">15</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="701356546322112069" datatype="html">
 | 
					      <trans-unit id="7548151332424148033" datatype="html">
 | 
				
			||||||
        <source>Fuzzy match</source>
 | 
					        <source>Regular expression: Document matches this regular expression</source>
 | 
				
			||||||
 | 
					        <context-group purpose="location">
 | 
				
			||||||
 | 
					          <context context-type="sourcefile">src/app/data/matching-model.ts</context>
 | 
				
			||||||
 | 
					          <context context-type="linenumber">15</context>
 | 
				
			||||||
 | 
					        </context-group>
 | 
				
			||||||
 | 
					      </trans-unit>
 | 
				
			||||||
 | 
					      <trans-unit id="1856513373880048959" datatype="html">
 | 
				
			||||||
 | 
					        <source>Fuzzy word</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/data/matching-model.ts</context>
 | 
					          <context context-type="sourcefile">src/app/data/matching-model.ts</context>
 | 
				
			||||||
          <context context-type="linenumber">16</context>
 | 
					          <context context-type="linenumber">16</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="616064537937996961" datatype="html">
 | 
					      <trans-unit id="8419167206585286450" datatype="html">
 | 
				
			||||||
        <source>Auto</source>
 | 
					        <source>Fuzzy: Document contains a word similar to this word</source>
 | 
				
			||||||
 | 
					        <context-group purpose="location">
 | 
				
			||||||
 | 
					          <context context-type="sourcefile">src/app/data/matching-model.ts</context>
 | 
				
			||||||
 | 
					          <context context-type="linenumber">16</context>
 | 
				
			||||||
 | 
					        </context-group>
 | 
				
			||||||
 | 
					      </trans-unit>
 | 
				
			||||||
 | 
					      <trans-unit id="2167862279705099846" datatype="html">
 | 
				
			||||||
 | 
					        <source>Auto: Learn matching automatically</source>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/data/matching-model.ts</context>
 | 
					          <context context-type="sourcefile">src/app/data/matching-model.ts</context>
 | 
				
			||||||
          <context context-type="linenumber">17</context>
 | 
					          <context context-type="linenumber">17</context>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
import { Component } from '@angular/core';
 | 
					import { Component } from '@angular/core';
 | 
				
			||||||
 | 
					import { SettingsService } from './services/settings.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Component({
 | 
					@Component({
 | 
				
			||||||
  selector: 'app-root',
 | 
					  selector: 'app-root',
 | 
				
			||||||
@@ -7,8 +8,10 @@ import { Component } from '@angular/core';
 | 
				
			|||||||
})
 | 
					})
 | 
				
			||||||
export class AppComponent {
 | 
					export class AppComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor () {
 | 
					  constructor (private settings: SettingsService) {
 | 
				
			||||||
 | 
					    let anyWindow = (window as any)
 | 
				
			||||||
 | 
					    anyWindow.pdfWorkerSrc = '/assets/js/pdf.worker.min.js';
 | 
				
			||||||
 | 
					    this.settings.updateDarkModeSettings()
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,17 +1,52 @@
 | 
				
			|||||||
<nav class="navbar navbar-dark sticky-top bg-primary flex-md-nowrap p-0 shadow">
 | 
					<nav class="navbar navbar-dark sticky-top bg-primary flex-md-nowrap p-0 shadow">
 | 
				
			||||||
  <a class="navbar-brand col-md-3 col-lg-2 mr-0 px-3" routerLink="/dashboard">
 | 
					  <button class="navbar-toggler d-md-none collapsed border-0" type="button" data-toggle="collapse"
 | 
				
			||||||
    <img src="assets/logo-dark-notext.svg" height="18px" class="mr-2">
 | 
					 | 
				
			||||||
    <ng-container i18n="app title">Paperless-ng</ng-container>
 | 
					 | 
				
			||||||
  </a>
 | 
					 | 
				
			||||||
  <button class="navbar-toggler position-absolute d-md-none collapsed" type="button" data-toggle="collapse"
 | 
					 | 
				
			||||||
    data-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation"
 | 
					    data-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation"
 | 
				
			||||||
    (click)="isMenuCollapsed = !isMenuCollapsed">
 | 
					    (click)="isMenuCollapsed = !isMenuCollapsed">
 | 
				
			||||||
    <span class="navbar-toggler-icon"></span>
 | 
					    <span class="navbar-toggler-icon"></span>
 | 
				
			||||||
  </button>
 | 
					  </button>
 | 
				
			||||||
  <form (ngSubmit)="search()" class="w-100 m-1">
 | 
					  <a class="navbar-brand col-auto col-md-3 col-lg-2 mr-0 px-3 py-3 order-sm-0" routerLink="/dashboard">
 | 
				
			||||||
    <input class="form-control form-control-dark" type="text" placeholder="Search for documents" aria-label="Search"
 | 
					    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.43 238.91" width="1rem" class="mr-2" fill="currentColor">
 | 
				
			||||||
      [formControl]="searchField" [ngbTypeahead]="searchAutoComplete" (selectItem)="itemSelected($event)" i18n-placeholder>
 | 
					      <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)"/>
 | 
				
			||||||
  </form>
 | 
					    </svg>
 | 
				
			||||||
 | 
					    <ng-container i18n="app title">Paperless-ng</ng-container>
 | 
				
			||||||
 | 
					  </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">
 | 
				
			||||||
 | 
					    <form (ngSubmit)="search()" class="form-inline flex-grow-1">
 | 
				
			||||||
 | 
					      <input class="form-control form-control-sm" type="text" placeholder="Search documents" aria-label="Search"
 | 
				
			||||||
 | 
					        [formControl]="searchField" [ngbTypeahead]="searchAutoComplete" (selectItem)="itemSelected($event)" i18n-placeholder>
 | 
				
			||||||
 | 
					      <svg width="1em" height="1em">
 | 
				
			||||||
 | 
					        <use xlink:href="assets/bootstrap-icons.svg#search"/>
 | 
				
			||||||
 | 
					      </svg>
 | 
				
			||||||
 | 
					    </form>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					  <ul ngbNav class="order-sm-3">
 | 
				
			||||||
 | 
					    <li ngbDropdown class="nav-item dropdown">
 | 
				
			||||||
 | 
					      <button class="btn text-light" id="userDropdown" ngbDropdownToggle>
 | 
				
			||||||
 | 
					        <span *ngIf="displayName" class="navbar-text small mr-2 text-light d-none d-sm-inline">
 | 
				
			||||||
 | 
					          {{displayName}}
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					        <svg width="1.3em" height="1.3em">
 | 
				
			||||||
 | 
					          <use xlink:href="assets/bootstrap-icons.svg#person-circle"/>
 | 
				
			||||||
 | 
					        </svg>
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					      <div ngbDropdownMenu class="dropdown-menu-right shadow mr-2" aria-labelledby="userDropdown">
 | 
				
			||||||
 | 
					        <div *ngIf="displayName" class="d-sm-none">
 | 
				
			||||||
 | 
					          <p class="small mb-0 px-3" i18n>Logged in as {{displayName}}</p>
 | 
				
			||||||
 | 
					          <div class="dropdown-divider"></div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <a ngbDropdownItem class="nav-link" routerLink="settings" (click)="closeMenu()">
 | 
				
			||||||
 | 
					          <svg class="sidebaricon mr-2" fill="currentColor">
 | 
				
			||||||
 | 
					            <use xlink:href="assets/bootstrap-icons.svg#gear"/>
 | 
				
			||||||
 | 
					          </svg><ng-container i18n>Settings</ng-container>
 | 
				
			||||||
 | 
					        </a>
 | 
				
			||||||
 | 
					        <a ngbDropdownItem class="nav-link" href="accounts/logout/">
 | 
				
			||||||
 | 
					          <svg class="sidebaricon mr-2" fill="currentColor">
 | 
				
			||||||
 | 
					            <use xlink:href="assets/bootstrap-icons.svg#door-open"/>
 | 
				
			||||||
 | 
					          </svg><ng-container i18n>Logout</ng-container>
 | 
				
			||||||
 | 
					        </a>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </li>
 | 
				
			||||||
 | 
					  </ul>
 | 
				
			||||||
</nav>
 | 
					</nav>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="container-fluid">
 | 
					<div class="container-fluid">
 | 
				
			||||||
@@ -105,13 +140,6 @@
 | 
				
			|||||||
              </svg> <ng-container i18n>Logs</ng-container>
 | 
					              </svg> <ng-container i18n>Logs</ng-container>
 | 
				
			||||||
            </a>
 | 
					            </a>
 | 
				
			||||||
          </li>
 | 
					          </li>
 | 
				
			||||||
          <li class="nav-item">
 | 
					 | 
				
			||||||
            <a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()">
 | 
					 | 
				
			||||||
              <svg class="sidebaricon" fill="currentColor">
 | 
					 | 
				
			||||||
                <use xlink:href="assets/bootstrap-icons.svg#gear"/>
 | 
					 | 
				
			||||||
              </svg> <ng-container i18n>Settings</ng-container>
 | 
					 | 
				
			||||||
            </a>
 | 
					 | 
				
			||||||
          </li>
 | 
					 | 
				
			||||||
          <li class="nav-item">
 | 
					          <li class="nav-item">
 | 
				
			||||||
            <a class="nav-link" href="admin/">
 | 
					            <a class="nav-link" href="admin/">
 | 
				
			||||||
              <svg class="sidebaricon" fill="currentColor">
 | 
					              <svg class="sidebaricon" fill="currentColor">
 | 
				
			||||||
@@ -139,13 +167,6 @@
 | 
				
			|||||||
              </svg> <ng-container i18n>GitHub</ng-container>
 | 
					              </svg> <ng-container i18n>GitHub</ng-container>
 | 
				
			||||||
            </a>
 | 
					            </a>
 | 
				
			||||||
          </li>
 | 
					          </li>
 | 
				
			||||||
          <li class="nav-item">
 | 
					 | 
				
			||||||
            <a class="nav-link" href="accounts/logout/">
 | 
					 | 
				
			||||||
              <svg class="sidebaricon" fill="currentColor">
 | 
					 | 
				
			||||||
                <use xlink:href="assets/bootstrap-icons.svg#door-open"/>
 | 
					 | 
				
			||||||
              </svg> <ng-container i18n>Logout</ng-container>
 | 
					 | 
				
			||||||
            </a>
 | 
					 | 
				
			||||||
          </li>
 | 
					 | 
				
			||||||
        </ul>
 | 
					        </ul>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </nav>
 | 
					    </nav>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,36 +1,30 @@
 | 
				
			|||||||
 | 
					 | 
				
			||||||
@import "/src/theme";
 | 
					@import "/src/theme";
 | 
				
			||||||
 | 
					/*
 | 
				
			||||||
  /*
 | 
					 | 
				
			||||||
 * Sidebar
 | 
					 * Sidebar
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					.sidebar {
 | 
				
			||||||
 .sidebar {
 | 
					 | 
				
			||||||
  position: fixed;
 | 
					  position: fixed;
 | 
				
			||||||
  top: 0;
 | 
					  top: 0;
 | 
				
			||||||
  bottom: 0;
 | 
					  bottom: 0;
 | 
				
			||||||
  left: 0;
 | 
					  left: 0;
 | 
				
			||||||
  z-index: 100; /* Behind the navbar */
 | 
					  z-index: 100; /* Behind the navbar */
 | 
				
			||||||
  padding: 48px 0 0; /* Height of navbar */
 | 
					  padding: 50px 0 0; /* Height of navbar */
 | 
				
			||||||
  box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
 | 
					  box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
@media (max-width: 767.98px) {
 | 
					@media (max-width: 767.98px) {
 | 
				
			||||||
  .sidebar {
 | 
					  .sidebar {
 | 
				
			||||||
    top: 3rem;
 | 
					    top: 3.5rem;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.sidebar-sticky {
 | 
					.sidebar-sticky {
 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
  top: 0;
 | 
					  top: 0;
 | 
				
			||||||
  /* height: calc(100vh - 48px); */
 | 
					 | 
				
			||||||
  height: 100%;
 | 
					  height: 100%;
 | 
				
			||||||
  padding-top: .5rem;
 | 
					  padding-top: 0.5rem;
 | 
				
			||||||
  overflow-x: hidden;
 | 
					  overflow-x: hidden;
 | 
				
			||||||
  overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
 | 
					  overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
@supports ((position: -webkit-sticky) or (position: sticky)) {
 | 
					@supports ((position: -webkit-sticky) or (position: sticky)) {
 | 
				
			||||||
  .sidebar-sticky {
 | 
					  .sidebar-sticky {
 | 
				
			||||||
    position: -webkit-sticky;
 | 
					    position: -webkit-sticky;
 | 
				
			||||||
@@ -53,36 +47,85 @@
 | 
				
			|||||||
  font-weight: bold;
 | 
					  font-weight: bold;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.sidebar .nav-link:hover .sidebaricon,
 | 
					.sidebar .nav-link.active .sidebaricon,
 | 
				
			||||||
.sidebar .nav-link.active .sidebaricon {
 | 
					.sidebar .nav-link:hover .sidebaricon {
 | 
				
			||||||
  color: inherit;
 | 
					  color: inherit;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.sidebar-heading {
 | 
					.sidebar-heading {
 | 
				
			||||||
  font-size: .75rem;
 | 
					  font-size: 0.75rem;
 | 
				
			||||||
  text-transform: uppercase;
 | 
					  text-transform: uppercase;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
/*
 | 
					/*
 | 
				
			||||||
 * Navbar
 | 
					 * Navbar
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 .navbar-brand {
 | 
					.navbar-brand {
 | 
				
			||||||
  padding-top: .75rem;
 | 
					  padding-top: 0.75rem;
 | 
				
			||||||
  padding-bottom: .75rem;
 | 
					  padding-bottom: 0.75rem;
 | 
				
			||||||
  font-size: 1rem;
 | 
					  font-size: 1rem;
 | 
				
			||||||
  background-color: rgba(0, 0, 0, .25);
 | 
					 | 
				
			||||||
  box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25);
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.navbar .navbar-toggler {
 | 
					.dropdown.show .dropdown-toggle,
 | 
				
			||||||
  top: .25rem;
 | 
					.dropdown-toggle:hover {
 | 
				
			||||||
  right: 1rem;
 | 
					  opacity: 0.7;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.navbar .form-control {
 | 
					.dropdown-toggle::after {
 | 
				
			||||||
  padding: .75rem 1rem;
 | 
					  margin-left: 0.4em;
 | 
				
			||||||
  border-width: 0;
 | 
					  vertical-align: 0.155em;
 | 
				
			||||||
  border-radius: 0;
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.navbar .dropdown-menu {
 | 
				
			||||||
 | 
					  font-size: 0.875rem; // body size
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  a svg {
 | 
				
			||||||
 | 
					    opacity: 0.6;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.navbar .search-form-container {
 | 
				
			||||||
 | 
					  max-width: 550px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  form {
 | 
				
			||||||
 | 
					    position: relative;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  svg {
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    left: 0.6rem;
 | 
				
			||||||
 | 
					    color: rgba(255, 255, 255, 0.6);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:focus-within {
 | 
				
			||||||
 | 
					    svg {
 | 
				
			||||||
 | 
					      display: none;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .form-control::placeholder {
 | 
				
			||||||
 | 
					      color: rgba(255, 255, 255, 0);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .form-control {
 | 
				
			||||||
 | 
					    color: rgba(255, 255, 255, 0.3);
 | 
				
			||||||
 | 
					    background-color: rgba(0, 0, 0, 0.15);
 | 
				
			||||||
 | 
					    padding-left: 1.8rem;
 | 
				
			||||||
 | 
					    border-color: rgba(255, 255, 255, 0.2);
 | 
				
			||||||
 | 
					    transition: flex 0.3s ease;
 | 
				
			||||||
 | 
					    max-width: 600px;
 | 
				
			||||||
 | 
					    min-width: 300px; // 1/2 max
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &::placeholder {
 | 
				
			||||||
 | 
					      color: rgba(255, 255, 255, 0.4);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:focus {
 | 
				
			||||||
 | 
					      background-color: #fff;
 | 
				
			||||||
 | 
					      color: #212529;
 | 
				
			||||||
 | 
					      flex-grow: 1;
 | 
				
			||||||
 | 
					      padding-left: 0.5rem;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,7 @@ import { SavedViewService } from 'src/app/services/rest/saved-view.service';
 | 
				
			|||||||
import { SearchService } from 'src/app/services/rest/search.service';
 | 
					import { SearchService } from 'src/app/services/rest/search.service';
 | 
				
			||||||
import { environment } from 'src/environments/environment';
 | 
					import { environment } from 'src/environments/environment';
 | 
				
			||||||
import { DocumentDetailComponent } from '../document-detail/document-detail.component';
 | 
					import { DocumentDetailComponent } from '../document-detail/document-detail.component';
 | 
				
			||||||
 | 
					import { Meta } from '@angular/platform-browser';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Component({
 | 
					@Component({
 | 
				
			||||||
  selector: 'app-app-frame',
 | 
					  selector: 'app-app-frame',
 | 
				
			||||||
@@ -22,8 +23,10 @@ export class AppFrameComponent implements OnInit, OnDestroy {
 | 
				
			|||||||
    private activatedRoute: ActivatedRoute,
 | 
					    private activatedRoute: ActivatedRoute,
 | 
				
			||||||
    private openDocumentsService: OpenDocumentsService,
 | 
					    private openDocumentsService: OpenDocumentsService,
 | 
				
			||||||
    private searchService: SearchService,
 | 
					    private searchService: SearchService,
 | 
				
			||||||
    public savedViewService: SavedViewService
 | 
					    public savedViewService: SavedViewService,
 | 
				
			||||||
 | 
					    private meta: Meta
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  versionString = `${environment.appTitle} ${environment.version}`
 | 
					  versionString = `${environment.appTitle} ${environment.version}`
 | 
				
			||||||
@@ -98,4 +101,17 @@ export class AppFrameComponent implements OnInit, OnDestroy {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  get displayName() {
 | 
				
			||||||
 | 
					    // TODO: taken from dashboard component, is this the best way to pass around username?
 | 
				
			||||||
 | 
					    let tagFullName = this.meta.getTag('name=full_name')
 | 
				
			||||||
 | 
					    let tagUsername = this.meta.getTag('name=username')
 | 
				
			||||||
 | 
					    if (tagFullName && tagFullName.content) {
 | 
				
			||||||
 | 
					      return tagFullName.content
 | 
				
			||||||
 | 
					    } else if (tagUsername && tagUsername.content) {
 | 
				
			||||||
 | 
					      return tagUsername.content
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      return null
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					table {
 | 
				
			||||||
 | 
					  overflow-wrap: anywhere;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					th:first-child {
 | 
				
			||||||
 | 
					  min-width: 5rem;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,7 +16,7 @@
 | 
				
			|||||||
      <tbody>
 | 
					      <tbody>
 | 
				
			||||||
          <tr *ngFor="let m of metadata">
 | 
					          <tr *ngFor="let m of metadata">
 | 
				
			||||||
              <td>{{m.prefix}}:{{m.key}}</td>
 | 
					              <td>{{m.prefix}}:{{m.key}}</td>
 | 
				
			||||||
              <td>{{m.value}}</td>
 | 
					              <td class="metadata-column">{{m.value}}</td>
 | 
				
			||||||
          </tr>
 | 
					          </tr>
 | 
				
			||||||
      </tbody>
 | 
					      </tbody>
 | 
				
			||||||
  </table>
 | 
					  </table>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					.metadata-column {
 | 
				
			||||||
 | 
					  overflow-wrap: anywhere;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -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">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -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 document-card" [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">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,14 +25,27 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
 | 
				
			|||||||
      switch(this.filterRules[0].rule_type) {
 | 
					      switch(this.filterRules[0].rule_type) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        case FILTER_CORRESPONDENT:
 | 
					        case FILTER_CORRESPONDENT:
 | 
				
			||||||
          return $localize`Correspondent: ${this.correspondents.find(c => c.id == +rule.value)?.name}`
 | 
					          if (rule.value) {
 | 
				
			||||||
 | 
					            return $localize`Correspondent: ${this.correspondents.find(c => c.id == +rule.value)?.name}`
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            return $localize`Without correspondent`
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        case FILTER_DOCUMENT_TYPE:
 | 
					        case FILTER_DOCUMENT_TYPE:
 | 
				
			||||||
          return $localize`Type: ${this.documentTypes.find(dt => dt.id == +rule.value)?.name}`
 | 
					          if (rule.value) {
 | 
				
			||||||
 | 
					            return $localize`Type: ${this.documentTypes.find(dt => dt.id == +rule.value)?.name}`
 | 
				
			||||||
 | 
					          } else {
 | 
				
			||||||
 | 
					            return $localize`Without document type`
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        case FILTER_HAS_TAG:
 | 
					        case FILTER_HAS_TAG:
 | 
				
			||||||
          return $localize`Tag: ${this.tags.find(t => t.id == +rule.value)?.name}`
 | 
					          return $localize`Tag: ${this.tags.find(t => t.id == +rule.value)?.name}`
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
 | 
					        case FILTER_HAS_ANY_TAG:
 | 
				
			||||||
 | 
					          if (rule.value == "false") {
 | 
				
			||||||
 | 
					            return $localize`Without any tag`
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,7 +9,7 @@
 | 
				
			|||||||
    
 | 
					    
 | 
				
			||||||
    <app-input-text i18n-title title="Name" formControlName="name"></app-input-text>
 | 
					    <app-input-text i18n-title title="Name" formControlName="name"></app-input-text>
 | 
				
			||||||
    <app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
 | 
					    <app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
 | 
				
			||||||
    <app-input-text i18n-title title="Match" formControlName="match" i18n-hint hint="Auto matching does not require you to fill in this field."></app-input-text>
 | 
					    <app-input-text i18n-title title="Matching pattern" formControlName="match" i18n-hint hint="Auto matching does not require you to fill in this field."></app-input-text>
 | 
				
			||||||
    <app-input-check i18n-title title="Case insensitive" formControlName="is_insensitive" i18n-hint hint="Auto matching ignores this option."></app-input-check>
 | 
					    <app-input-check i18n-title title="Case insensitive" formControlName="is_insensitive" i18n-hint hint="Auto matching ignores this option."></app-input-check>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
  <div class="modal-footer">
 | 
					  <div class="modal-footer">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,7 +9,7 @@
 | 
				
			|||||||
      
 | 
					      
 | 
				
			||||||
      <app-input-text i18n-title title="Name" formControlName="name"></app-input-text>
 | 
					      <app-input-text i18n-title title="Name" formControlName="name"></app-input-text>
 | 
				
			||||||
      <app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
 | 
					      <app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
 | 
				
			||||||
      <app-input-text i18n-title title="Match" formControlName="match" i18n-hint hint="Auto matching does not require you to fill in this field."></app-input-text>
 | 
					      <app-input-text i18n-title title="Matching pattern" formControlName="match" i18n-hint hint="Auto matching does not require you to fill in this field."></app-input-text>
 | 
				
			||||||
      <app-input-check i18n-title title="Case insensitive" formControlName="is_insensitive" i18n-hint hint="Auto matching ignores this option."></app-input-check>
 | 
					      <app-input-check i18n-title title="Case insensitive" formControlName="is_insensitive" i18n-hint hint="Auto matching ignores this option."></app-input-check>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,7 +30,7 @@ export abstract class GenericListComponent<T extends ObjectWithId> implements On
 | 
				
			|||||||
    if (o.matching_algorithm == MATCH_AUTO) {
 | 
					    if (o.matching_algorithm == MATCH_AUTO) {
 | 
				
			||||||
      return $localize`Automatic`
 | 
					      return $localize`Automatic`
 | 
				
			||||||
    } else if (o.match && o.match.length > 0) {
 | 
					    } else if (o.match && o.match.length > 0) {
 | 
				
			||||||
      return `${o.match} (${MATCHING_ALGORITHMS.find(a => a.id == o.matching_algorithm).name})`
 | 
					      return `${MATCHING_ALGORITHMS.find(a => a.id == o.matching_algorithm).shortName}: ${o.match}`
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      return "-"
 | 
					      return "-"
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
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';
 | 
				
			||||||
@@ -19,9 +19,13 @@ 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,
 | 
				
			||||||
@@ -29,8 +33,6 @@ export class SettingsComponent implements OnInit {
 | 
				
			|||||||
    private settings: SettingsService
 | 
					    private settings: SettingsService
 | 
				
			||||||
  ) { }
 | 
					  ) { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  savedViews: PaperlessSavedView[]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  ngOnInit() {
 | 
					  ngOnInit() {
 | 
				
			||||||
    this.savedViewService.listAll().subscribe(r => {
 | 
					    this.savedViewService.listAll().subscribe(r => {
 | 
				
			||||||
      this.savedViews = r.results
 | 
					      this.savedViews = r.results
 | 
				
			||||||
@@ -53,11 +55,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.settings.updateDarkModeSettings()
 | 
				
			||||||
    this.toastService.showInfo($localize`Settings saved successfully.`)
 | 
					    this.toastService.showInfo($localize`Settings saved successfully.`)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,7 +20,7 @@
 | 
				
			|||||||
     
 | 
					     
 | 
				
			||||||
      <app-input-check i18n-title title="Inbox tag" formControlName="is_inbox_tag" i18n-hint hint="Inbox tags are automatically assigned to all consumed documents."></app-input-check>
 | 
					      <app-input-check i18n-title title="Inbox tag" formControlName="is_inbox_tag" i18n-hint hint="Inbox tags are automatically assigned to all consumed documents."></app-input-check>
 | 
				
			||||||
      <app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
 | 
					      <app-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
 | 
				
			||||||
      <app-input-text i18n-title title="Match" formControlName="match" i18n-hint hint="Auto matching does not require you to fill in this field."></app-input-text>
 | 
					      <app-input-text i18n-title title="Matching pattern" formControlName="match" i18n-hint hint="Auto matching does not require you to fill in this field."></app-input-text>
 | 
				
			||||||
      <app-input-check i18n-title title="Case insensitive" formControlName="is_insensitive" i18n-hint hint="Auto matching ignores this option."></app-input-check>
 | 
					      <app-input-check i18n-title title="Case insensitive" formControlName="is_insensitive" i18n-hint hint="Auto matching ignores this option."></app-input-check>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div class="modal-footer">
 | 
					    <div class="modal-footer">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,12 +9,12 @@ export const MATCH_FUZZY = 5
 | 
				
			|||||||
export const MATCH_AUTO = 6
 | 
					export const MATCH_AUTO = 6
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const MATCHING_ALGORITHMS = [
 | 
					export const MATCHING_ALGORITHMS = [
 | 
				
			||||||
    {id: MATCH_ANY, name: $localize`Any`},
 | 
					    {id: MATCH_ANY, shortName: $localize`Any word`, name: $localize`Any: Document contains any of these words (space separated)`},
 | 
				
			||||||
    {id: MATCH_ALL, name: $localize`All`},
 | 
					    {id: MATCH_ALL, shortName: $localize`All words`, name: $localize`All: Document contains all of these words (space separated)`},
 | 
				
			||||||
    {id: MATCH_LITERAL, name: $localize`Literal`},
 | 
					    {id: MATCH_LITERAL, shortName: $localize`Exact match`, name: $localize`Exact: Document contains this string`},
 | 
				
			||||||
    {id: MATCH_REGEX, name: $localize`Regular expression`},
 | 
					    {id: MATCH_REGEX, shortName: $localize`Regular expression`, name: $localize`Regular expression: Document matches this regular expression`},
 | 
				
			||||||
    {id: MATCH_FUZZY, name: $localize`Fuzzy match`},
 | 
					    {id: MATCH_FUZZY, shortName: $localize`Fuzzy word`, name: $localize`Fuzzy: Document contains a word similar to this word`},
 | 
				
			||||||
    {id: MATCH_AUTO, name: $localize`Auto`},
 | 
					    {id: MATCH_AUTO, shortName: $localize`Automatic`, name: $localize`Auto: Learn matching automatically`},
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface MatchingModel extends ObjectWithId {
 | 
					export interface MatchingModel extends ObjectWithId {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
import { Injectable } from '@angular/core';
 | 
					import { DOCUMENT } from '@angular/common';
 | 
				
			||||||
 | 
					import { Inject, Injectable, Renderer2, RendererFactory2 } from '@angular/core';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface PaperlessSettings {
 | 
					export interface PaperlessSettings {
 | 
				
			||||||
  key: string
 | 
					  key: string
 | 
				
			||||||
@@ -10,12 +11,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({
 | 
				
			||||||
@@ -23,7 +28,30 @@ const SETTINGS: PaperlessSettings[] = [
 | 
				
			|||||||
})
 | 
					})
 | 
				
			||||||
export class SettingsService {
 | 
					export class SettingsService {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor() { }
 | 
					  private renderer: Renderer2;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor(
 | 
				
			||||||
 | 
					    private rendererFactory: RendererFactory2,
 | 
				
			||||||
 | 
					    @Inject(DOCUMENT) private document
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    this.renderer = rendererFactory.createRenderer(null, null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.updateDarkModeSettings()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  updateDarkModeSettings(): void {
 | 
				
			||||||
 | 
					    let darkModeUseSystem = this.get(SETTINGS_KEYS.DARK_MODE_USE_SYSTEM)
 | 
				
			||||||
 | 
					    let darkModeEnabled = this.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')
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get(key: string): any {
 | 
					  get(key: string): any {
 | 
				
			||||||
    let setting = SETTINGS.find(s => s.key == key)
 | 
					    let setting = SETTINGS.find(s => s.key == key)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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  | 
							
								
								
									
										69
									
								
								src-ui/src/assets/logo-white-notext.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								src-ui/src/assets/logo-white-notext.svg
									
									
									
									
									
										Normal 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  | 
@@ -5,11 +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" />
 | 
					  <meta name="theme-color" content="#17541f" />
 | 
				
			||||||
  <link rel="manifest" href="manifest.webmanifest">
 | 
					  <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>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1923
									
								
								src-ui/src/locale/messages.de_DE.xlf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1923
									
								
								src-ui/src/locale/messages.de_DE.xlf
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -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";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										337
									
								
								src-ui/src/theme_dark.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										337
									
								
								src-ui/src/theme_dark.scss
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,337 @@
 | 
				
			|||||||
 | 
					$primary-dark-mode: #45973a;
 | 
				
			||||||
 | 
					$danger-dark-mode: #b71631;
 | 
				
			||||||
 | 
					$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;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .text-light {
 | 
				
			||||||
 | 
					    color: $text-color-dark-mode !important;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .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,
 | 
				
			||||||
 | 
					  .card-title a {
 | 
				
			||||||
 | 
					    color: $primary-dark-mode;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:hover {
 | 
				
			||||||
 | 
					      color: lighten($primary, 10%);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  table {
 | 
				
			||||||
 | 
					    background-color: $bg-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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:not(:disabled):not(.disabled).active,
 | 
				
			||||||
 | 
					    &:not(:disabled):not(.disabled):hover {
 | 
				
			||||||
 | 
					      background-color: darken($primary-dark-mode, 10%);
 | 
				
			||||||
 | 
					      border-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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:not(:disabled):not(.disabled):hover {
 | 
				
			||||||
 | 
					      background-color: $bg-dark-mode;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .btn-outline-danger {
 | 
				
			||||||
 | 
					    border-color: $danger-dark-mode;
 | 
				
			||||||
 | 
					    color: $danger-dark-mode;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:not(:disabled):not(.disabled):hover {
 | 
				
			||||||
 | 
					      background-color: darken($danger-dark-mode, 10%);
 | 
				
			||||||
 | 
					      border-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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:not(:disabled):not(.disabled):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;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -6,29 +6,21 @@ class DocumentsConfig(AppConfig):
 | 
				
			|||||||
    name = "documents"
 | 
					    name = "documents"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def ready(self):
 | 
					    def ready(self):
 | 
				
			||||||
 | 
					 | 
				
			||||||
        from .signals import document_consumption_started
 | 
					 | 
				
			||||||
        from .signals import document_consumption_finished
 | 
					        from .signals import document_consumption_finished
 | 
				
			||||||
        from .signals.handlers import (
 | 
					        from .signals.handlers import (
 | 
				
			||||||
            add_inbox_tags,
 | 
					            add_inbox_tags,
 | 
				
			||||||
            run_pre_consume_script,
 | 
					 | 
				
			||||||
            run_post_consume_script,
 | 
					 | 
				
			||||||
            set_log_entry,
 | 
					            set_log_entry,
 | 
				
			||||||
            set_correspondent,
 | 
					            set_correspondent,
 | 
				
			||||||
            set_document_type,
 | 
					            set_document_type,
 | 
				
			||||||
            set_tags,
 | 
					            set_tags,
 | 
				
			||||||
            add_to_index
 | 
					            add_to_index
 | 
				
			||||||
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        document_consumption_started.connect(run_pre_consume_script)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        document_consumption_finished.connect(add_inbox_tags)
 | 
					        document_consumption_finished.connect(add_inbox_tags)
 | 
				
			||||||
        document_consumption_finished.connect(set_correspondent)
 | 
					        document_consumption_finished.connect(set_correspondent)
 | 
				
			||||||
        document_consumption_finished.connect(set_document_type)
 | 
					        document_consumption_finished.connect(set_document_type)
 | 
				
			||||||
        document_consumption_finished.connect(set_tags)
 | 
					        document_consumption_finished.connect(set_tags)
 | 
				
			||||||
        document_consumption_finished.connect(set_log_entry)
 | 
					        document_consumption_finished.connect(set_log_entry)
 | 
				
			||||||
        document_consumption_finished.connect(add_to_index)
 | 
					        document_consumption_finished.connect(add_to_index)
 | 
				
			||||||
        document_consumption_finished.connect(run_post_consume_script)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        AppConfig.ready(self)
 | 
					        AppConfig.ready(self)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
import datetime
 | 
					import datetime
 | 
				
			||||||
import hashlib
 | 
					import hashlib
 | 
				
			||||||
import logging
 | 
					 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
 | 
					from subprocess import Popen
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import magic
 | 
					import magic
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
@@ -9,6 +9,7 @@ from django.db import transaction
 | 
				
			|||||||
from django.db.models import Q
 | 
					from django.db.models import Q
 | 
				
			||||||
from django.utils import timezone
 | 
					from django.utils import timezone
 | 
				
			||||||
from filelock import FileLock
 | 
					from filelock import FileLock
 | 
				
			||||||
 | 
					from rest_framework.reverse import reverse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .classifier import DocumentClassifier, IncompatibleClassifierVersionError
 | 
					from .classifier import DocumentClassifier, IncompatibleClassifierVersionError
 | 
				
			||||||
from .file_handling import create_source_path_directory, \
 | 
					from .file_handling import create_source_path_directory, \
 | 
				
			||||||
@@ -66,6 +67,39 @@ class Consumer(LoggingMixin):
 | 
				
			|||||||
        os.makedirs(settings.ORIGINALS_DIR, exist_ok=True)
 | 
					        os.makedirs(settings.ORIGINALS_DIR, exist_ok=True)
 | 
				
			||||||
        os.makedirs(settings.ARCHIVE_DIR, exist_ok=True)
 | 
					        os.makedirs(settings.ARCHIVE_DIR, exist_ok=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def run_pre_consume_script(self):
 | 
				
			||||||
 | 
					        if not settings.PRE_CONSUME_SCRIPT:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            Popen((settings.PRE_CONSUME_SCRIPT, self.path)).wait()
 | 
				
			||||||
 | 
					        except Exception as e:
 | 
				
			||||||
 | 
					            raise ConsumerError(
 | 
				
			||||||
 | 
					                f"Error while executing pre-consume script: {e}"
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def run_post_consume_script(self, document):
 | 
				
			||||||
 | 
					        if not settings.POST_CONSUME_SCRIPT:
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            Popen((
 | 
				
			||||||
 | 
					                settings.POST_CONSUME_SCRIPT,
 | 
				
			||||||
 | 
					                str(document.pk),
 | 
				
			||||||
 | 
					                document.get_public_filename(),
 | 
				
			||||||
 | 
					                os.path.normpath(document.source_path),
 | 
				
			||||||
 | 
					                os.path.normpath(document.thumbnail_path),
 | 
				
			||||||
 | 
					                reverse("document-download", kwargs={"pk": document.pk}),
 | 
				
			||||||
 | 
					                reverse("document-thumb", kwargs={"pk": document.pk}),
 | 
				
			||||||
 | 
					                str(document.correspondent),
 | 
				
			||||||
 | 
					                str(",".join(document.tags.all().values_list(
 | 
				
			||||||
 | 
					                    "name", flat=True)))
 | 
				
			||||||
 | 
					            )).wait()
 | 
				
			||||||
 | 
					        except Exception as e:
 | 
				
			||||||
 | 
					            raise ConsumerError(
 | 
				
			||||||
 | 
					                f"Error while executing pre-consume script: {e}"
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def try_consume_file(self,
 | 
					    def try_consume_file(self,
 | 
				
			||||||
                         path,
 | 
					                         path,
 | 
				
			||||||
                         override_filename=None,
 | 
					                         override_filename=None,
 | 
				
			||||||
@@ -119,6 +153,8 @@ class Consumer(LoggingMixin):
 | 
				
			|||||||
            logging_group=self.logging_group
 | 
					            logging_group=self.logging_group
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.run_pre_consume_script()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # This doesn't parse the document yet, but gives us a parser.
 | 
					        # This doesn't parse the document yet, but gives us a parser.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        document_parser = parser_class(self.logging_group)
 | 
					        document_parser = parser_class(self.logging_group)
 | 
				
			||||||
@@ -130,7 +166,7 @@ class Consumer(LoggingMixin):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            self.log("debug", "Parsing {}...".format(self.filename))
 | 
					            self.log("debug", "Parsing {}...".format(self.filename))
 | 
				
			||||||
            document_parser.parse(self.path, mime_type)
 | 
					            document_parser.parse(self.path, mime_type, self.filename)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            self.log("debug", f"Generating thumbnail for {self.filename}...")
 | 
					            self.log("debug", f"Generating thumbnail for {self.filename}...")
 | 
				
			||||||
            thumbnail = document_parser.get_optimised_thumbnail(
 | 
					            thumbnail = document_parser.get_optimised_thumbnail(
 | 
				
			||||||
@@ -215,6 +251,9 @@ class Consumer(LoggingMixin):
 | 
				
			|||||||
                # Delete the file only if it was successfully consumed
 | 
					                # Delete the file only if it was successfully consumed
 | 
				
			||||||
                self.log("debug", "Deleting file {}".format(self.path))
 | 
					                self.log("debug", "Deleting file {}".format(self.path))
 | 
				
			||||||
                os.unlink(self.path)
 | 
					                os.unlink(self.path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                self.run_post_consume_script(document)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        except Exception as e:
 | 
					        except Exception as e:
 | 
				
			||||||
            self.log(
 | 
					            self.log(
 | 
				
			||||||
                "error",
 | 
					                "error",
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										18
									
								
								src/documents/migrations/1010_auto_20210101_2159.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/documents/migrations/1010_auto_20210101_2159.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					# Generated by Django 3.1.4 on 2021-01-01 21:59
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('documents', '1009_auto_20201216_2005'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AlterField(
 | 
				
			||||||
 | 
					            model_name='savedviewfilterrule',
 | 
				
			||||||
 | 
					            name='value',
 | 
				
			||||||
 | 
					            field=models.CharField(blank=True, max_length=128, null=True),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -404,7 +404,9 @@ class SavedViewFilterRule(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    value = models.CharField(
 | 
					    value = models.CharField(
 | 
				
			||||||
        _("value"),
 | 
					        _("value"),
 | 
				
			||||||
        max_length=128)
 | 
					        max_length=128,
 | 
				
			||||||
 | 
					        blank=True,
 | 
				
			||||||
 | 
					        null=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        verbose_name = _("filter rule")
 | 
					        verbose_name = _("filter rule")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -144,6 +144,52 @@ def run_convert(input_file,
 | 
				
			|||||||
        raise ParseError("Convert failed at {}".format(args))
 | 
					        raise ParseError("Convert failed at {}".format(args))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def make_thumbnail_from_pdf(in_path, temp_dir, logging_group=None):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    The thumbnail of a PDF is just a 500px wide image of the first page.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    out_path = os.path.join(temp_dir, "convert.png")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Run convert to get a decent thumbnail
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        run_convert(density=300,
 | 
				
			||||||
 | 
					                    scale="500x5000>",
 | 
				
			||||||
 | 
					                    alpha="remove",
 | 
				
			||||||
 | 
					                    strip=True,
 | 
				
			||||||
 | 
					                    trim=False,
 | 
				
			||||||
 | 
					                    auto_orient=True,
 | 
				
			||||||
 | 
					                    input_file="{}[0]".format(in_path),
 | 
				
			||||||
 | 
					                    output_file=out_path,
 | 
				
			||||||
 | 
					                    logging_group=logging_group)
 | 
				
			||||||
 | 
					    except ParseError:
 | 
				
			||||||
 | 
					        # if convert fails, fall back to extracting
 | 
				
			||||||
 | 
					        # the first PDF page as a PNG using Ghostscript
 | 
				
			||||||
 | 
					        logger.warning(
 | 
				
			||||||
 | 
					            "Thumbnail generation with ImageMagick failed, falling back "
 | 
				
			||||||
 | 
					            "to ghostscript. Check your /etc/ImageMagick-x/policy.xml!",
 | 
				
			||||||
 | 
					            extra={'group': logging_group}
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        gs_out_path = os.path.join(temp_dir, "gs_out.png")
 | 
				
			||||||
 | 
					        cmd = [settings.GS_BINARY,
 | 
				
			||||||
 | 
					               "-q",
 | 
				
			||||||
 | 
					               "-sDEVICE=pngalpha",
 | 
				
			||||||
 | 
					               "-o", gs_out_path,
 | 
				
			||||||
 | 
					               in_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,
 | 
				
			||||||
 | 
					                    auto_orient=True,
 | 
				
			||||||
 | 
					                    input_file=gs_out_path,
 | 
				
			||||||
 | 
					                    output_file=out_path,
 | 
				
			||||||
 | 
					                    logging_group=logging_group)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return out_path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def parse_date(filename, text):
 | 
					def parse_date(filename, text):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Returns the date of the document.
 | 
					    Returns the date of the document.
 | 
				
			||||||
@@ -221,7 +267,7 @@ class DocumentParser(LoggingMixin):
 | 
				
			|||||||
    def extract_metadata(self, document_path, mime_type):
 | 
					    def extract_metadata(self, document_path, mime_type):
 | 
				
			||||||
        return []
 | 
					        return []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def parse(self, document_path, mime_type):
 | 
					    def parse(self, document_path, mime_type, file_name=None):
 | 
				
			||||||
        raise NotImplementedError()
 | 
					        raise NotImplementedError()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_archive_path(self):
 | 
					    def get_archive_path(self):
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,7 +11,6 @@ from django.db.models import Q
 | 
				
			|||||||
from django.dispatch import receiver
 | 
					from django.dispatch import receiver
 | 
				
			||||||
from django.utils import timezone
 | 
					from django.utils import timezone
 | 
				
			||||||
from filelock import FileLock
 | 
					from filelock import FileLock
 | 
				
			||||||
from rest_framework.reverse import reverse
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .. import index, matching
 | 
					from .. import index, matching
 | 
				
			||||||
from ..file_handling import delete_empty_directories, \
 | 
					from ..file_handling import delete_empty_directories, \
 | 
				
			||||||
@@ -147,32 +146,6 @@ def set_tags(sender,
 | 
				
			|||||||
    document.tags.add(*relevant_tags)
 | 
					    document.tags.add(*relevant_tags)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def run_pre_consume_script(sender, filename, **kwargs):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if not settings.PRE_CONSUME_SCRIPT:
 | 
					 | 
				
			||||||
        return
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Popen((settings.PRE_CONSUME_SCRIPT, filename)).wait()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def run_post_consume_script(sender, document, **kwargs):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if not settings.POST_CONSUME_SCRIPT:
 | 
					 | 
				
			||||||
        return
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    Popen((
 | 
					 | 
				
			||||||
        settings.POST_CONSUME_SCRIPT,
 | 
					 | 
				
			||||||
        str(document.pk),
 | 
					 | 
				
			||||||
        document.get_public_filename(),
 | 
					 | 
				
			||||||
        os.path.normpath(document.source_path),
 | 
					 | 
				
			||||||
        os.path.normpath(document.thumbnail_path),
 | 
					 | 
				
			||||||
        reverse("document-download", kwargs={"pk": document.pk}),
 | 
					 | 
				
			||||||
        reverse("document-thumb", kwargs={"pk": document.pk}),
 | 
					 | 
				
			||||||
        str(document.correspondent),
 | 
					 | 
				
			||||||
        str(",".join(document.tags.all().values_list("name", flat=True)))
 | 
					 | 
				
			||||||
    )).wait()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@receiver(models.signals.post_delete, sender=Document)
 | 
					@receiver(models.signals.post_delete, sender=Document)
 | 
				
			||||||
def cleanup_document_deletion(sender, instance, using, **kwargs):
 | 
					def cleanup_document_deletion(sender, instance, using, **kwargs):
 | 
				
			||||||
    with FileLock(settings.MEDIA_LOCK):
 | 
					    with FileLock(settings.MEDIA_LOCK):
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -177,7 +177,7 @@ class DummyParser(DocumentParser):
 | 
				
			|||||||
    def get_optimised_thumbnail(self, document_path, mime_type):
 | 
					    def get_optimised_thumbnail(self, document_path, mime_type):
 | 
				
			||||||
        return self.fake_thumb
 | 
					        return self.fake_thumb
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def parse(self, document_path, mime_type):
 | 
					    def parse(self, document_path, mime_type, file_name=None):
 | 
				
			||||||
        self.text = "The Text"
 | 
					        self.text = "The Text"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -194,7 +194,7 @@ class FaultyParser(DocumentParser):
 | 
				
			|||||||
    def get_optimised_thumbnail(self, document_path, mime_type):
 | 
					    def get_optimised_thumbnail(self, document_path, mime_type):
 | 
				
			||||||
        return self.fake_thumb
 | 
					        return self.fake_thumb
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def parse(self, document_path, mime_type):
 | 
					    def parse(self, document_path, mime_type, file_name=None):
 | 
				
			||||||
        raise ParseError("Does not compute.")
 | 
					        raise ParseError("Does not compute.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -466,3 +466,53 @@ class TestConsumer(DirectoriesMixin, TestCase):
 | 
				
			|||||||
        self.assertTrue(os.path.isfile(dst))
 | 
					        self.assertTrue(os.path.isfile(dst))
 | 
				
			||||||
        self.assertRaises(ConsumerError, self.consumer.try_consume_file, dst)
 | 
					        self.assertRaises(ConsumerError, self.consumer.try_consume_file, dst)
 | 
				
			||||||
        self.assertTrue(os.path.isfile(dst))
 | 
					        self.assertTrue(os.path.isfile(dst))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class PostConsumeTestCase(TestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @mock.patch("documents.signals.handlers.Popen")
 | 
				
			||||||
 | 
					    @override_settings(POST_CONSUME_SCRIPT=None)
 | 
				
			||||||
 | 
					    def test_no_post_consume_script(self, m):
 | 
				
			||||||
 | 
					        doc = Document.objects.create(title="Test", mime_type="application/pdf")
 | 
				
			||||||
 | 
					        tag1 = Tag.objects.create(name="a")
 | 
				
			||||||
 | 
					        tag2 = Tag.objects.create(name="b")
 | 
				
			||||||
 | 
					        doc.tags.add(tag1)
 | 
				
			||||||
 | 
					        doc.tags.add(tag2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Consumer().run_post_consume_script(doc)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        m.assert_not_called()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @mock.patch("documents.signals.handlers.Popen")
 | 
				
			||||||
 | 
					    @override_settings(POST_CONSUME_SCRIPT="script")
 | 
				
			||||||
 | 
					    def test_post_consume_script_simple(self, m):
 | 
				
			||||||
 | 
					        doc = Document.objects.create(title="Test", mime_type="application/pdf")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Consumer().run_post_consume_script(doc)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        m.assert_called_once()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @mock.patch("documents.signals.handlers.Popen")
 | 
				
			||||||
 | 
					    @override_settings(POST_CONSUME_SCRIPT="script")
 | 
				
			||||||
 | 
					    def test_post_consume_script_with_correspondent(self, m):
 | 
				
			||||||
 | 
					        c = Correspondent.objects.create(name="my_bank")
 | 
				
			||||||
 | 
					        doc = Document.objects.create(title="Test", mime_type="application/pdf", correspondent=c)
 | 
				
			||||||
 | 
					        tag1 = Tag.objects.create(name="a")
 | 
				
			||||||
 | 
					        tag2 = Tag.objects.create(name="b")
 | 
				
			||||||
 | 
					        doc.tags.add(tag1)
 | 
				
			||||||
 | 
					        doc.tags.add(tag2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Consumer().run_post_consume_script(doc)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        m.assert_called_once()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        args, kwargs = m.call_args
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        command = args[0]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertEqual(command[0], "script")
 | 
				
			||||||
 | 
					        self.assertEqual(command[1], str(doc.pk))
 | 
				
			||||||
 | 
					        self.assertEqual(command[5], f"/api/documents/{doc.pk}/download/")
 | 
				
			||||||
 | 
					        self.assertEqual(command[6], f"/api/documents/{doc.pk}/thumb/")
 | 
				
			||||||
 | 
					        self.assertEqual(command[7], "my_bank")
 | 
				
			||||||
 | 
					        self.assertCountEqual(command[8].split(","), ["a", "b"])
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,56 +0,0 @@
 | 
				
			|||||||
from unittest import mock
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from django.test import TestCase, override_settings
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from documents.models import Document, Tag, Correspondent
 | 
					 | 
				
			||||||
from documents.signals.handlers import run_post_consume_script
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class PostConsumeTestCase(TestCase):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @mock.patch("documents.signals.handlers.Popen")
 | 
					 | 
				
			||||||
    @override_settings(POST_CONSUME_SCRIPT=None)
 | 
					 | 
				
			||||||
    def test_no_post_consume_script(self, m):
 | 
					 | 
				
			||||||
        doc = Document.objects.create(title="Test", mime_type="application/pdf")
 | 
					 | 
				
			||||||
        tag1 = Tag.objects.create(name="a")
 | 
					 | 
				
			||||||
        tag2 = Tag.objects.create(name="b")
 | 
					 | 
				
			||||||
        doc.tags.add(tag1)
 | 
					 | 
				
			||||||
        doc.tags.add(tag2)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        run_post_consume_script(None, doc)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        m.assert_not_called()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @mock.patch("documents.signals.handlers.Popen")
 | 
					 | 
				
			||||||
    @override_settings(POST_CONSUME_SCRIPT="script")
 | 
					 | 
				
			||||||
    def test_post_consume_script_simple(self, m):
 | 
					 | 
				
			||||||
        doc = Document.objects.create(title="Test", mime_type="application/pdf")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        run_post_consume_script(None, doc)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        m.assert_called_once()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @mock.patch("documents.signals.handlers.Popen")
 | 
					 | 
				
			||||||
    @override_settings(POST_CONSUME_SCRIPT="script")
 | 
					 | 
				
			||||||
    def test_post_consume_script_with_correspondent(self, m):
 | 
					 | 
				
			||||||
        c = Correspondent.objects.create(name="my_bank")
 | 
					 | 
				
			||||||
        doc = Document.objects.create(title="Test", mime_type="application/pdf", correspondent=c)
 | 
					 | 
				
			||||||
        tag1 = Tag.objects.create(name="a")
 | 
					 | 
				
			||||||
        tag2 = Tag.objects.create(name="b")
 | 
					 | 
				
			||||||
        doc.tags.add(tag1)
 | 
					 | 
				
			||||||
        doc.tags.add(tag2)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        run_post_consume_script(None, doc)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        m.assert_called_once()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        args, kwargs = m.call_args
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        command = args[0]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.assertEqual(command[0], "script")
 | 
					 | 
				
			||||||
        self.assertEqual(command[1], str(doc.pk))
 | 
					 | 
				
			||||||
        self.assertEqual(command[5], f"/api/documents/{doc.pk}/download/")
 | 
					 | 
				
			||||||
        self.assertEqual(command[6], f"/api/documents/{doc.pk}/thumb/")
 | 
					 | 
				
			||||||
        self.assertEqual(command[7], "my_bank")
 | 
					 | 
				
			||||||
        self.assertCountEqual(command[8].split(","), ["a", "b"])
 | 
					 | 
				
			||||||
@@ -89,6 +89,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",
 | 
				
			||||||
@@ -436,3 +437,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"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,6 @@
 | 
				
			|||||||
import json
 | 
					import json
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import re
 | 
					import re
 | 
				
			||||||
import subprocess
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import ocrmypdf
 | 
					import ocrmypdf
 | 
				
			||||||
import pdftotext
 | 
					import pdftotext
 | 
				
			||||||
@@ -10,7 +9,8 @@ from PIL import Image
 | 
				
			|||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
from ocrmypdf import InputFileError, EncryptedPdfError
 | 
					from ocrmypdf import InputFileError, EncryptedPdfError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from documents.parsers import DocumentParser, ParseError, run_convert
 | 
					from documents.parsers import DocumentParser, ParseError, \
 | 
				
			||||||
 | 
					    make_thumbnail_from_pdf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class RasterisedDocumentParser(DocumentParser):
 | 
					class RasterisedDocumentParser(DocumentParser):
 | 
				
			||||||
@@ -47,50 +47,8 @@ class RasterisedDocumentParser(DocumentParser):
 | 
				
			|||||||
        return result
 | 
					        return result
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_thumbnail(self, document_path, mime_type):
 | 
					    def get_thumbnail(self, document_path, mime_type):
 | 
				
			||||||
        """
 | 
					        return make_thumbnail_from_pdf(
 | 
				
			||||||
        The thumbnail of a PDF is just a 500px wide image of the first page.
 | 
					            document_path, self.tempdir, self.logging_group)
 | 
				
			||||||
        """
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        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,
 | 
					 | 
				
			||||||
                        auto_orient=True,
 | 
					 | 
				
			||||||
                        input_file="{}[0]".format(document_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,
 | 
					 | 
				
			||||||
                   document_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,
 | 
					 | 
				
			||||||
                        auto_orient=True,
 | 
					 | 
				
			||||||
                        input_file=gs_out_path,
 | 
					 | 
				
			||||||
                        output_file=out_path,
 | 
					 | 
				
			||||||
                        logging_group=self.logging_group)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return out_path
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def is_image(self, mime_type):
 | 
					    def is_image(self, mime_type):
 | 
				
			||||||
        return mime_type in [
 | 
					        return mime_type in [
 | 
				
			||||||
@@ -130,7 +88,7 @@ class RasterisedDocumentParser(DocumentParser):
 | 
				
			|||||||
                f"Error while calculating DPI for image {image}: {e}")
 | 
					                f"Error while calculating DPI for image {image}: {e}")
 | 
				
			||||||
            return None
 | 
					            return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def parse(self, document_path, mime_type):
 | 
					    def parse(self, document_path, mime_type, file_name=None):
 | 
				
			||||||
        mode = settings.OCR_MODE
 | 
					        mode = settings.OCR_MODE
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        text_original = get_text_from_pdf(document_path)
 | 
					        text_original = get_text_from_pdf(document_path)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,6 +32,6 @@ class TextDocumentParser(DocumentParser):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        return out_path
 | 
					        return out_path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def parse(self, document_path, mime_type):
 | 
					    def parse(self, document_path, mime_type, file_name=None):
 | 
				
			||||||
        with open(document_path, 'r') as f:
 | 
					        with open(document_path, 'r') as f:
 | 
				
			||||||
            self.text = f.read()
 | 
					            self.text = f.read()
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										14
									
								
								src/paperless_tika/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/paperless_tika/apps.py
									
									
									
									
									
										Normal 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)
 | 
				
			||||||
							
								
								
									
										86
									
								
								src/paperless_tika/parsers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								src/paperless_tika/parsers.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,86 @@
 | 
				
			|||||||
 | 
					import os
 | 
				
			||||||
 | 
					import requests
 | 
				
			||||||
 | 
					import dateutil.parser
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from documents.parsers import DocumentParser, ParseError, \
 | 
				
			||||||
 | 
					    make_thumbnail_from_pdf
 | 
				
			||||||
 | 
					from tika import parser
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TikaDocumentParser(DocumentParser):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    This parser sends documents to a local tika server
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_thumbnail(self, document_path, mime_type):
 | 
				
			||||||
 | 
					        if not self.archive_path:
 | 
				
			||||||
 | 
					            self.archive_path = self.convert_to_pdf(document_path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return make_thumbnail_from_pdf(
 | 
				
			||||||
 | 
					            self.archive_path, self.tempdir, self.logging_group)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def extract_metadata(self, document_path, mime_type):
 | 
				
			||||||
 | 
					        tika_server = settings.PAPERLESS_TIKA_ENDPOINT
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            parsed = parser.from_file(document_path, tika_server)
 | 
				
			||||||
 | 
					        except Exception as e:
 | 
				
			||||||
 | 
					            self.log("warning", f"Error while fetching document metadata for "
 | 
				
			||||||
 | 
					                                f"{document_path}: {e}")
 | 
				
			||||||
 | 
					            return []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                "namespace": "",
 | 
				
			||||||
 | 
					                "prefix": "",
 | 
				
			||||||
 | 
					                "key": key,
 | 
				
			||||||
 | 
					                "value": parsed['metadata'][key]
 | 
				
			||||||
 | 
					            } for key in parsed['metadata']
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def parse(self, document_path, mime_type, file_name=None):
 | 
				
			||||||
 | 
					        self.log("info", f"Sending {document_path} to Tika server")
 | 
				
			||||||
 | 
					        tika_server = settings.PAPERLESS_TIKA_ENDPOINT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            parsed = parser.from_file(document_path, tika_server)
 | 
				
			||||||
 | 
					        except Exception as err:
 | 
				
			||||||
 | 
					            raise ParseError(
 | 
				
			||||||
 | 
					                f"Could not parse {document_path} with tika server at "
 | 
				
			||||||
 | 
					                f"{tika_server}: {err}"
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.text = parsed["content"].strip()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            self.date = dateutil.parser.isoparse(
 | 
				
			||||||
 | 
					                parsed["metadata"]["Creation-Date"])
 | 
				
			||||||
 | 
					        except Exception as e:
 | 
				
			||||||
 | 
					            self.log("warning", f"Unable to extract date for document "
 | 
				
			||||||
 | 
					                                f"{document_path}: {e}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.archive_path = self.convert_to_pdf(document_path, file_name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def convert_to_pdf(self, document_path, file_name):
 | 
				
			||||||
 | 
					        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"Converting {document_path} to PDF as {pdf_path}")
 | 
				
			||||||
 | 
					        files = {"files": (file_name, open(document_path, "rb"))}
 | 
				
			||||||
 | 
					        headers = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            response = requests.post(url, files=files, headers=headers)
 | 
				
			||||||
 | 
					            response.raise_for_status()  # ensure we notice bad responses
 | 
				
			||||||
 | 
					        except Exception as err:
 | 
				
			||||||
 | 
					            raise ParseError(
 | 
				
			||||||
 | 
					                f"Error while converting document to PDF: {err}"
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        file = open(pdf_path, "wb")
 | 
				
			||||||
 | 
					        file.write(response.content)
 | 
				
			||||||
 | 
					        file.close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return pdf_path
 | 
				
			||||||
							
								
								
									
										20
									
								
								src/paperless_tika/signals.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/paperless_tika/signals.py
									
									
									
									
									
										Normal 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",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
		Reference in New Issue
	
	Block a user