mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			3825023337
			...
			feature-pr
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 4070cd0e1b | 
| @@ -1,10 +1,14 @@ | |||||||
|  | @if (!previewOnly) { | ||||||
|   <a [href]="link ?? previewUrl" class="{{linkClasses}}" [target]="linkTarget" [title]="linkTitle" |   <a [href]="link ?? previewUrl" class="{{linkClasses}}" [target]="linkTarget" [title]="linkTitle" | ||||||
|     [ngbPopover]="previewContent" [popoverTitle]="document.title | documentTitle" container="body" |     [ngbPopover]="previewContent" [popoverTitle]="document.title | documentTitle" container="body" | ||||||
|     autoClose="true" [popoverClass]="popoverClass" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover"> |     autoClose="true" [popoverClass]="popoverClass" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover"> | ||||||
|     <ng-content></ng-content> |     <ng-content></ng-content> | ||||||
|   </a> |   </a> | ||||||
|  | } @else { | ||||||
|  |   <ng-container [ngTemplateOutlet]="previewContent" [ngTemplateOutletContext]="{ $implicit: document }"></ng-container> | ||||||
|  | } | ||||||
| <ng-template #previewContent> | <ng-template #previewContent> | ||||||
|   <div class="preview-popup-container" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview(); close()"> |   <div class="preview-popup-container" [class.full-size]="previewOnly" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview(); close()"> | ||||||
|     @if (error) { |     @if (error) { | ||||||
|       <div class="w-100 h-100 position-relative"> |       <div class="w-100 h-100 position-relative"> | ||||||
|         <p class="fst-italic position-absolute top-50 start-50 translate-middle" i18n>Error loading preview</p> |         <p class="fst-italic position-absolute top-50 start-50 translate-middle" i18n>Error loading preview</p> | ||||||
|   | |||||||
| @@ -4,6 +4,16 @@ | |||||||
|     overflow-y: scroll; |     overflow-y: scroll; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .preview-popup-container.full-size { | ||||||
|  |   width: 100% !important; | ||||||
|  |   height: 100% !important; | ||||||
|  |  | ||||||
|  |   > * { | ||||||
|  |     width: 100% !important; | ||||||
|  |     height: 100% !important; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
| ::ng-deep .popover.popover-preview { | ::ng-deep .popover.popover-preview { | ||||||
|     max-width: 32rem; |     max-width: 32rem; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | import { NgTemplateOutlet } from '@angular/common' | ||||||
| import { HttpClient } from '@angular/common/http' | import { HttpClient } from '@angular/common/http' | ||||||
| import { Component, Input, OnDestroy, ViewChild } from '@angular/core' | import { Component, Input, OnDestroy, ViewChild } from '@angular/core' | ||||||
| import { NgbPopover, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap' | import { NgbPopover, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap' | ||||||
| @@ -17,6 +18,7 @@ import { SettingsService } from 'src/app/services/settings.service' | |||||||
|   styleUrls: ['./preview-popup.component.scss'], |   styleUrls: ['./preview-popup.component.scss'], | ||||||
|   imports: [ |   imports: [ | ||||||
|     NgbPopoverModule, |     NgbPopoverModule, | ||||||
|  |     NgTemplateOutlet, | ||||||
|     DocumentTitlePipe, |     DocumentTitlePipe, | ||||||
|     PdfViewerModule, |     PdfViewerModule, | ||||||
|     SafeUrlPipe, |     SafeUrlPipe, | ||||||
| @@ -47,6 +49,9 @@ export class PreviewPopupComponent implements OnDestroy { | |||||||
|   @Input() |   @Input() | ||||||
|   linkTitle: string = $localize`Open preview` |   linkTitle: string = $localize`Open preview` | ||||||
|  |  | ||||||
|  |   @Input() | ||||||
|  |   previewOnly: boolean = false | ||||||
|  |  | ||||||
|   unsubscribeNotifier: Subject<any> = new Subject() |   unsubscribeNotifier: Subject<any> = new Subject() | ||||||
|  |  | ||||||
|   error = false |   error = false | ||||||
| @@ -91,6 +96,8 @@ export class PreviewPopupComponent implements OnDestroy { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   init() { |   init() { | ||||||
|  |     this.error = false | ||||||
|  |     this.requiresPassword = false | ||||||
|     if (this.document.mime_type?.includes('text')) { |     if (this.document.mime_type?.includes('text')) { | ||||||
|       this.http |       this.http | ||||||
|         .get(this.previewURL, { responseType: 'text' }) |         .get(this.previewURL, { responseType: 'text' }) | ||||||
| @@ -119,6 +126,7 @@ export class PreviewPopupComponent implements OnDestroy { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   mouseEnterPreview() { |   mouseEnterPreview() { | ||||||
|  |     if (this.previewOnly) return | ||||||
|     this.mouseOnPreview = true |     this.mouseOnPreview = true | ||||||
|     if (!this.popover.isOpen()) { |     if (!this.popover.isOpen()) { | ||||||
|       // we're going to open but hide to pre-load content during hover delay |       // we're going to open but hide to pre-load content during hover delay | ||||||
| @@ -136,10 +144,12 @@ export class PreviewPopupComponent implements OnDestroy { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   mouseLeavePreview() { |   mouseLeavePreview() { | ||||||
|  |     if (this.previewOnly) return | ||||||
|     this.mouseOnPreview = false |     this.mouseOnPreview = false | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public close(immediate: boolean = false) { |   public close(immediate: boolean = false) { | ||||||
|  |     if (this.previewOnly) return | ||||||
|     setTimeout( |     setTimeout( | ||||||
|       () => { |       () => { | ||||||
|         if (!this.mouseOnPreview) this.popover.close() |         if (!this.mouseOnPreview) this.popover.close() | ||||||
|   | |||||||
| @@ -27,6 +27,7 @@ | |||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
|  |  | ||||||
|   <div class="btn-group flex-fill" role="group"> |   <div class="btn-group flex-fill" role="group"> | ||||||
|     <input type="radio" class="btn-check" [(ngModel)]="list.displayMode" value="table" id="displayModeDetails" name="displayModeDetails"> |     <input type="radio" class="btn-check" [(ngModel)]="list.displayMode" value="table" id="displayModeDetails" name="displayModeDetails"> | ||||||
|     <label for="displayModeDetails" class="btn btn-outline-primary btn-sm"> |     <label for="displayModeDetails" class="btn btn-outline-primary btn-sm"> | ||||||
| @@ -42,6 +43,13 @@ | |||||||
|     </label> |     </label> | ||||||
|   </div> |   </div> | ||||||
|  |  | ||||||
|  |   <div class="btn-group flex-fill" role="group"> | ||||||
|  |     <input type="checkbox" class="btn-check" [(ngModel)]="list.showPreviewPane" value="table" id="previewPane" name="previewPane"> | ||||||
|  |     <label for="previewPane" class="btn btn-outline-primary btn-sm"> | ||||||
|  |       <i-bs name="window-split"></i-bs> | ||||||
|  |     </label> | ||||||
|  |   </div> | ||||||
|  |  | ||||||
|   <div ngbDropdown class="btn-group flex-fill"> |   <div ngbDropdown class="btn-group flex-fill"> | ||||||
|     <button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle> |     <button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle> | ||||||
|       <i-bs name="arrow-down-up"></i-bs> |       <i-bs name="arrow-down-up"></i-bs> | ||||||
| @@ -105,7 +113,8 @@ | |||||||
|   <pngx-bulk-editor [hidden]="!isBulkEditing" [disabled]="!isBulkEditing"></pngx-bulk-editor> |   <pngx-bulk-editor [hidden]="!isBulkEditing" [disabled]="!isBulkEditing"></pngx-bulk-editor> | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
|  | <div class="row"> | ||||||
|  |   <div [class.col-lg-6]="list.showPreviewPane" [class.col]="!list.showPreviewPane"> | ||||||
|   <ng-template #pagination> |   <ng-template #pagination> | ||||||
|     <div class="d-flex flex-wrap gap-3 justify-content-between align-items-center mb-3"> |     <div class="d-flex flex-wrap gap-3 justify-content-between align-items-center mb-3"> | ||||||
|       <div class="d-flex align-items-center"> |       <div class="d-flex align-items-center"> | ||||||
| @@ -400,3 +409,39 @@ | |||||||
|         </div> |         </div> | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |   </div> | ||||||
|  |   @if (list.showPreviewPane) { | ||||||
|  |     <div class="col-lg-6"> | ||||||
|  |       <div class="row"> | ||||||
|  |         <div class="btn-toolbar mb-1 border-bottom align-items-center"> | ||||||
|  |           <div class="btn-group pb-3"> | ||||||
|  |             <button type="button" class="btn btn-sm btn-outline-secondary" i18n-title title="Previous" (click)="previousDoc()" [disabled]="list.documents.length === 0 || !hasPrevious"> | ||||||
|  |               <i-bs width="1.2em" height="1.2em" name="arrow-left" class="me-1"></i-bs><ng-container i18n>Previous</ng-container> | ||||||
|  |             </button> | ||||||
|  |             <button type="button" class="btn btn-sm btn-outline-secondary"  i18n-title title="Next" (click)="nextDoc()" [disabled]="list.documents.length === 0 || !hasNext"> | ||||||
|  |               <ng-container i18n>Next</ng-container><i-bs width="1.2em" height="1.2em" name="arrow-right" class="ms-1"></i-bs> | ||||||
|  |             </button> | ||||||
|  |           </div> | ||||||
|  |           <div class="input-group pb-3 ms-auto"> | ||||||
|  |             <h5 class="mb-0"> | ||||||
|  |               {{list.firstSelectedDocument?.title}} | ||||||
|  |             </h5> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |       <div class="row"> | ||||||
|  |         <div class="col preview-pane"> | ||||||
|  |           @if (list.selected.size > 0) { | ||||||
|  |             <pngx-preview-popup [document]="list.firstSelectedDocument" [previewOnly]="true"></pngx-preview-popup> | ||||||
|  |           } @else { | ||||||
|  |             <div class="w-100 h-100 position-relative"> | ||||||
|  |               <p class="fst-italic"> | ||||||
|  |                 <ng-container i18n>No document selected</ng-container> | ||||||
|  |               </p> | ||||||
|  |             </div> | ||||||
|  |           } | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   } | ||||||
|  | </div> | ||||||
|   | |||||||
| @@ -80,3 +80,9 @@ a { | |||||||
| pngx-page-header .dropdown-menu { | pngx-page-header .dropdown-menu { | ||||||
|   --bs-dropdown-min-width: 12em; |   --bs-dropdown-min-width: 12em; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .preview-pane { | ||||||
|  |   height: 60rem; | ||||||
|  |   top: 70px; | ||||||
|  |   position: sticky; | ||||||
|  | } | ||||||
|   | |||||||
| @@ -326,25 +326,37 @@ export class DocumentListComponent | |||||||
|     this.hotKeyService |     this.hotKeyService | ||||||
|       .addShortcut({ |       .addShortcut({ | ||||||
|         keys: 'control.arrowleft', |         keys: 'control.arrowleft', | ||||||
|         description: $localize`Previous page`, |         description: $localize`Previous page / document`, | ||||||
|       }) |       }) | ||||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) |       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||||
|       .subscribe(() => { |       .subscribe(() => { | ||||||
|  |         if (this.list.showPreviewPane) { | ||||||
|  |           if (this.hasPrevious) { | ||||||
|  |             this.previousDoc() | ||||||
|  |           } | ||||||
|  |         } else { | ||||||
|           if (this.list.currentPage > 1) { |           if (this.list.currentPage > 1) { | ||||||
|             this.list.currentPage-- |             this.list.currentPage-- | ||||||
|           } |           } | ||||||
|  |         } | ||||||
|       }) |       }) | ||||||
|  |  | ||||||
|     this.hotKeyService |     this.hotKeyService | ||||||
|       .addShortcut({ |       .addShortcut({ | ||||||
|         keys: 'control.arrowright', |         keys: 'control.arrowright', | ||||||
|         description: $localize`Next page`, |         description: $localize`Next page / document`, | ||||||
|       }) |       }) | ||||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) |       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||||
|       .subscribe(() => { |       .subscribe(() => { | ||||||
|  |         if (this.list.showPreviewPane) { | ||||||
|  |           if (this.hasNext) { | ||||||
|  |             this.nextDoc() | ||||||
|  |           } | ||||||
|  |         } else { | ||||||
|           if (this.list.currentPage < this.list.getLastPage()) { |           if (this.list.currentPage < this.list.getLastPage()) { | ||||||
|             this.list.currentPage++ |             this.list.currentPage++ | ||||||
|           } |           } | ||||||
|  |         } | ||||||
|       }) |       }) | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -473,4 +485,45 @@ export class DocumentListComponent | |||||||
|   resetFilters() { |   resetFilters() { | ||||||
|     this.filterEditor.resetSelected() |     this.filterEditor.resetSelected() | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public get hasPrevious(): boolean { | ||||||
|  |     return ( | ||||||
|  |       (this.list.selected.size > 0 && | ||||||
|  |         this.list.documents.indexOf(this.list.firstSelectedDocument) > 0) || | ||||||
|  |       (this.list.selected.size === 0 && this.list.documents.length > 0) | ||||||
|  |     ) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public get hasNext(): boolean { | ||||||
|  |     return ( | ||||||
|  |       (this.list.selected.size > 0 && | ||||||
|  |         this.list.documents.indexOf(this.list.firstSelectedDocument) < | ||||||
|  |           this.list.documents.length - 1) || | ||||||
|  |       (this.list.selected.size === 0 && this.list.documents.length > 0) | ||||||
|  |     ) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public nextDoc(): void { | ||||||
|  |     const index = | ||||||
|  |       this.list.selected.size === 0 | ||||||
|  |         ? 0 | ||||||
|  |         : Math.min( | ||||||
|  |             this.list.documents.indexOf(this.list.firstSelectedDocument) + 1, | ||||||
|  |             this.list.documents.length - 1 | ||||||
|  |           ) | ||||||
|  |     this.list.selected.clear() | ||||||
|  |     this.list.selected.add(this.list.documents[index].id) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public previousDoc(): void { | ||||||
|  |     const index = | ||||||
|  |       this.list.selected.size === 0 | ||||||
|  |         ? 0 | ||||||
|  |         : Math.max( | ||||||
|  |             this.list.documents.indexOf(this.list.firstSelectedDocument) - 1, | ||||||
|  |             0 | ||||||
|  |           ) | ||||||
|  |     this.list.selected.clear() | ||||||
|  |     this.list.selected.add(this.list.documents[index].id) | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -79,6 +79,11 @@ export interface ListViewState { | |||||||
|    * The fields to display in the document list. |    * The fields to display in the document list. | ||||||
|    */ |    */ | ||||||
|   displayFields?: DisplayField[] |   displayFields?: DisplayField[] | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Whether the preview pane is shown. | ||||||
|  |    */ | ||||||
|  |   showPreviewPane?: boolean | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -165,6 +170,7 @@ export class DocumentListViewService { | |||||||
|       sortReverse: true, |       sortReverse: true, | ||||||
|       filterRules: [], |       filterRules: [], | ||||||
|       selected: new Set<number>(), |       selected: new Set<number>(), | ||||||
|  |       showPreviewPane: false, | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -451,6 +457,15 @@ export class DocumentListViewService { | |||||||
|     this.saveDocumentListView() |     this.saveDocumentListView() | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   get showPreviewPane(): boolean { | ||||||
|  |     return this.activeListViewState.showPreviewPane | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   set showPreviewPane(show: boolean) { | ||||||
|  |     this.activeListViewState.showPreviewPane = show | ||||||
|  |     this.saveDocumentListView() | ||||||
|  |   } | ||||||
|  |  | ||||||
|   private saveDocumentListView() { |   private saveDocumentListView() { | ||||||
|     if (this._activeSavedViewId == null) { |     if (this._activeSavedViewId == null) { | ||||||
|       let savedState: ListViewState = { |       let savedState: ListViewState = { | ||||||
| @@ -461,6 +476,7 @@ export class DocumentListViewService { | |||||||
|         sortReverse: this.activeListViewState.sortReverse, |         sortReverse: this.activeListViewState.sortReverse, | ||||||
|         displayMode: this.activeListViewState.displayMode, |         displayMode: this.activeListViewState.displayMode, | ||||||
|         displayFields: this.activeListViewState.displayFields, |         displayFields: this.activeListViewState.displayFields, | ||||||
|  |         showPreviewPane: this.activeListViewState.showPreviewPane, | ||||||
|       } |       } | ||||||
|       localStorage.setItem( |       localStorage.setItem( | ||||||
|         DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG, |         DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG, | ||||||
| @@ -626,4 +642,8 @@ export class DocumentListViewService { | |||||||
|   documentIndexInCurrentView(documentID: number): number { |   documentIndexInCurrentView(documentID: number): number { | ||||||
|     return this.documents.map((d) => d.id).indexOf(documentID) |     return this.documents.map((d) => d.id).indexOf(documentID) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   get firstSelectedDocument(): Document { | ||||||
|  |     return this.documents.find((d) => this.selected.has(d.id)) | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -125,6 +125,7 @@ import { | |||||||
|   trash, |   trash, | ||||||
|   uiRadios, |   uiRadios, | ||||||
|   upcScan, |   upcScan, | ||||||
|  |   windowSplit, | ||||||
|   windowStack, |   windowStack, | ||||||
|   x, |   x, | ||||||
|   xCircle, |   xCircle, | ||||||
| @@ -323,6 +324,7 @@ const icons = { | |||||||
|   trash, |   trash, | ||||||
|   uiRadios, |   uiRadios, | ||||||
|   upcScan, |   upcScan, | ||||||
|  |   windowSplit, | ||||||
|   windowStack, |   windowStack, | ||||||
|   x, |   x, | ||||||
|   xCircle, |   xCircle, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user