mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05: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" | ||||
| ocrmypdf = "*" | ||||
| tqdm = "*" | ||||
| tika = "*" | ||||
|  | ||||
| [dev-packages] | ||||
| coveralls = "*" | ||||
|   | ||||
							
								
								
									
										57
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										57
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							| @@ -1,7 +1,7 @@ | ||||
| { | ||||
|     "_meta": { | ||||
|         "hash": { | ||||
|             "sha256": "3d576f289958226a7583e4c471c7f8c11bff6933bf093185f623cfb381a92412" | ||||
|             "sha256": "993e362c31af6b8094693075f614270a820cf0b557369d66d674e1a107b7bd31" | ||||
|         }, | ||||
|         "pipfile-spec": 6, | ||||
|         "requires": { | ||||
| @@ -44,6 +44,13 @@ | ||||
|             ], | ||||
|             "version": "==1.17.12" | ||||
|         }, | ||||
|         "certifi": { | ||||
|             "hashes": [ | ||||
|                 "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", | ||||
|                 "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" | ||||
|             ], | ||||
|             "version": "==2020.12.5" | ||||
|         }, | ||||
|         "cffi": { | ||||
|             "hashes": [ | ||||
|                 "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'", | ||||
|             "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": { | ||||
|             "hashes": [ | ||||
|                 "sha256:72bf46dc135b039a5d5b59f4e079242ac15eac02a30038e8cb2dec7b153cab65", | ||||
| @@ -683,6 +699,14 @@ | ||||
|             ], | ||||
|             "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": { | ||||
|             "hashes": [ | ||||
|                 "sha256:090bbf144fd5823c1f2efa3e1a9bf180295b24294ca8f478e75b40ed54f8036e", | ||||
| @@ -769,6 +793,14 @@ | ||||
|             "markers": "python_version >= '3.5'", | ||||
|             "version": "==2.1.0" | ||||
|         }, | ||||
|         "tika": { | ||||
|             "hashes": [ | ||||
|                 "sha256:c2c50f405622f74531841104f9e85c17511aede11de8e5385eab1a29a31f191b", | ||||
|                 "sha256:d1f2eddb93caa9a2857569486aa2bc0320d0bf1796cdbe03066954cbc4b4bf62" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==1.24" | ||||
|         }, | ||||
|         "tqdm": { | ||||
|             "hashes": [ | ||||
|                 "sha256:38b658a3e4ecf9b4f6f8ff75ca16221ae3378b2e175d846b6b33ea3a20852cf5", | ||||
| @@ -777,6 +809,15 @@ | ||||
|             "index": "pypi", | ||||
|             "version": "==4.54.1" | ||||
|         }, | ||||
|         "typing-extensions": { | ||||
|             "hashes": [ | ||||
|                 "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", | ||||
|                 "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", | ||||
|                 "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" | ||||
|             ], | ||||
|             "markers": "python_version < '3.8'", | ||||
|             "version": "==3.7.4.3" | ||||
|         }, | ||||
|         "tzlocal": { | ||||
|             "hashes": [ | ||||
|                 "sha256:643c97c5294aedc737780a49d9df30889321cbe1204eac2c2ec6134035a92e44", | ||||
| @@ -784,6 +825,14 @@ | ||||
|             ], | ||||
|             "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": { | ||||
|             "hashes": [ | ||||
|                 "sha256:3caefdcc8f06a57fdc5ef2d22aa7c0bfda4f55e71a0bee74cbf3176d97536ef3", | ||||
| @@ -1197,11 +1246,11 @@ | ||||
|         }, | ||||
|         "requests": { | ||||
|             "hashes": [ | ||||
|                 "sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8", | ||||
|                 "sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998" | ||||
|                 "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.0" | ||||
|             "version": "==2.25.1" | ||||
|         }, | ||||
|         "six": { | ||||
|             "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"}     | ||||
|      | ||||
| .. _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 | ||||
| ############### | ||||
|   | ||||
| @@ -1,2 +1,4 @@ | ||||
| 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 -p 3000:3000 -d thecodingmachine/gotenberg | ||||
| docker run -p 9998:9998 -d apache/tika | ||||
|   | ||||
| @@ -34,7 +34,11 @@ | ||||
| 						"assets": [ | ||||
| 							"src/favicon.ico", | ||||
| 							"src/assets", | ||||
| 							"src/manifest.webmanifest" | ||||
| 							"src/manifest.webmanifest", { | ||||
| 								"glob": "pdf.worker.min.js", | ||||
| 								"input": "node_modules/pdfjs-dist/build/", | ||||
| 								"output": "/assets/js/" | ||||
| 							} | ||||
| 						], | ||||
| 						"styles": [ | ||||
| 							"src/styles.scss" | ||||
|   | ||||
| @@ -377,7 +377,7 @@ | ||||
|         <source>Do you really want to delete the tag "<x id="PH" equiv-text="object.name"/>"?</source> | ||||
|         <context-group purpose="location"> | ||||
|           <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> | ||||
|       </trans-unit> | ||||
|       <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> | ||||
|         <context-group purpose="location"> | ||||
|           <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> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="bc000b39af12c0925c424f4cb85f0c31c0f8eca8" datatype="html"> | ||||
| @@ -468,21 +468,21 @@ | ||||
|         <source>Saved view "<x id="PH" equiv-text="savedView.name"/> deleted.</source> | ||||
|         <context-group purpose="location"> | ||||
|           <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> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="5647210819299459618" datatype="html"> | ||||
|         <source>Settings saved successfully.</source> | ||||
|         <context-group purpose="location"> | ||||
|           <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> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="8488620293789898901" datatype="html"> | ||||
|         <source>Error while storing settings on server: <x id="PH" equiv-text="JSON.stringify(error.error)"/></source> | ||||
|         <context-group purpose="location"> | ||||
|           <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> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="11ebd254cc9294717105c5982eb0cd2af30a446d" datatype="html"> | ||||
| @@ -496,11 +496,11 @@ | ||||
|         <source>Saved views</source> | ||||
|         <context-group purpose="location"> | ||||
|           <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> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="0d8ceb153aa715eb905da0710cc0b2ac73159abc" datatype="html"> | ||||
|         <source>Document list</source> | ||||
|       <trans-unit id="bbe41ac2ea4a6c00ea941a41b33105048f8e9f13" datatype="html"> | ||||
|         <source>Appearance</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/manage/settings/settings.component.html</context> | ||||
|           <context context-type="linenumber">13</context> | ||||
| @@ -513,60 +513,74 @@ | ||||
|           <context context-type="linenumber">17</context> | ||||
|         </context-group> | ||||
|       </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"> | ||||
|         <source>Bulk editing</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 context-type="linenumber">44</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="c0ac61661c6c326d6e0e00c231b95cf2ac0c6586" datatype="html"> | ||||
|         <source>Show confirmation dialogs</source> | ||||
|         <context-group purpose="location"> | ||||
|           <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> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="291bbe56ecbe945dcf05580a57d679fa7bd1e06a" datatype="html"> | ||||
|         <source>Deleting documents will always ask for confirmation.</source> | ||||
|         <context-group purpose="location"> | ||||
|           <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> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="8cfddc13e04f5545ac63f419ef363505d6f78c2e" datatype="html"> | ||||
|         <source>Apply on close</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 context-type="linenumber">49</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="8cb90334f5dfd7fc67205085f59381e2a334ccfc" datatype="html"> | ||||
|         <source>Appears on</source> | ||||
|         <context-group purpose="location"> | ||||
|           <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> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="6717cf1acf04728fc2b7c39f6d3297f8ff15fde5" datatype="html"> | ||||
|         <source>Show on dashboard</source> | ||||
|         <context-group purpose="location"> | ||||
|           <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> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="541bfc5b123b3f8867fd681eaceefb663a811973" datatype="html"> | ||||
|         <source>Show in sidebar</source> | ||||
|         <context-group purpose="location"> | ||||
|           <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> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="abba764a7a595d04dc8c3b26e04b3780d4fdb540" datatype="html"> | ||||
|         <source>No saved views defined.</source> | ||||
|         <context-group purpose="location"> | ||||
|           <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> | ||||
|       </trans-unit> | ||||
|       <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> | ||||
|         <context-group purpose="location"> | ||||
|           <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> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="c3f3334de899327bf3ec8999236e10798ff76e72" datatype="html"> | ||||
| @@ -639,8 +653,8 @@ | ||||
|           <context context-type="linenumber">11</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="180092a6b8a6151a05f4a7552a2fb75fd159dfa8" datatype="html"> | ||||
|         <source>Match</source> | ||||
|       <trans-unit id="eab7fc7cf2d663e54de934b779fce4275a303f0f" datatype="html"> | ||||
|         <source>Matching pattern</source> | ||||
|         <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="linenumber">12</context> | ||||
| @@ -776,26 +790,33 @@ | ||||
|         <source>Paperless-ng</source> | ||||
|         <context-group purpose="location"> | ||||
|           <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> | ||||
|         <note priority="1" from="description">app title</note> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="8d667444401ef6380fd262e4fe4795f261a427b1" datatype="html"> | ||||
|         <source>Search for documents</source> | ||||
|       <trans-unit id="069566c6ed4f051b5b5617ef1935837226585dad" datatype="html"> | ||||
|         <source>Search documents</source> | ||||
|         <context-group purpose="location"> | ||||
|           <context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">12</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 context-type="linenumber">15</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="121cc5391cd2a5115bc2b3160379ee5b36cd7716" datatype="html"> | ||||
|         <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 context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context> | ||||
|           <context context-type="linenumber">112</context> | ||||
| @@ -805,70 +826,91 @@ | ||||
|         <source>Admin</source> | ||||
|         <context-group purpose="location"> | ||||
|           <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> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="46aa32e581922d6d2c3d7bc4c87209ad5808b029" datatype="html"> | ||||
|         <source>Misc</source> | ||||
|         <context-group purpose="location"> | ||||
|           <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> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="fcfd4675b4c90f08d18d3abede9a9a4dff4cfdc7" datatype="html"> | ||||
|         <source>Documentation</source> | ||||
|         <context-group purpose="location"> | ||||
|           <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> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="355a222236bc01b9a8cd3cb9ecf76891125aed69" datatype="html"> | ||||
|         <source>GitHub</source> | ||||
|         <context-group purpose="location"> | ||||
|           <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> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="bb694b49d408265c91c62799c2b3a7e3151c824d" datatype="html"> | ||||
|         <source>Logout</source> | ||||
|       <trans-unit id="af665f8de8fabe306aaf27443957e69bcbbce63c" datatype="html"> | ||||
|         <source>Logged in as <x id="INTERPOLATION" equiv-text="{{displayName}}"/></source> | ||||
|         <context-group purpose="location"> | ||||
|           <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> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="4f55b670f49d927c6026bb614c7c62b1f2a394c0" datatype="html"> | ||||
|         <source>Open documents</source> | ||||
|         <context-group purpose="location"> | ||||
|           <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> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="dca5bf9344a759fa5a07f1b21f50286ec242ba44" datatype="html"> | ||||
|         <source>Close all</source> | ||||
|         <context-group purpose="location"> | ||||
|           <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> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="5195932016807797291" datatype="html"> | ||||
|         <source>Correspondent: <x id="PH" equiv-text="this.correspondents.find(c => c.id == +rule.value)?.name"/></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">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> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="8705701325879965907" datatype="html"> | ||||
|         <source>Type: <x id="PH" equiv-text="this.documentTypes.find(dt => dt.id == +rule.value)?.name"/></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 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> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="8180755793012580465" datatype="html"> | ||||
|         <source>Tag: <x id="PH" equiv-text="this.tags.find(t => t.id == +rule.value)?.name"/></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">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> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="ddb40946e790522301687ecddb9ce1cb8ad40dd1" datatype="html"> | ||||
| @@ -910,7 +952,7 @@ | ||||
|         <source>Not assigned</source> | ||||
|         <context-group purpose="location"> | ||||
|           <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> | ||||
|         <note priority="1" from="description">Filter drop down element to filter for documents with no correspondent/type/tag assigned</note> | ||||
|       </trans-unit> | ||||
| @@ -1561,22 +1603,43 @@ | ||||
|           <context context-type="linenumber">97</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="3184700926171002527" datatype="html"> | ||||
|         <source>Any</source> | ||||
|       <trans-unit id="5851669019930456395" datatype="html"> | ||||
|         <source>Any word</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="1616102757855967475" datatype="html"> | ||||
|         <source>All</source> | ||||
|       <trans-unit id="7517655726614958140" datatype="html"> | ||||
|         <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 context-type="sourcefile">src/app/data/matching-model.ts</context> | ||||
|           <context context-type="linenumber">13</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="1968183742008490888" datatype="html"> | ||||
|         <source>Literal</source> | ||||
|       <trans-unit id="111914402588955480" datatype="html"> | ||||
|         <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 context-type="sourcefile">src/app/data/matching-model.ts</context> | ||||
|           <context context-type="linenumber">14</context> | ||||
| @@ -1589,15 +1652,29 @@ | ||||
|           <context context-type="linenumber">15</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="701356546322112069" datatype="html"> | ||||
|         <source>Fuzzy match</source> | ||||
|       <trans-unit id="7548151332424148033" datatype="html"> | ||||
|         <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 context-type="sourcefile">src/app/data/matching-model.ts</context> | ||||
|           <context context-type="linenumber">16</context> | ||||
|         </context-group> | ||||
|       </trans-unit> | ||||
|       <trans-unit id="616064537937996961" datatype="html"> | ||||
|         <source>Auto</source> | ||||
|       <trans-unit id="8419167206585286450" datatype="html"> | ||||
|         <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 context-type="sourcefile">src/app/data/matching-model.ts</context> | ||||
|           <context context-type="linenumber">17</context> | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import { Component } from '@angular/core'; | ||||
| import { SettingsService } from './services/settings.service'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-root', | ||||
| @@ -6,9 +7,11 @@ import { Component } from '@angular/core'; | ||||
|   styleUrls: ['./app.component.scss'] | ||||
| }) | ||||
| 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"> | ||||
|   <a class="navbar-brand col-md-3 col-lg-2 mr-0 px-3" routerLink="/dashboard"> | ||||
|     <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" | ||||
|   <button class="navbar-toggler d-md-none collapsed border-0" type="button" data-toggle="collapse" | ||||
|     data-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation" | ||||
|     (click)="isMenuCollapsed = !isMenuCollapsed"> | ||||
|     <span class="navbar-toggler-icon"></span> | ||||
|   </button> | ||||
|   <form (ngSubmit)="search()" class="w-100 m-1"> | ||||
|     <input class="form-control form-control-dark" type="text" placeholder="Search for documents" aria-label="Search" | ||||
|       [formControl]="searchField" [ngbTypeahead]="searchAutoComplete" (selectItem)="itemSelected($event)" i18n-placeholder> | ||||
|   </form> | ||||
|   <a class="navbar-brand col-auto col-md-3 col-lg-2 mr-0 px-3 py-3 order-sm-0" routerLink="/dashboard"> | ||||
|     <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.43 238.91" width="1rem" class="mr-2" fill="currentColor"> | ||||
|       <path d="M194.7,0C164.22,70.94,17.64,79.74,64.55,194.06c.58,1.47-10.85,17-18.47,29.9-1.76-6.45-3.81-13.48-3.52-14.07,38.11-45.14-27.26-70.65-30.78-107.58C-4.64,131.62-10.5,182.92,39,212.53c.3,0,2.64,11.14,3.81,16.71a58.55,58.55,0,0,0-2.93,6.45c-1.17,2.93,7.62,2.64,7.62,3.22.88-.29,21.7-36.93,22.28-37.23C187.67,174.72,208.48,68.6,194.7,0ZM134.61,74.75C79.5,124,70.12,160.64,71.88,178.53,53.41,134.85,107.64,86.77,134.61,74.75ZM28.2,145.11c10.55,9.67,28.14,39.28,13.19,56.57C44.91,193.77,46.08,175.89,28.2,145.11Z" transform="translate(0 0)"/> | ||||
|     </svg> | ||||
|     <ng-container i18n="app title">Paperless-ng</ng-container> | ||||
|   </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> | ||||
|  | ||||
| <div class="container-fluid"> | ||||
| @@ -105,13 +140,6 @@ | ||||
|               </svg> <ng-container i18n>Logs</ng-container> | ||||
|             </a> | ||||
|           </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"> | ||||
|             <a class="nav-link" href="admin/"> | ||||
|               <svg class="sidebaricon" fill="currentColor"> | ||||
| @@ -139,13 +167,6 @@ | ||||
|               </svg> <ng-container i18n>GitHub</ng-container> | ||||
|             </a> | ||||
|           </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> | ||||
|       </div> | ||||
|     </nav> | ||||
|   | ||||
| @@ -1,36 +1,30 @@ | ||||
|  | ||||
| @import "/src/theme"; | ||||
|  | ||||
|   /* | ||||
| /* | ||||
|  * Sidebar | ||||
|  */ | ||||
|  | ||||
|  .sidebar { | ||||
| .sidebar { | ||||
|   position: fixed; | ||||
|   top: 0; | ||||
|   bottom: 0; | ||||
|   left: 0; | ||||
|   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); | ||||
| } | ||||
|  | ||||
| @media (max-width: 767.98px) { | ||||
|   .sidebar { | ||||
|     top: 3rem; | ||||
|     top: 3.5rem; | ||||
|   } | ||||
| } | ||||
|  | ||||
| .sidebar-sticky { | ||||
|   position: relative; | ||||
|   top: 0; | ||||
|   /* height: calc(100vh - 48px); */ | ||||
|   height: 100%; | ||||
|   padding-top: .5rem; | ||||
|   padding-top: 0.5rem; | ||||
|   overflow-x: hidden; | ||||
|   overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */ | ||||
| } | ||||
|  | ||||
| @supports ((position: -webkit-sticky) or (position: sticky)) { | ||||
|   .sidebar-sticky { | ||||
|     position: -webkit-sticky; | ||||
| @@ -53,36 +47,85 @@ | ||||
|   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; | ||||
| } | ||||
|  | ||||
| .sidebar-heading { | ||||
|   font-size: .75rem; | ||||
|   font-size: 0.75rem; | ||||
|   text-transform: uppercase; | ||||
| } | ||||
|  | ||||
|  | ||||
| /* | ||||
|  * Navbar | ||||
|  */ | ||||
|  | ||||
|  .navbar-brand { | ||||
|   padding-top: .75rem; | ||||
|   padding-bottom: .75rem; | ||||
| .navbar-brand { | ||||
|   padding-top: 0.75rem; | ||||
|   padding-bottom: 0.75rem; | ||||
|   font-size: 1rem; | ||||
|   background-color: rgba(0, 0, 0, .25); | ||||
|   box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25); | ||||
| } | ||||
|  | ||||
| .navbar .navbar-toggler { | ||||
|   top: .25rem; | ||||
|   right: 1rem; | ||||
| .dropdown.show .dropdown-toggle, | ||||
| .dropdown-toggle:hover { | ||||
|   opacity: 0.7; | ||||
| } | ||||
|  | ||||
| .navbar .form-control { | ||||
|   padding: .75rem 1rem; | ||||
|   border-width: 0; | ||||
|   border-radius: 0; | ||||
| .dropdown-toggle::after { | ||||
|   margin-left: 0.4em; | ||||
|   vertical-align: 0.155em; | ||||
| } | ||||
|  | ||||
| .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,7 +9,8 @@ import { SavedViewService } from 'src/app/services/rest/saved-view.service'; | ||||
| import { SearchService } from 'src/app/services/rest/search.service'; | ||||
| import { environment } from 'src/environments/environment'; | ||||
| import { DocumentDetailComponent } from '../document-detail/document-detail.component'; | ||||
|    | ||||
| import { Meta } from '@angular/platform-browser'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-app-frame', | ||||
|   templateUrl: './app-frame.component.html', | ||||
| @@ -22,8 +23,10 @@ export class AppFrameComponent implements OnInit, OnDestroy { | ||||
|     private activatedRoute: ActivatedRoute, | ||||
|     private openDocumentsService: OpenDocumentsService, | ||||
|     private searchService: SearchService, | ||||
|     public savedViewService: SavedViewService | ||||
|     public savedViewService: SavedViewService, | ||||
|     private meta: Meta | ||||
|     ) { | ||||
|        | ||||
|   } | ||||
|  | ||||
|   versionString = `${environment.appTitle} ${environment.version}` | ||||
| @@ -55,7 +58,7 @@ export class AppFrameComponent implements OnInit, OnDestroy { | ||||
|         term.length < 2 ? from([[]]) : this.searchService.autocomplete(term) | ||||
|       ) | ||||
|     ) | ||||
|    | ||||
|  | ||||
|   itemSelected(event) { | ||||
|     event.preventDefault() | ||||
|     let currentSearch: string = this.searchField.value | ||||
| @@ -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> | ||||
|           <tr *ngFor="let m of metadata"> | ||||
|               <td>{{m.prefix}}:{{m.key}}</td> | ||||
|               <td>{{m.value}}</td> | ||||
|               <td class="metadata-column">{{m.value}}</td> | ||||
|           </tr> | ||||
|       </tbody> | ||||
|   </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="col-md-2 d-none d-lg-block doc-img-background" [class.doc-img-background-selected]="selected"> | ||||
|       <img [src]="getThumbUrl()" class="card-img doc-img border-right" (click)="setSelected(selectable ? !selected : false)"> | ||||
|     <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 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 class="custom-control custom-checkbox"> | ||||
| @@ -12,7 +12,7 @@ | ||||
|  | ||||
|     </div> | ||||
|     <div class="col"> | ||||
|       <div class="card-body"> | ||||
|       <div class="card-body bg-light"> | ||||
|  | ||||
|         <div class="d-flex justify-content-between align-items-center"> | ||||
|           <h5 class="card-title"> | ||||
| @@ -55,16 +55,16 @@ | ||||
|                 <path fill-rule="evenodd" d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/> | ||||
|               </svg> <ng-container i18n>Download</ng-container> | ||||
|             </a> | ||||
|              | ||||
|  | ||||
|           </div> | ||||
|  | ||||
|           <small class="text-muted ml-auto" i18n>Score:</small> | ||||
|  | ||||
|           <ngb-progressbar *ngIf="searchScore" [type]="searchScoreClass" [value]="searchScore" class="search-score-bar mx-2" [max]="1"></ngb-progressbar> | ||||
|            | ||||
|  | ||||
|           <small class="text-muted" i18n>Created: {{document.created | date}}</small> | ||||
|         </div> | ||||
|          | ||||
|  | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|   | ||||
| @@ -30,10 +30,6 @@ | ||||
|   border-color: $primary; | ||||
| } | ||||
|  | ||||
| .doc-img-background { | ||||
|   background-color: white; | ||||
| } | ||||
|  | ||||
| .doc-img-background-selected { | ||||
|   background-color: $primaryFaded; | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| <div class="col p-2 h-100"> | ||||
|   <div class="card h-100 shadow-sm document-card" [class.card-selected]="selected"> | ||||
|     <div class="border-bottom" [class.doc-img-background-selected]="selected"> | ||||
|       <img class="card-img doc-img" [src]="getThumbUrl()" (click)="setSelected(!selected)"> | ||||
|     <div class="border-bottom doc-img-container" [class.doc-img-background-selected]="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="custom-control custom-checkbox"> | ||||
|   | ||||
| @@ -25,13 +25,26 @@ export class FilterEditorComponent implements OnInit, OnDestroy { | ||||
|       switch(this.filterRules[0].rule_type) { | ||||
|  | ||||
|         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: | ||||
|           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: | ||||
|           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-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> | ||||
|   </div> | ||||
|   <div class="modal-footer"> | ||||
|   | ||||
| @@ -9,7 +9,7 @@ | ||||
|        | ||||
|       <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-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> | ||||
|  | ||||
|     </div> | ||||
|   | ||||
| @@ -30,7 +30,7 @@ export abstract class GenericListComponent<T extends ObjectWithId> implements On | ||||
|     if (o.matching_algorithm == MATCH_AUTO) { | ||||
|       return $localize`Automatic` | ||||
|     } 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 { | ||||
|       return "-" | ||||
|     } | ||||
|   | ||||
| @@ -10,30 +10,45 @@ | ||||
|       <a ngbNavLink i18n>General settings</a> | ||||
|       <ng-template ngbNavContent> | ||||
|  | ||||
|         <h4 i18n>Document list</h4> | ||||
|          | ||||
|         <h4 i18n>Appearance</h4> | ||||
|  | ||||
|         <div class="form-row form-group"> | ||||
|           <div class="col-md-3 col-form-label"> | ||||
|             <span i18n>Items per page</span> | ||||
|           </div> | ||||
|           <div class="col"> | ||||
|          | ||||
|  | ||||
|             <select class="form-control" formControlName="documentListItemPerPage"> | ||||
|               <option [ngValue]="10">10</option> | ||||
|               <option [ngValue]="25">25</option> | ||||
|               <option [ngValue]="50">50</option> | ||||
|               <option [ngValue]="100">100</option> | ||||
|             </select> | ||||
|          | ||||
|           </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> | ||||
|         <app-input-check i18n-title title="Apply on close" formControlName="bulkEditApplyOnClose"></app-input-check> | ||||
|         <h4 class="mt-4" i18n>Bulk editing</h4> | ||||
|  | ||||
|         <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> | ||||
|     </li> | ||||
| @@ -42,7 +57,7 @@ | ||||
|       <ng-template ngbNavContent> | ||||
|  | ||||
|         <div formGroupName="savedViews"> | ||||
|            | ||||
|  | ||||
|             <div *ngFor="let view of savedViews" [formGroupName]="view.id" class="form-row"> | ||||
|               <div class="form-group col-4 mr-3"> | ||||
|                 <label for="name_{{view.id}}" i18n>Name</label> | ||||
| @@ -68,7 +83,7 @@ | ||||
|             </div> | ||||
|  | ||||
|             <div *ngIf="savedViews.length == 0" i18n>No saved views defined.</div> | ||||
|            | ||||
|  | ||||
|         </div> | ||||
|  | ||||
|       </ng-template> | ||||
| @@ -78,4 +93,4 @@ | ||||
|   <div [ngbNavOutlet]="nav" class="border-left border-right border-bottom p-3 mb-3 shadow"></div> | ||||
|  | ||||
|   <button type="submit" class="btn btn-primary">Save</button> | ||||
| </form> | ||||
| </form> | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { Component, OnInit } from '@angular/core'; | ||||
| import { Component, OnInit, Renderer2  } from '@angular/core'; | ||||
| import { FormControl, FormGroup } from '@angular/forms'; | ||||
| import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'; | ||||
| import { DocumentListViewService } from 'src/app/services/document-list-view.service'; | ||||
| @@ -19,9 +19,13 @@ export class SettingsComponent implements OnInit { | ||||
|     'bulkEditConfirmationDialogs': new FormControl(this.settings.get(SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS)), | ||||
|     'bulkEditApplyOnClose': new FormControl(this.settings.get(SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE)), | ||||
|     'documentListItemPerPage': new FormControl(this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)), | ||||
|     'darkModeUseSystem': new FormControl(this.settings.get(SETTINGS_KEYS.DARK_MODE_USE_SYSTEM)), | ||||
|     'darkModeEnabled': new FormControl(this.settings.get(SETTINGS_KEYS.DARK_MODE_ENABLED)), | ||||
|     'savedViews': this.savedViewGroup | ||||
|   }) | ||||
|  | ||||
|   savedViews: PaperlessSavedView[] | ||||
|  | ||||
|   constructor( | ||||
|     public savedViewService: SavedViewService, | ||||
|     private documentListViewService: DocumentListViewService, | ||||
| @@ -29,8 +33,6 @@ export class SettingsComponent implements OnInit { | ||||
|     private settings: SettingsService | ||||
|   ) { } | ||||
|  | ||||
|   savedViews: PaperlessSavedView[] | ||||
|  | ||||
|   ngOnInit() { | ||||
|     this.savedViewService.listAll().subscribe(r => { | ||||
|       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() { | ||||
|     this.settings.set(SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE, this.settingsForm.value.bulkEditApplyOnClose) | ||||
|     this.settings.set(SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS, this.settingsForm.value.bulkEditConfirmationDialogs) | ||||
|     this.settings.set(SETTINGS_KEYS.DOCUMENT_LIST_SIZE, this.settingsForm.value.documentListItemPerPage) | ||||
|     this.settings.set(SETTINGS_KEYS.DARK_MODE_USE_SYSTEM, this.settingsForm.value.darkModeUseSystem) | ||||
|     this.settings.set(SETTINGS_KEYS.DARK_MODE_ENABLED, (this.settingsForm.value.darkModeEnabled == true).toString()) | ||||
|     this.documentListViewService.updatePageSize() | ||||
|     this.settings.updateDarkModeSettings() | ||||
|     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-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> | ||||
|     </div> | ||||
|     <div class="modal-footer"> | ||||
|   | ||||
| @@ -9,12 +9,12 @@ export const MATCH_FUZZY = 5 | ||||
| export const MATCH_AUTO = 6 | ||||
|  | ||||
| export const MATCHING_ALGORITHMS = [ | ||||
|     {id: MATCH_ANY, name: $localize`Any`}, | ||||
|     {id: MATCH_ALL, name: $localize`All`}, | ||||
|     {id: MATCH_LITERAL, name: $localize`Literal`}, | ||||
|     {id: MATCH_REGEX, name: $localize`Regular expression`}, | ||||
|     {id: MATCH_FUZZY, name: $localize`Fuzzy match`}, | ||||
|     {id: MATCH_AUTO, name: $localize`Auto`}, | ||||
|     {id: MATCH_ANY, shortName: $localize`Any word`, name: $localize`Any: Document contains any of these words (space separated)`}, | ||||
|     {id: MATCH_ALL, shortName: $localize`All words`, name: $localize`All: Document contains all of these words (space separated)`}, | ||||
|     {id: MATCH_LITERAL, shortName: $localize`Exact match`, name: $localize`Exact: Document contains this string`}, | ||||
|     {id: MATCH_REGEX, shortName: $localize`Regular expression`, name: $localize`Regular expression: Document matches this regular expression`}, | ||||
|     {id: MATCH_FUZZY, shortName: $localize`Fuzzy word`, name: $localize`Fuzzy: Document contains a word similar to this word`}, | ||||
|     {id: MATCH_AUTO, shortName: $localize`Automatic`, name: $localize`Auto: Learn matching automatically`}, | ||||
| ] | ||||
|  | ||||
| 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 { | ||||
|   key: string | ||||
| @@ -10,12 +11,16 @@ export const SETTINGS_KEYS = { | ||||
|   BULK_EDIT_CONFIRMATION_DIALOGS: 'general-settings:bulk-edit:confirmation-dialogs', | ||||
|   BULK_EDIT_APPLY_ON_CLOSE: 'general-settings:bulk-edit:apply-on-close', | ||||
|   DOCUMENT_LIST_SIZE: 'general-settings:documentListSize', | ||||
|   DARK_MODE_USE_SYSTEM: 'general-settings:dark-mode:use-system', | ||||
|   DARK_MODE_ENABLED: 'general-settings:dark-mode:enabled' | ||||
| } | ||||
|  | ||||
| const SETTINGS: PaperlessSettings[] = [ | ||||
|   {key: SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS, type: "boolean", default: true}, | ||||
|   {key: SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE, type: "boolean", default: false}, | ||||
|   {key: SETTINGS_KEYS.DOCUMENT_LIST_SIZE, type: "number", default: 50} | ||||
|   {key: SETTINGS_KEYS.DOCUMENT_LIST_SIZE, type: "number", default: 50}, | ||||
|   {key: SETTINGS_KEYS.DARK_MODE_USE_SYSTEM, type: "boolean", default: true}, | ||||
|   {key: SETTINGS_KEYS.DARK_MODE_ENABLED, type: "boolean", default: false} | ||||
| ] | ||||
|  | ||||
| @Injectable({ | ||||
| @@ -23,7 +28,30 @@ const SETTINGS: PaperlessSettings[] = [ | ||||
| }) | ||||
| 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 { | ||||
|     let setting = SETTINGS.find(s => s.key == key) | ||||
|   | ||||
| @@ -1,69 +1,19 @@ | ||||
| <?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> | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <!-- Generator: Adobe Illustrator 25.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  --> | ||||
| <svg version="1.1" | ||||
| 	 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="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" | ||||
| 	 style="enable-background:new 0 0 198.4 238.9;" xml:space="preserve"> | ||||
| <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"> | ||||
| 	</sodipodi:namedview> | ||||
| <g id="layer1" transform="translate(-9.9999792,-10.000082)" inkscape:groupmode="layer" inkscape:label="Layer 1"> | ||||
| 	<g id="SvgjsG1020" transform="matrix(0.10341565,0,0,0.10341565,1.2287665,8.3453496)"> | ||||
| 		<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 | ||||
| 			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 | ||||
| 			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 | ||||
| 			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 | ||||
| 			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" | ||||
| 			/> | ||||
| 	</g> | ||||
| </g> | ||||
| </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> | ||||
|   <base href="/"> | ||||
|   <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | ||||
|   <meta name="color-scheme" content="dark light"> | ||||
|   <meta name="theme-color" content="#17541f" /> | ||||
|   <link rel="manifest" href="manifest.webmanifest"> | ||||
|   <link rel="icon" type="image/x-icon" href="favicon.ico"> | ||||
| </head> | ||||
| <body> | ||||
| <body class="color-scheme-system"> | ||||
|   <app-root></app-root> | ||||
| </body> | ||||
| </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_dark"; | ||||
| @import "node_modules/bootstrap/scss/bootstrap"; | ||||
| @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" | ||||
|  | ||||
|     def ready(self): | ||||
|  | ||||
|         from .signals import document_consumption_started | ||||
|         from .signals import document_consumption_finished | ||||
|         from .signals.handlers import ( | ||||
|             add_inbox_tags, | ||||
|             run_pre_consume_script, | ||||
|             run_post_consume_script, | ||||
|             set_log_entry, | ||||
|             set_correspondent, | ||||
|             set_document_type, | ||||
|             set_tags, | ||||
|             add_to_index | ||||
|  | ||||
|         ) | ||||
|  | ||||
|         document_consumption_started.connect(run_pre_consume_script) | ||||
|  | ||||
|         document_consumption_finished.connect(add_inbox_tags) | ||||
|         document_consumption_finished.connect(set_correspondent) | ||||
|         document_consumption_finished.connect(set_document_type) | ||||
|         document_consumption_finished.connect(set_tags) | ||||
|         document_consumption_finished.connect(set_log_entry) | ||||
|         document_consumption_finished.connect(add_to_index) | ||||
|         document_consumption_finished.connect(run_post_consume_script) | ||||
|  | ||||
|         AppConfig.ready(self) | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import datetime | ||||
| import hashlib | ||||
| import logging | ||||
| import os | ||||
| from subprocess import Popen | ||||
|  | ||||
| import magic | ||||
| from django.conf import settings | ||||
| @@ -9,6 +9,7 @@ from django.db import transaction | ||||
| from django.db.models import Q | ||||
| from django.utils import timezone | ||||
| from filelock import FileLock | ||||
| from rest_framework.reverse import reverse | ||||
|  | ||||
| from .classifier import DocumentClassifier, IncompatibleClassifierVersionError | ||||
| 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.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, | ||||
|                          path, | ||||
|                          override_filename=None, | ||||
| @@ -119,6 +153,8 @@ class Consumer(LoggingMixin): | ||||
|             logging_group=self.logging_group | ||||
|         ) | ||||
|  | ||||
|         self.run_pre_consume_script() | ||||
|  | ||||
|         # This doesn't parse the document yet, but gives us a parser. | ||||
|  | ||||
|         document_parser = parser_class(self.logging_group) | ||||
| @@ -130,7 +166,7 @@ class Consumer(LoggingMixin): | ||||
|  | ||||
|         try: | ||||
|             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}...") | ||||
|             thumbnail = document_parser.get_optimised_thumbnail( | ||||
| @@ -215,6 +251,9 @@ class Consumer(LoggingMixin): | ||||
|                 # Delete the file only if it was successfully consumed | ||||
|                 self.log("debug", "Deleting file {}".format(self.path)) | ||||
|                 os.unlink(self.path) | ||||
|  | ||||
|                 self.run_post_consume_script(document) | ||||
|  | ||||
|         except Exception as e: | ||||
|             self.log( | ||||
|                 "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"), | ||||
|         max_length=128) | ||||
|         max_length=128, | ||||
|         blank=True, | ||||
|         null=True) | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _("filter rule") | ||||
|   | ||||
| @@ -144,6 +144,52 @@ def run_convert(input_file, | ||||
|         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): | ||||
|     """ | ||||
|     Returns the date of the document. | ||||
| @@ -221,7 +267,7 @@ class DocumentParser(LoggingMixin): | ||||
|     def extract_metadata(self, document_path, mime_type): | ||||
|         return [] | ||||
|  | ||||
|     def parse(self, document_path, mime_type): | ||||
|     def parse(self, document_path, mime_type, file_name=None): | ||||
|         raise NotImplementedError() | ||||
|  | ||||
|     def get_archive_path(self): | ||||
|   | ||||
| @@ -11,7 +11,6 @@ from django.db.models import Q | ||||
| from django.dispatch import receiver | ||||
| from django.utils import timezone | ||||
| from filelock import FileLock | ||||
| from rest_framework.reverse import reverse | ||||
|  | ||||
| from .. import index, matching | ||||
| from ..file_handling import delete_empty_directories, \ | ||||
| @@ -147,32 +146,6 @@ def set_tags(sender, | ||||
|     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) | ||||
| def cleanup_document_deletion(sender, instance, using, **kwargs): | ||||
|     with FileLock(settings.MEDIA_LOCK): | ||||
|   | ||||
| @@ -177,7 +177,7 @@ class DummyParser(DocumentParser): | ||||
|     def get_optimised_thumbnail(self, document_path, mime_type): | ||||
|         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" | ||||
|  | ||||
|  | ||||
| @@ -194,7 +194,7 @@ class FaultyParser(DocumentParser): | ||||
|     def get_optimised_thumbnail(self, document_path, mime_type): | ||||
|         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.") | ||||
|  | ||||
|  | ||||
| @@ -466,3 +466,53 @@ class TestConsumer(DirectoriesMixin, TestCase): | ||||
|         self.assertTrue(os.path.isfile(dst)) | ||||
|         self.assertRaises(ConsumerError, self.consumer.try_consume_file, 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", | ||||
|     "paperless_tesseract.apps.PaperlessTesseractConfig", | ||||
|     "paperless_text.apps.PaperlessTextConfig", | ||||
|     "paperless_tika.apps.PaperlessTikaConfig", | ||||
|     "paperless_mail.apps.PaperlessMailConfig", | ||||
|  | ||||
|     "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") | ||||
|  | ||||
| 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 os | ||||
| import re | ||||
| import subprocess | ||||
|  | ||||
| import ocrmypdf | ||||
| import pdftotext | ||||
| @@ -10,7 +9,8 @@ from PIL import Image | ||||
| from django.conf import settings | ||||
| 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): | ||||
| @@ -47,50 +47,8 @@ class RasterisedDocumentParser(DocumentParser): | ||||
|         return result | ||||
|  | ||||
|     def get_thumbnail(self, document_path, mime_type): | ||||
|         """ | ||||
|         The thumbnail of a PDF is just a 500px wide image of the first page. | ||||
|         """ | ||||
|  | ||||
|         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 | ||||
|         return make_thumbnail_from_pdf( | ||||
|             document_path, self.tempdir, self.logging_group) | ||||
|  | ||||
|     def is_image(self, mime_type): | ||||
|         return mime_type in [ | ||||
| @@ -130,7 +88,7 @@ class RasterisedDocumentParser(DocumentParser): | ||||
|                 f"Error while calculating DPI for image {image}: {e}") | ||||
|             return None | ||||
|  | ||||
|     def parse(self, document_path, mime_type): | ||||
|     def parse(self, document_path, mime_type, file_name=None): | ||||
|         mode = settings.OCR_MODE | ||||
|  | ||||
|         text_original = get_text_from_pdf(document_path) | ||||
|   | ||||
| @@ -32,6 +32,6 @@ class TextDocumentParser(DocumentParser): | ||||
|  | ||||
|         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: | ||||
|             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
	 jonaswinkler
					jonaswinkler