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 @@ | ||||
| <a [href]="link ?? previewUrl" class="{{linkClasses}}" [target]="linkTarget" [title]="linkTitle" | ||||
| @if (!previewOnly) { | ||||
|   <a [href]="link ?? previewUrl" class="{{linkClasses}}" [target]="linkTarget" [title]="linkTitle" | ||||
|     [ngbPopover]="previewContent" [popoverTitle]="document.title | documentTitle" container="body" | ||||
|     autoClose="true" [popoverClass]="popoverClass" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover"> | ||||
|     <ng-content></ng-content> | ||||
| </a> | ||||
|   </a> | ||||
| } @else { | ||||
|   <ng-container [ngTemplateOutlet]="previewContent" [ngTemplateOutletContext]="{ $implicit: document }"></ng-container> | ||||
| } | ||||
| <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) { | ||||
|       <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> | ||||
|   | ||||
| @@ -4,6 +4,16 @@ | ||||
|     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 { | ||||
|     max-width: 32rem; | ||||
| } | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import { NgTemplateOutlet } from '@angular/common' | ||||
| import { HttpClient } from '@angular/common/http' | ||||
| import { Component, Input, OnDestroy, ViewChild } from '@angular/core' | ||||
| 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'], | ||||
|   imports: [ | ||||
|     NgbPopoverModule, | ||||
|     NgTemplateOutlet, | ||||
|     DocumentTitlePipe, | ||||
|     PdfViewerModule, | ||||
|     SafeUrlPipe, | ||||
| @@ -47,6 +49,9 @@ export class PreviewPopupComponent implements OnDestroy { | ||||
|   @Input() | ||||
|   linkTitle: string = $localize`Open preview` | ||||
|  | ||||
|   @Input() | ||||
|   previewOnly: boolean = false | ||||
|  | ||||
|   unsubscribeNotifier: Subject<any> = new Subject() | ||||
|  | ||||
|   error = false | ||||
| @@ -91,6 +96,8 @@ export class PreviewPopupComponent implements OnDestroy { | ||||
|   } | ||||
|  | ||||
|   init() { | ||||
|     this.error = false | ||||
|     this.requiresPassword = false | ||||
|     if (this.document.mime_type?.includes('text')) { | ||||
|       this.http | ||||
|         .get(this.previewURL, { responseType: 'text' }) | ||||
| @@ -119,6 +126,7 @@ export class PreviewPopupComponent implements OnDestroy { | ||||
|   } | ||||
|  | ||||
|   mouseEnterPreview() { | ||||
|     if (this.previewOnly) return | ||||
|     this.mouseOnPreview = true | ||||
|     if (!this.popover.isOpen()) { | ||||
|       // we're going to open but hide to pre-load content during hover delay | ||||
| @@ -136,10 +144,12 @@ export class PreviewPopupComponent implements OnDestroy { | ||||
|   } | ||||
|  | ||||
|   mouseLeavePreview() { | ||||
|     if (this.previewOnly) return | ||||
|     this.mouseOnPreview = false | ||||
|   } | ||||
|  | ||||
|   public close(immediate: boolean = false) { | ||||
|     if (this.previewOnly) return | ||||
|     setTimeout( | ||||
|       () => { | ||||
|         if (!this.mouseOnPreview) this.popover.close() | ||||
|   | ||||
| @@ -27,6 +27,7 @@ | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|  | ||||
|   <div class="btn-group flex-fill" role="group"> | ||||
|     <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"> | ||||
| @@ -42,6 +43,13 @@ | ||||
|     </label> | ||||
|   </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"> | ||||
|     <button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle> | ||||
|       <i-bs name="arrow-down-up"></i-bs> | ||||
| @@ -105,8 +113,9 @@ | ||||
|   <pngx-bulk-editor [hidden]="!isBulkEditing" [disabled]="!isBulkEditing"></pngx-bulk-editor> | ||||
| </div> | ||||
|  | ||||
|  | ||||
| <ng-template #pagination> | ||||
| <div class="row"> | ||||
|   <div [class.col-lg-6]="list.showPreviewPane" [class.col]="!list.showPreviewPane"> | ||||
|   <ng-template #pagination> | ||||
|     <div class="d-flex flex-wrap gap-3 justify-content-between align-items-center mb-3"> | ||||
|       <div class="d-flex align-items-center"> | ||||
|         @if (list.isReloading) { | ||||
| @@ -400,3 +409,39 @@ | ||||
|         </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 { | ||||
|   --bs-dropdown-min-width: 12em; | ||||
| } | ||||
|  | ||||
| .preview-pane { | ||||
|   height: 60rem; | ||||
|   top: 70px; | ||||
|   position: sticky; | ||||
| } | ||||
|   | ||||
| @@ -326,25 +326,37 @@ export class DocumentListComponent | ||||
|     this.hotKeyService | ||||
|       .addShortcut({ | ||||
|         keys: 'control.arrowleft', | ||||
|         description: $localize`Previous page`, | ||||
|         description: $localize`Previous page / document`, | ||||
|       }) | ||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|       .subscribe(() => { | ||||
|         if (this.list.showPreviewPane) { | ||||
|           if (this.hasPrevious) { | ||||
|             this.previousDoc() | ||||
|           } | ||||
|         } else { | ||||
|           if (this.list.currentPage > 1) { | ||||
|             this.list.currentPage-- | ||||
|           } | ||||
|         } | ||||
|       }) | ||||
|  | ||||
|     this.hotKeyService | ||||
|       .addShortcut({ | ||||
|         keys: 'control.arrowright', | ||||
|         description: $localize`Next page`, | ||||
|         description: $localize`Next page / document`, | ||||
|       }) | ||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|       .subscribe(() => { | ||||
|         if (this.list.showPreviewPane) { | ||||
|           if (this.hasNext) { | ||||
|             this.nextDoc() | ||||
|           } | ||||
|         } else { | ||||
|           if (this.list.currentPage < this.list.getLastPage()) { | ||||
|             this.list.currentPage++ | ||||
|           } | ||||
|         } | ||||
|       }) | ||||
|   } | ||||
|  | ||||
| @@ -473,4 +485,45 @@ export class DocumentListComponent | ||||
|   resetFilters() { | ||||
|     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. | ||||
|    */ | ||||
|   displayFields?: DisplayField[] | ||||
|  | ||||
|   /** | ||||
|    * Whether the preview pane is shown. | ||||
|    */ | ||||
|   showPreviewPane?: boolean | ||||
| } | ||||
|  | ||||
| /** | ||||
| @@ -165,6 +170,7 @@ export class DocumentListViewService { | ||||
|       sortReverse: true, | ||||
|       filterRules: [], | ||||
|       selected: new Set<number>(), | ||||
|       showPreviewPane: false, | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -451,6 +457,15 @@ export class DocumentListViewService { | ||||
|     this.saveDocumentListView() | ||||
|   } | ||||
|  | ||||
|   get showPreviewPane(): boolean { | ||||
|     return this.activeListViewState.showPreviewPane | ||||
|   } | ||||
|  | ||||
|   set showPreviewPane(show: boolean) { | ||||
|     this.activeListViewState.showPreviewPane = show | ||||
|     this.saveDocumentListView() | ||||
|   } | ||||
|  | ||||
|   private saveDocumentListView() { | ||||
|     if (this._activeSavedViewId == null) { | ||||
|       let savedState: ListViewState = { | ||||
| @@ -461,6 +476,7 @@ export class DocumentListViewService { | ||||
|         sortReverse: this.activeListViewState.sortReverse, | ||||
|         displayMode: this.activeListViewState.displayMode, | ||||
|         displayFields: this.activeListViewState.displayFields, | ||||
|         showPreviewPane: this.activeListViewState.showPreviewPane, | ||||
|       } | ||||
|       localStorage.setItem( | ||||
|         DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG, | ||||
| @@ -626,4 +642,8 @@ export class DocumentListViewService { | ||||
|   documentIndexInCurrentView(documentID: number): number { | ||||
|     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, | ||||
|   uiRadios, | ||||
|   upcScan, | ||||
|   windowSplit, | ||||
|   windowStack, | ||||
|   x, | ||||
|   xCircle, | ||||
| @@ -323,6 +324,7 @@ const icons = { | ||||
|   trash, | ||||
|   uiRadios, | ||||
|   upcScan, | ||||
|   windowSplit, | ||||
|   windowStack, | ||||
|   x, | ||||
|   xCircle, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user