RED-3842: Show progress bar when downloading file

This commit is contained in:
Dan Percic 2022-06-28 16:47:27 +03:00
parent 9d62607b64
commit 54bf4cbc09
8 changed files with 71 additions and 23 deletions

View File

@ -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);
}

View File

@ -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<Blob>): 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<Blob> {
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<Blob> {
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<Blob>) => event.body),
);
}
#showLoadingIfIsDownloadEvent(event: HttpEvent<Blob>, 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),
});
}
}

View File

@ -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;

View File

@ -215,8 +215,8 @@ export class FileUploadService extends GenericService<IFileUploadResult> 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<IFileUploadResult> 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<IFileUploadResult> impleme
this.scheduleUpload(uploadFile);
}
},
);
});
}
private _removeUpload(fileUploadModel: FileUploadModel) {

View File

@ -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<unknown> {
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<unknown> {
return this._post(body, `rotate/${dossierId}/${fileId}`);
}
downloadOriginalFile(dossierId: string, fileId: string, observe?: 'body', indicator?: string): Observable<Blob>;
downloadOriginalFile(dossierId: string, fileId: string, observe?: 'response', indicator?: string): Observable<HttpResponse<Blob>>;
downloadOriginal(dossierId: string, fileId: string, observe?: 'events', indicator?: string): Observable<HttpEvent<Blob>>;
downloadOriginal(dossierId: string, fileId: string, observe?: 'response', indicator?: string): Observable<HttpResponse<Blob>>;
@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<unknown> {
params: this._queryParams(queryParams),
headers: headers,
observe: observe,
reportProgress: observe === 'events',
});
}
}

@ -1 +1 @@
Subproject commit f9e24883381ddbf93df5074ec1c176973db44ed1
Subproject commit d5ded3615fdf420ca42c21826654013165279462

View File

@ -31,6 +31,7 @@ export class File extends Entity<IFile> 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<IFile> 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;

View File

@ -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.
*/