diff --git a/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts b/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts index cf8b02fd6..27baa47d8 100644 --- a/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts +++ b/apps/red-ui/src/app/modules/file-preview/file-preview-screen.component.ts @@ -92,7 +92,6 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni private readonly _router: Router, private readonly _ngZone: NgZone, private readonly _logger: NGXLogger, - private readonly _filesService: FilesService, private readonly _annotationManager: REDAnnotationManager, private readonly _errorService: ErrorService, private readonly _filterService: FilterService, @@ -347,7 +346,7 @@ export class FilePreviewScreenComponent extends AutoUnsubscribe implements OnIni async downloadOriginalFile({ cacheIdentifier, dossierId, fileId, filename }: File) { const fileManagementService = this._injector.get(FileManagementService); - const originalFile = fileManagementService.downloadOriginalFile(dossierId, fileId, 'response', cacheIdentifier); + const originalFile = fileManagementService.downloadOriginal(dossierId, fileId, 'response', cacheIdentifier); download(await firstValueFrom(originalFile), filename); } diff --git a/apps/red-ui/src/app/modules/file-preview/services/file-preview-state.service.ts b/apps/red-ui/src/app/modules/file-preview/services/file-preview-state.service.ts index 2f0e883c9..4f138b3c1 100644 --- a/apps/red-ui/src/app/modules/file-preview/services/file-preview-state.service.ts +++ b/apps/red-ui/src/app/modules/file-preview/services/file-preview-state.service.ts @@ -4,7 +4,7 @@ import { Dictionary, Dossier, DOSSIER_ID, File, FILE_ID } from '@red/domain'; import { ActivatedRoute, Router } from '@angular/router'; import { FilesMapService } from '@services/files/files-map.service'; import { PermissionsService } from '@services/permissions.service'; -import { boolFactory } from '@iqser/common-ui'; +import { boolFactory, LoadingService } from '@iqser/common-ui'; import { filter, map, startWith, tap, withLatestFrom } from 'rxjs/operators'; import { FileManagementService } from '@services/files/file-management.service'; import { dossiersServiceResolver } from '@services/entity-services/dossiers.service.provider'; @@ -12,6 +12,28 @@ import { wipeFilesCache } from '@red/cache'; import { DossiersService } from '@services/dossiers/dossiers.service'; import { FilesService } from '@services/files/files.service'; import { DictionaryService } from '@services/entity-services/dictionary.service'; +import { HttpEvent, HttpEventType, HttpProgressEvent, HttpResponse } from '@angular/common/http'; + +const ONE_MEGABYTE = 1024 * 1024; + +function getRemainingTime(event: HttpProgressEvent, startTime: number) { + const currTime = new Date().getTime(); + const remaining = event.total - event.loaded; + const speed = event.loaded / ((currTime - startTime) / 1000); + return Math.round(remaining / speed); +} + +function getRemainingTimeVerbose(event: HttpProgressEvent, startTime: number) { + const remainingTime = getRemainingTime(event, startTime); + if (remainingTime > 60) { + return `${Math.round(remainingTime / 60)} minutes`; + } + return `${remainingTime} seconds`; +} + +function isDownload(event: HttpEvent): event is HttpProgressEvent { + return event.type === HttpEventType.DownloadProgress && event.total > ONE_MEGABYTE; +} @Injectable() export class FilePreviewStateService { @@ -40,6 +62,7 @@ export class FilePreviewStateService { private readonly _dossiersService: DossiersService, private readonly _fileManagementService: FileManagementService, private readonly _dictionaryService: DictionaryService, + private readonly _loadingService: LoadingService, ) { const dossiersService = dossiersServiceResolver(_injector, router); @@ -97,8 +120,35 @@ export class FilePreviewStateService { } #downloadOriginalFile(cacheIdentifier: string, wipeCaches = true): Observable { - const downloadFile = this._fileManagementService.downloadOriginalFile(this.dossierId, this.fileId, 'body', cacheIdentifier); + const downloadFile$ = this.#getFileToDownload(cacheIdentifier); const obs = wipeCaches ? from(wipeFilesCache()) : of({}); - return obs.pipe(switchMap(() => downloadFile)); + return obs.pipe(switchMap(() => downloadFile$)); + } + + #getFileToDownload(cacheIdentifier: string): Observable { + const downloadFile$ = this._fileManagementService.downloadOriginal(this.dossierId, this.fileId, 'events', cacheIdentifier); + let start; + return downloadFile$.pipe( + tap(() => (start ? undefined : (start = new Date().getTime()))), + tap(event => this.#showLoadingIfIsDownloadEvent(event, start)), + filter(event => event.type === HttpEventType.Response), + map((event: HttpResponse) => event.body), + ); + } + + #showLoadingIfIsDownloadEvent(event: HttpEvent, start) { + if (isDownload(event)) { + this.#updateDownloadProgress(event, start); + } + } + + #updateDownloadProgress(event: HttpProgressEvent, startTime: number) { + const progress = Math.round((event.loaded / event.total) * 100); + this._loadingService.update({ + title: 'Loading ' + this.file.filename, + type: 'progress-bar', + value: progress, + remainingTime: getRemainingTimeVerbose(event, startTime), + }); } } diff --git a/apps/red-ui/src/app/modules/shared-dossiers/components/file-actions/file-actions.component.ts b/apps/red-ui/src/app/modules/shared-dossiers/components/file-actions/file-actions.component.ts index de8bee50e..de12143e1 100644 --- a/apps/red-ui/src/app/modules/shared-dossiers/components/file-actions/file-actions.component.ts +++ b/apps/red-ui/src/app/modules/shared-dossiers/components/file-actions/file-actions.component.ts @@ -10,7 +10,7 @@ import { ViewChild, } from '@angular/core'; import { PermissionsService } from '@services/permissions.service'; -import { Action, ActionTypes, Dossier, File } from '@red/domain'; +import { Action, ActionTypes, Dossier, File, User } from '@red/domain'; import { DossiersDialogService } from '../../services/dossiers-dialog.service'; import { CircleButtonType, @@ -46,7 +46,7 @@ import { ROTATION_ACTION_BUTTONS } from '../../../pdf-viewer/utils/constants'; }) export class FileActionsComponent implements OnChanges { readonly circleButtonTypes = CircleButtonTypes; - readonly currentUser; + readonly currentUser: User; @Input() file: File; @Input() dossier: Dossier; diff --git a/apps/red-ui/src/app/modules/upload-download/services/file-upload.service.ts b/apps/red-ui/src/app/modules/upload-download/services/file-upload.service.ts index 242c54047..f3ca4fbac 100644 --- a/apps/red-ui/src/app/modules/upload-download/services/file-upload.service.ts +++ b/apps/red-ui/src/app/modules/upload-download/services/file-upload.service.ts @@ -215,8 +215,8 @@ export class FileUploadService extends GenericService impleme private _createSubscription(uploadFile: FileUploadModel) { this.activeUploads++; const obs = this.uploadFileForm(uploadFile.dossierId, uploadFile.keepManualRedactions, uploadFile.file); - return obs.subscribe( - event => { + return obs.subscribe({ + next: event => { if (event.type === HttpEventType.UploadProgress) { uploadFile.progress = Math.round((event.loaded / (event.total || event.loaded)) * 100); this._applicationRef.tick(); @@ -234,7 +234,7 @@ export class FileUploadService extends GenericService impleme this._removeUpload(uploadFile); } }, - (err: HttpErrorResponse) => { + error: (err: HttpErrorResponse) => { uploadFile.completed = true; uploadFile.error = { // Extract error message @@ -246,7 +246,7 @@ export class FileUploadService extends GenericService impleme this.scheduleUpload(uploadFile); } }, - ); + }); } private _removeUpload(fileUploadModel: FileUploadModel) { diff --git a/apps/red-ui/src/app/services/files/file-management.service.ts b/apps/red-ui/src/app/services/files/file-management.service.ts index 4d1d1ec9f..1797710bf 100644 --- a/apps/red-ui/src/app/services/files/file-management.service.ts +++ b/apps/red-ui/src/app/services/files/file-management.service.ts @@ -1,21 +1,16 @@ import { GenericService, HeadersConfiguration, List, QueryParam, RequiredParam, Validate } from '@iqser/common-ui'; import { Injectable, Injector } from '@angular/core'; -import { HttpHeaders, HttpResponse } from '@angular/common/http'; +import { HttpEvent, HttpHeaders, HttpResponse } from '@angular/common/http'; import { Observable } from 'rxjs'; import { switchMap } from 'rxjs/operators'; import { FilesService } from './files.service'; -import { DossierStatsService } from '../dossiers/dossier-stats.service'; import { File, IPageRotationRequest } from '@red/domain'; @Injectable({ providedIn: 'root', }) export class FileManagementService extends GenericService { - constructor( - protected readonly _injector: Injector, - private readonly _filesService: FilesService, - private readonly _dossierStatsService: DossierStatsService, - ) { + constructor(protected readonly _injector: Injector, private readonly _filesService: FilesService) { super(_injector, ''); } @@ -30,13 +25,13 @@ export class FileManagementService extends GenericService { return this._post(body, `rotate/${dossierId}/${fileId}`); } - downloadOriginalFile(dossierId: string, fileId: string, observe?: 'body', indicator?: string): Observable; - downloadOriginalFile(dossierId: string, fileId: string, observe?: 'response', indicator?: string): Observable>; + downloadOriginal(dossierId: string, fileId: string, observe?: 'events', indicator?: string): Observable>; + downloadOriginal(dossierId: string, fileId: string, observe?: 'response', indicator?: string): Observable>; @Validate() - downloadOriginalFile( + downloadOriginal( @RequiredParam() dossierId: string, @RequiredParam() fileId: string, - observe: 'body' | 'response' = 'body', + observe: 'events' | 'response' = 'events', indicator?: string, ) { const queryParams: QueryParam[] = [{ key: 'inline', value: true }]; @@ -56,6 +51,7 @@ export class FileManagementService extends GenericService { params: this._queryParams(queryParams), headers: headers, observe: observe, + reportProgress: observe === 'events', }); } } diff --git a/libs/common-ui b/libs/common-ui index f9e248833..d5ded3615 160000 --- a/libs/common-ui +++ b/libs/common-ui @@ -1 +1 @@ -Subproject commit f9e24883381ddbf93df5074ec1c176973db44ed1 +Subproject commit d5ded3615fdf420ca42c21826654013165279462 diff --git a/libs/red-domain/src/lib/files/file.model.ts b/libs/red-domain/src/lib/files/file.model.ts index 54f4ad208..7d3886e83 100644 --- a/libs/red-domain/src/lib/files/file.model.ts +++ b/libs/red-domain/src/lib/files/file.model.ts @@ -31,6 +31,7 @@ export class File extends Entity implements IFile { readonly fileAttributes: FileAttributes; readonly fileId: string; readonly filename: string; + readonly fileSize: number; readonly hasAnnotationComments: boolean; readonly hasHints: boolean; readonly hasImages: boolean; @@ -94,6 +95,7 @@ export class File extends Entity implements IFile { this.excludedFromAutomaticAnalysis = !!file.excludedFromAutomaticAnalysis; this.fileId = file.fileId; this.filename = file.filename; + this.fileSize = file.fileSize; this.hasAnnotationComments = !!file.hasAnnotationComments; this.hasHints = !!file.hasHints; this.hasImages = !!file.hasImages; diff --git a/libs/red-domain/src/lib/files/file.ts b/libs/red-domain/src/lib/files/file.ts index 59ba00cd8..0d0155445 100644 --- a/libs/red-domain/src/lib/files/file.ts +++ b/libs/red-domain/src/lib/files/file.ts @@ -62,6 +62,7 @@ export interface IFile { * The file's name. */ readonly filename: string; + readonly fileSize: number; /** * Shows if this file has comments on annotations. */