From 2f4c74b8da69b554376450b8a879ff1c6f1e25b7 Mon Sep 17 00:00:00 2001 From: Rubikscraft Date: Mon, 26 Dec 2022 16:55:54 +0100 Subject: [PATCH] Move to axios --- frontend/package.json | 1 + .../picsur-img/picsur-img.component.ts | 8 +- .../models/dto/images-upload-request.dto.ts | 13 + frontend/src/app/services/api/api.service.ts | 270 +++++++++++------- .../src/app/services/api/apikeys.service.ts | 20 +- .../src/app/services/api/image.service.ts | 35 ++- frontend/src/app/services/api/info.service.ts | 2 +- .../app/services/api/permission.service.ts | 2 +- .../src/app/services/api/roles.service.ts | 13 +- .../app/services/api/static-info.service.ts | 6 +- .../src/app/services/api/sys-pref.service.ts | 12 +- .../app/services/api/user-manage.service.ts | 12 +- frontend/src/app/services/api/user.service.ts | 12 +- .../src/app/services/api/usr-pref.service.ts | 12 +- .../download-dialog.component.html | 2 +- .../download-dialog.component.ts | 8 +- .../util/download-manager/download.service.ts | 22 +- yarn.lock | 65 ++++- 18 files changed, 343 insertions(+), 172 deletions(-) create mode 100644 frontend/src/app/models/dto/images-upload-request.dto.ts diff --git a/frontend/package.json b/frontend/package.json index 8df5eaa7..0fce8186 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -38,6 +38,7 @@ "@types/resize-observer-browser": "^0.1.7", "@types/validator": "^13.7.10", "ackee-tracker": "^5.1.0", + "axios": "^1.2.1", "bootstrap": "^5.2.3", "caniuse-lite": "^1.0.30001441", "fuse.js": "^6.6.2", diff --git a/frontend/src/app/components/picsur-img/picsur-img.component.ts b/frontend/src/app/components/picsur-img/picsur-img.component.ts index 3df5c67d..a7827175 100644 --- a/frontend/src/app/components/picsur-img/picsur-img.component.ts +++ b/frontend/src/app/components/picsur-img/picsur-img.component.ts @@ -6,7 +6,7 @@ import { Input, OnChanges, SimpleChanges, - ViewChild, + ViewChild } from '@angular/core'; import { FileType, ImageFileType } from 'picsur-shared/dist/dto/mimes.dto'; import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types'; @@ -85,7 +85,7 @@ export class PicsurImgComponent implements OnChanges { this.state = PicsurImgState.Canvas; } else { - const result = await this.apiService.getBuffer(url); + const result = await this.apiService.getBuffer(url).result; if (HasFailed(result)) return result; const img = this.img.nativeElement; @@ -99,12 +99,12 @@ export class PicsurImgComponent implements OnChanges { } private async getFileType(url: string): AsyncFailable { - const response = await this.apiService.head(url); + const response = await this.apiService.head(url).result; if (HasFailed(response)) { return response; } - const mimeHeader = response.get('content-type') ?? ''; + const mimeHeader = response['content-type'] ?? ''; const mime = mimeHeader.split(';')[0]; return ParseMime2FileType(mime); diff --git a/frontend/src/app/models/dto/images-upload-request.dto.ts b/frontend/src/app/models/dto/images-upload-request.dto.ts new file mode 100644 index 00000000..9d862eae --- /dev/null +++ b/frontend/src/app/models/dto/images-upload-request.dto.ts @@ -0,0 +1,13 @@ +import { MultiPartRequest } from './multi-part-request.dto'; + +export class ImagesUploadRequest implements MultiPartRequest { + constructor(private images: File[]) {} + + public createFormData(): FormData { + const data = new FormData(); + for (let i = 0; i < this.images.length; i++) { + data.append(`images[${i}]`, this.images[i]); + } + return data; + } +} diff --git a/frontend/src/app/services/api/api.service.ts b/frontend/src/app/services/api/api.service.ts index d95ad891..37246ad1 100644 --- a/frontend/src/app/services/api/api.service.ts +++ b/frontend/src/app/services/api/api.service.ts @@ -1,17 +1,23 @@ import { Inject, Injectable } from '@angular/core'; import { WINDOW } from '@ng-web-apis/common'; +import axios, { + AxiosRequestConfig, + AxiosResponse, + AxiosResponseHeaders +} from 'axios'; import { ApiResponseSchema } from 'picsur-shared/dist/dto/api/api.dto'; import { FileType2Ext } from 'picsur-shared/dist/dto/mimes.dto'; import { AsyncFailable, Fail, + Failure, FT, HasFailed, - HasSuccess, + HasSuccess } from 'picsur-shared/dist/types'; import { ZodDtoStatic } from 'picsur-shared/dist/util/create-zod-dto'; import { ParseMime2FileType } from 'picsur-shared/dist/util/parse-mime'; -import { Subject } from 'rxjs'; +import { BehaviorSubject, Observable, Subject } from 'rxjs'; import { ApiBuffer } from 'src/app/models/dto/api-buffer.dto'; import { ApiError } from 'src/app/models/dto/api-error.dto'; import { z } from 'zod'; @@ -23,6 +29,37 @@ import { KeyStorageService } from '../storage/key-storage.service'; Proud of this, it works so smoooth */ +interface RunningRequest { + uploadProgress: Observable; + downloadProgress: Observable; + result: AsyncFailable; + cancel: () => void; +} + +function MapRunningRequest( + runningRequest: RunningRequest, + map: (r: R) => AsyncFailable, +): RunningRequest { + return { + ...runningRequest, + result: runningRequest.result.then(async (result) => { + if (HasFailed(result)) return result; + return map(result); + }), + }; +} + +function CreateFailedRunningRequest(failure: Failure) { + const subject = new Subject(); + subject.complete(); + return { + uploadProgress: subject.asObservable(), + downloadProgress: subject.asObservable(), + result: Promise.resolve(failure), + cancel: () => {}, + } as RunningRequest; +} + @Injectable({ providedIn: 'root', }) @@ -40,177 +77,196 @@ export class ApiService { @Inject(WINDOW) private readonly windowRef: Window, ) {} - public async get( + public get( type: ZodDtoStatic, url: string, - ): AsyncFailable> { + ): RunningRequest> { return this.fetchSafeJson(type, url, { method: 'GET' }); } - public async head(url: string): AsyncFailable { + public head(url: string): RunningRequest { return this.fetchHead(url, { method: 'HEAD' }); } - public async getBuffer(url: string): AsyncFailable { + public getBuffer(url: string): RunningRequest { return this.fetchBuffer(url, { method: 'GET' }); } - public async post( + public post( sendType: ZodDtoStatic, receiveType: ZodDtoStatic, url: string, data: z.infer, - ): AsyncFailable> { + ): RunningRequest> { const sendSchema = sendType.zodSchema; const validateResult = sendSchema.safeParse(data); if (!validateResult.success) { - return Fail( - FT.SysValidation, - 'Something went wrong', - validateResult.error, + return CreateFailedRunningRequest( + Fail(FT.SysValidation, 'Something went wrong', validateResult.error), ); } return this.fetchSafeJson(receiveType, url, { method: 'POST', - body: JSON.stringify(validateResult.data), + data: validateResult.data, }); } - public async postEmpty( + public postEmpty( type: ZodDtoStatic, url: string, - ): AsyncFailable> { + ): RunningRequest> { return this.fetchSafeJson(type, url, { method: 'POST' }); } - public async postForm( + public postForm( receiveType: ZodDtoStatic, url: string, data: MultiPartRequest, - ): AsyncFailable> { + ): RunningRequest> { return this.fetchSafeJson(receiveType, url, { method: 'POST', - body: data.createFormData(), + data: data.createFormData(), }); } - private async fetchSafeJson( + private fetchSafeJson( type: ZodDtoStatic, - url: RequestInfo, - options: RequestInit, - ): AsyncFailable> { + url: string, + options: AxiosRequestConfig, + ): RunningRequest> { const resultSchema = ApiResponseSchema(type.zodSchema as z.AnyZodObject); type resultType = z.infer; - let result = await this.fetchJsonAs(url, options); - if (HasFailed(result)) return result; + let result = this.fetchJsonAs(url, options); - const validateResult = resultSchema.safeParse(result); - if (!validateResult.success) { - return Fail( - FT.SysValidation, - 'Something went wrong', - validateResult.error, - ); - } + return MapRunningRequest(result, async (r) => { + const validateResult = resultSchema.safeParse(r); + if (!validateResult.success) { + return Fail( + FT.SysValidation, + 'Something went wrong', + validateResult.error, + ); + } - if (validateResult.data.success === false) - return Fail(FT.Unknown, result.data.message); + if (validateResult.data.success === false) + return Fail(FT.Unknown, r.data.message); - return validateResult.data.data; + return validateResult.data.data; + }); } - private async fetchJsonAs( - url: RequestInfo, - options: RequestInit, - ): AsyncFailable { - const response = await this.fetch(url, options); - if (HasFailed(response)) { - return response; - } - try { - return await response.json(); - } catch (e) { - return Fail(FT.Internal, e); - } - } + private fetchJsonAs( + url: string, + options: AxiosRequestConfig, + ): RunningRequest { + const response = this.fetch(url, { + ...options, + responseType: 'json', + }); - private async fetchBuffer( - url: RequestInfo, - options: RequestInit, - ): AsyncFailable { - const response = await this.fetch(url, options); - if (HasFailed(response)) return response; + return MapRunningRequest(response, async (r) => r.data); + } - if (!response.ok) return Fail(FT.Network, 'Recieved a non-ok response'); + private fetchBuffer( + url: string, + options: AxiosRequestConfig, + ): RunningRequest { + const response = this.fetch(url, { + ...options, + responseType: 'arraybuffer', + }); - const mimeType = response.headers.get('Content-Type') ?? 'other/unknown'; - let name = response.headers.get('Content-Disposition'); - if (!name) { - if (typeof url === 'string') { + return MapRunningRequest(response, async (r) => { + const mimeType = r.headers['Content-Type'] ?? 'other/unknown'; + let name = r.headers['Content-Disposition']; + if (!name) { name = url.split('/').pop() ?? 'unnamed'; - } else { - name = url.url.split('/').pop() ?? 'unnamed'; } - } - const filetype = ParseMime2FileType(mimeType); - if (HasSuccess(filetype)) { - const ext = FileType2Ext(filetype.identifier); - if (HasSuccess(ext)) { - if (!name.endsWith(ext)) { - name += '.' + ext; + const filetype = ParseMime2FileType(mimeType); + if (HasSuccess(filetype)) { + const ext = FileType2Ext(filetype.identifier); + if (HasSuccess(ext)) { + if (!name.endsWith(ext)) { + name += '.' + ext; + } } } - } - try { - const arrayBuffer = await response.arrayBuffer(); return { - buffer: arrayBuffer, + buffer: r.data, mimeType, name, }; - } catch (e) { - return Fail(FT.Internal, e); - } + }); } - private async fetchHead( - url: RequestInfo, - options: RequestInit, - ): AsyncFailable { - const response = await this.fetch(url, options); - if (HasFailed(response)) return response; - - if (!response.ok) return Fail(FT.Network, 'Recieved a non-ok response'); + private fetchHead( + url: string, + options: AxiosRequestConfig, + ): RunningRequest { + const response = this.fetch(url, options); - return response.headers; + return MapRunningRequest(response, async (r) => { + return r.headers as AxiosResponseHeaders; + }); } - private async fetch( - url: RequestInfo, - options: RequestInit, - ): AsyncFailable { - try { - const key = this.keyService.get(); - const isJSON = typeof options.body === 'string'; - - const headers: any = options.headers || {}; - if (key !== null) - headers['Authorization'] = `Bearer ${this.keyService.get()}`; - if (isJSON) headers['Content-Type'] = 'application/json'; - options.headers = headers; - - return await this.windowRef.fetch(url, options); - } catch (e) { - this.errorSubject.next({ - error: e, - url, - }); - return Fail(FT.Network, e); - } + private fetch( + url: string, + options: AxiosRequestConfig, + ): RunningRequest { + const key = this.keyService.get(); + const isJSON = typeof options.data === 'string'; + + const headers: any = options.headers || {}; + if (key !== null) + headers['Authorization'] = `Bearer ${this.keyService.get()}`; + if (isJSON) headers['Content-Type'] = 'application/json'; + options.headers = headers; + + const uploadProgress = new BehaviorSubject(0); + const downloadProgress = new BehaviorSubject(0); + const abortController = new AbortController(); + + const resultPromise: AsyncFailable = (async () => { + try { + const result = await axios.request({ + url, + onDownloadProgress: (e) => { + downloadProgress.next(e.loaded / (e.total ?? 1000000) * 100); + }, + onUploadProgress: (e) => { + uploadProgress.next(e.loaded / (e.total ?? 1000000) * 100); + }, + signal: abortController.signal, + ...options, + }); + + uploadProgress.complete(); + downloadProgress.complete(); + + if (result.status < 200 || result.status >= 300) { + return Fail(FT.Network, 'Recieved a non-ok response'); + } + return result; + } catch (e) { + return Fail(FT.Network, e); + } + })(); + + return { + result: resultPromise, + uploadProgress, + downloadProgress, + cancel: () => { + abortController.abort(); + uploadProgress.complete(); + downloadProgress.complete(); + }, + }; } } diff --git a/frontend/src/app/services/api/apikeys.service.ts b/frontend/src/app/services/api/apikeys.service.ts index c6b2d3c3..38e2c9ab 100644 --- a/frontend/src/app/services/api/apikeys.service.ts +++ b/frontend/src/app/services/api/apikeys.service.ts @@ -8,7 +8,7 @@ import { ApiKeyListRequest, ApiKeyListResponse, ApiKeyUpdateRequest, - ApiKeyUpdateResponse, + ApiKeyUpdateResponse } from 'picsur-shared/dist/dto/api/apikeys.dto'; import { EApiKey } from 'picsur-shared/dist/entities/apikey.entity'; import { AsyncFailable } from 'picsur-shared/dist/types'; @@ -26,7 +26,7 @@ export class ApiKeysService { page: number, userID?: string, ): AsyncFailable> { - const response = await this.api.post( + return await this.api.post( ApiKeyListRequest, ApiKeyListResponse, '/api/apikeys/list', @@ -35,9 +35,7 @@ export class ApiKeysService { page, user_id: userID, }, - ); - - return response; + ).result; } public async getApiKey(id: string): AsyncFailable { @@ -48,14 +46,12 @@ export class ApiKeysService { { id, }, - ); + ).result; } public async createApiKey(): AsyncFailable { - return await this.api.postEmpty( - ApiKeyCreateResponse, - '/api/apikeys/create', - ); + return await this.api.postEmpty(ApiKeyCreateResponse, '/api/apikeys/create') + .result; } public async updateApiKey(id: string, name: string): AsyncFailable { @@ -67,7 +63,7 @@ export class ApiKeysService { id, name, }, - ); + ).result; } public async deleteApiKey(id: string): AsyncFailable> { @@ -78,6 +74,6 @@ export class ApiKeysService { { id, }, - ); + ).result; } } diff --git a/frontend/src/app/services/api/image.service.ts b/frontend/src/app/services/api/image.service.ts index 7997727b..9018f23b 100644 --- a/frontend/src/app/services/api/image.service.ts +++ b/frontend/src/app/services/api/image.service.ts @@ -23,6 +23,7 @@ import { HasSuccess, Open } from 'picsur-shared/dist/types/failable'; +import { ImagesUploadRequest } from 'src/app/models/dto/images-upload-request.dto'; import { ImageUploadRequest } from '../../models/dto/image-upload-request.dto'; import { ApiService } from './api.service'; import { InfoService } from './info.service'; @@ -43,13 +44,28 @@ export class ImageService { ImageUploadResponse, '/api/image/upload', new ImageUploadRequest(image), - ); + ).result; return Open(result, 'id'); } + public async UploadImages(images: File[]): AsyncFailable { + console.log('Uploading images', images); + + // Split into chunks of 20 + const groups = this.chunks(images, 20); + + const result = await this.api.postForm( + ImageUploadResponse, + '/api/image/upload/bulk', + new ImagesUploadRequest(images), + ); + + return []; + } + public async GetImageMeta(image: string): AsyncFailable { - return await this.api.get(ImageMetaResponse, `/i/meta/${image}`); + return await this.api.get(ImageMetaResponse, `/i/meta/${image}`).result; } public async ListAllImages( @@ -66,7 +82,7 @@ export class ImageService { page, user_id: userID, }, - ); + ).result; } public async ListMyImages( @@ -93,7 +109,7 @@ export class ImageService { id, ...settings, }, - ); + ).result; } public async DeleteImages( @@ -106,7 +122,7 @@ export class ImageService { { ids: images, }, - ); + ).result; } public async DeleteImage(image: string): AsyncFailable { @@ -192,4 +208,13 @@ export class ImageService { ): ImageLinks { return this.CreateImageLinks(this.GetImageURL(imageID, mime, true), name); } + + private chunks(arr: T[], size: number): T[][] { + let result = []; + for (let i = 0; i < arr.length; i += size) { + result.push(arr.slice(i, size + i)); + } + + return result; + } } diff --git a/frontend/src/app/services/api/info.service.ts b/frontend/src/app/services/api/info.service.ts index 8eff4fa7..8bd8e1e4 100644 --- a/frontend/src/app/services/api/info.service.ts +++ b/frontend/src/app/services/api/info.service.ts @@ -101,7 +101,7 @@ export class InfoService { } private async updateInfo(): AsyncFailable { - const response = await this.api.get(InfoResponse, '/api/info'); + const response = await this.api.get(InfoResponse, '/api/info').result; if (HasFailed(response)) return response; this.infoSubject.next(response); diff --git a/frontend/src/app/services/api/permission.service.ts b/frontend/src/app/services/api/permission.service.ts index 7c62ef14..9d6e192d 100644 --- a/frontend/src/app/services/api/permission.service.ts +++ b/frontend/src/app/services/api/permission.service.ts @@ -69,7 +69,7 @@ export class PermissionService { const got = await this.api.get( UserMePermissionsResponse, '/api/user/me/permissions', - ); + ).result; if (HasFailed(got)) return got; this.permissionsSubject.next(got.permissions); diff --git a/frontend/src/app/services/api/roles.service.ts b/frontend/src/app/services/api/roles.service.ts index 53d74f43..90cda8e7 100644 --- a/frontend/src/app/services/api/roles.service.ts +++ b/frontend/src/app/services/api/roles.service.ts @@ -8,7 +8,7 @@ import { RoleInfoResponse, RoleListResponse, RoleUpdateRequest, - RoleUpdateResponse, + RoleUpdateResponse } from 'picsur-shared/dist/dto/api/roles.dto'; import { ERole } from 'picsur-shared/dist/entities/role.entity'; import { AsyncFailable, Open } from 'picsur-shared/dist/types'; @@ -22,7 +22,8 @@ export class RolesService { constructor(private readonly api: ApiService) {} public async getRoles(): AsyncFailable { - const response = await this.api.get(RoleListResponse, '/api/roles/list'); + const response = await this.api.get(RoleListResponse, '/api/roles/list') + .result; return Open(response, 'results'); } @@ -35,7 +36,7 @@ export class RolesService { { name, }, - ); + ).result; } public async createRole(role: RoleModel): AsyncFailable { @@ -44,7 +45,7 @@ export class RolesService { RoleCreateResponse, '/api/roles/create', role, - ); + ).result; } public async updateRole(role: RoleModel): AsyncFailable { @@ -53,7 +54,7 @@ export class RolesService { RoleUpdateResponse, '/api/roles/update', role, - ); + ).result; } public async deleteRole(name: string): AsyncFailable { @@ -64,6 +65,6 @@ export class RolesService { { name, }, - ); + ).result; } } diff --git a/frontend/src/app/services/api/static-info.service.ts b/frontend/src/app/services/api/static-info.service.ts index 29dd277f..590bba27 100644 --- a/frontend/src/app/services/api/static-info.service.ts +++ b/frontend/src/app/services/api/static-info.service.ts @@ -29,7 +29,7 @@ export class StaticInfoService { SoulBoundRoles: [], UndeletableRoles: [], }, - () => this.api.get(SpecialRolesResponse, '/api/roles/special'), + () => this.api.get(SpecialRolesResponse, '/api/roles/special').result, ); } @@ -41,7 +41,7 @@ export class StaticInfoService { LockedLoginUsersList: [], UndeletableUsersList: [], }, - () => this.api.get(GetSpecialUsersResponse, '/api/user/special'), + () => this.api.get(GetSpecialUsersResponse, '/api/user/special').result, ); } @@ -54,7 +54,7 @@ export class StaticInfoService { const res = await this.api.get( AllPermissionsResponse, '/api/info/permissions', - ); + ).result; return Open(res, 'permissions'); }, ); diff --git a/frontend/src/app/services/api/sys-pref.service.ts b/frontend/src/app/services/api/sys-pref.service.ts index c45904ce..05f2edfd 100644 --- a/frontend/src/app/services/api/sys-pref.service.ts +++ b/frontend/src/app/services/api/sys-pref.service.ts @@ -4,19 +4,19 @@ import { GetPreferenceResponse, MultiplePreferencesResponse, UpdatePreferenceRequest, - UpdatePreferenceResponse, + UpdatePreferenceResponse } from 'picsur-shared/dist/dto/api/pref.dto'; import { Permission } from 'picsur-shared/dist/dto/permissions.enum'; import { DecodedPref, - PrefValueType, + PrefValueType } from 'picsur-shared/dist/dto/preferences.dto'; import { AsyncFailable, Fail, FT, HasFailed, - Map, + Map } from 'picsur-shared/dist/types'; import { BehaviorSubject } from 'rxjs'; import { ErrorService } from 'src/app/util/error-manager/error.service'; @@ -69,7 +69,7 @@ export class SysPrefService { const response = await this.api.get( MultiplePreferencesResponse, '/api/pref/sys', - ); + ).result; return Map(response, (pref) => { this.sysprefObservable.next(pref.results); @@ -89,7 +89,7 @@ export class SysPrefService { const response = await this.api.get( GetPreferenceResponse, `/api/pref/sys/${key}`, - ); + ).result; if (!HasFailed(response)) this.updatePrefArray(response); return response; @@ -110,7 +110,7 @@ export class SysPrefService { UpdatePreferenceResponse, `/api/pref/sys/${key}`, { value }, - ); + ).result; if (!HasFailed(response)) this.updatePrefArray(response); return response; diff --git a/frontend/src/app/services/api/user-manage.service.ts b/frontend/src/app/services/api/user-manage.service.ts index 44757b89..f01d6354 100644 --- a/frontend/src/app/services/api/user-manage.service.ts +++ b/frontend/src/app/services/api/user-manage.service.ts @@ -9,7 +9,7 @@ import { UserListRequest, UserListResponse, UserUpdateRequest, - UserUpdateResponse, + UserUpdateResponse } from 'picsur-shared/dist/dto/api/user-manage.dto'; import { EUser } from 'picsur-shared/dist/entities/user.entity'; import { AsyncFailable } from 'picsur-shared/dist/types'; @@ -27,7 +27,7 @@ export class UserAdminService { UserInfoResponse, 'api/user/info', { id }, - ); + ).result; } public async getUsers( @@ -42,7 +42,7 @@ export class UserAdminService { count, page, }, - ); + ).result; } public async createUser(user: UserCreateRequest): AsyncFailable { @@ -51,7 +51,7 @@ export class UserAdminService { UserCreateResponse, '/api/user/create', user, - ); + ).result; } public async updateUser(user: UserUpdateRequest): AsyncFailable { @@ -60,7 +60,7 @@ export class UserAdminService { UserUpdateResponse, '/api/user/update', user, - ); + ).result; } public async deleteUser(id: string): AsyncFailable> { @@ -69,6 +69,6 @@ export class UserAdminService { UserDeleteResponse, '/api/user/delete', { id }, - ); + ).result; } } diff --git a/frontend/src/app/services/api/user.service.ts b/frontend/src/app/services/api/user.service.ts index 8795cfcd..a4f4c355 100644 --- a/frontend/src/app/services/api/user.service.ts +++ b/frontend/src/app/services/api/user.service.ts @@ -7,7 +7,7 @@ import { UserLoginResponse, UserMeResponse, UserRegisterRequest, - UserRegisterResponse, + UserRegisterResponse } from 'picsur-shared/dist/dto/api/user.dto'; import { JwtDataSchema } from 'picsur-shared/dist/dto/jwt.dto'; import { EUser } from 'picsur-shared/dist/entities/user.entity'; @@ -16,7 +16,7 @@ import { Fail, FT, HasFailed, - Open, + Open } from 'picsur-shared/dist/types'; import { BehaviorSubject } from 'rxjs'; import { Logger } from '../logger/logger.service'; @@ -72,7 +72,7 @@ export class UserService { username, password, }, - ); + ).result; if (HasFailed(response)) return response; // Set the key so the apiservice can use it @@ -94,7 +94,7 @@ export class UserService { { username, }, - ), + ).result, 'available', ); } @@ -111,7 +111,7 @@ export class UserService { username, password, }, - ); + ).result; } public async logout(): AsyncFailable { @@ -147,7 +147,7 @@ export class UserService { // This actually fetches up to date information from the server private async fetchUser(): AsyncFailable { - const got = await this.api.get(UserMeResponse, '/api/user/me'); + const got = await this.api.get(UserMeResponse, '/api/user/me').result; if (HasFailed(got)) return got; this.key.set(got.token); diff --git a/frontend/src/app/services/api/usr-pref.service.ts b/frontend/src/app/services/api/usr-pref.service.ts index 3e7cbaf8..a0d13472 100644 --- a/frontend/src/app/services/api/usr-pref.service.ts +++ b/frontend/src/app/services/api/usr-pref.service.ts @@ -4,19 +4,19 @@ import { GetPreferenceResponse, MultiplePreferencesResponse, UpdatePreferenceRequest, - UpdatePreferenceResponse, + UpdatePreferenceResponse } from 'picsur-shared/dist/dto/api/pref.dto'; import { Permission } from 'picsur-shared/dist/dto/permissions.enum'; import { DecodedPref, - PrefValueType, + PrefValueType } from 'picsur-shared/dist/dto/preferences.dto'; import { AsyncFailable, Fail, FT, HasFailed, - Map, + Map } from 'picsur-shared/dist/types'; import { BehaviorSubject } from 'rxjs'; import { ErrorService } from 'src/app/util/error-manager/error.service'; @@ -69,7 +69,7 @@ export class UsrPrefService { const response = await this.api.get( MultiplePreferencesResponse, '/api/pref/usr', - ); + ).result; return Map(response, (pref) => { this.usrprefObservable.next(pref.results); @@ -89,7 +89,7 @@ export class UsrPrefService { const response = await this.api.get( GetPreferenceResponse, `/api/pref/usr/${key}`, - ); + ).result; if (!HasFailed(response)) this.updatePrefArray(response); return response; @@ -110,7 +110,7 @@ export class UsrPrefService { UpdatePreferenceResponse, `/api/pref/usr/${key}`, { value }, - ); + ).result; if (!HasFailed(response)) this.updatePrefArray(response); return response; diff --git a/frontend/src/app/util/dialog-manager/download-dialog/download-dialog.component.html b/frontend/src/app/util/dialog-manager/download-dialog/download-dialog.component.html index 85784ded..e05ebb57 100644 --- a/frontend/src/app/util/dialog-manager/download-dialog/download-dialog.component.html +++ b/frontend/src/app/util/dialog-manager/download-dialog/download-dialog.component.html @@ -1,5 +1,5 @@

Downloading {{ data.name }}...

- +
diff --git a/frontend/src/app/util/dialog-manager/download-dialog/download-dialog.component.ts b/frontend/src/app/util/dialog-manager/download-dialog/download-dialog.component.ts index 17e21705..32c9cfa4 100644 --- a/frontend/src/app/util/dialog-manager/download-dialog/download-dialog.component.ts +++ b/frontend/src/app/util/dialog-manager/download-dialog/download-dialog.component.ts @@ -1,8 +1,10 @@ import { Component, Inject } from '@angular/core'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { Observable } from 'rxjs'; export interface DownloadDialogData { name: string; + progress?: Observable; } @Component({ @@ -10,8 +12,12 @@ export interface DownloadDialogData { templateUrl: './download-dialog.component.html', }) export class DownloadDialogComponent { + public progress: Observable; + constructor( public readonly dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public readonly data: DownloadDialogData, - ) {} + ) { + this.progress = data.progress ?? new Observable(); + } } diff --git a/frontend/src/app/util/download-manager/download.service.ts b/frontend/src/app/util/download-manager/download.service.ts index 8e88218a..0af30aa3 100644 --- a/frontend/src/app/util/download-manager/download.service.ts +++ b/frontend/src/app/util/download-manager/download.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { Fail, FT, HasFailed } from 'picsur-shared/dist/types'; +import { Observable } from 'rxjs'; import { ApiService } from 'src/app/services/api/api.service'; import { Logger } from 'src/app/services/logger/logger.service'; import { DownloadDialogComponent } from '../dialog-manager/download-dialog/download-dialog.component'; @@ -20,9 +21,12 @@ export class DownloadService { private readonly errorService: ErrorService, ) {} - public showDownloadDialog(filename: string): () => void { + public showDownloadDialog( + filename: string, + progress?: Observable, + ): () => void { const ref = this.dialog.open(DownloadDialogComponent, { - data: { name: filename }, + data: { name: filename, progress: progress }, disableClose: true, closeOnNavigation: false, }); @@ -31,11 +35,17 @@ export class DownloadService { } public async downloadFile(url: string) { - const closeDialog = this.showDownloadDialog('image'); - const file = await this.api.getBuffer(url); - if (HasFailed(file)) + + const request = this.api.getBuffer(url); + const closeDialog = this.showDownloadDialog('image', request.downloadProgress); + + const file = await request.result; + + if (HasFailed(file)){ + closeDialog(); return this.errorService.showFailure(file, this.logger); + } this.util.downloadBuffer(file.buffer, file.name, file.mimeType); @@ -80,7 +90,7 @@ export class DownloadService { url, }; } else { - const image = await this.api.getBuffer(url); + const image = await this.api.getBuffer(url).result; if (HasFailed(image)) return this.errorService.showFailure(image, this.logger); diff --git a/yarn.lock b/yarn.lock index 821f33f4..dc3e3881 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4354,6 +4354,13 @@ __metadata: languageName: node linkType: hard +"asynckit@npm:^0.4.0": + version: 0.4.0 + resolution: "asynckit@npm:0.4.0" + checksum: 7b78c451df768adba04e2d02e63e2d0bf3b07adcd6e42b4cf665cb7ce899bedd344c69a1dcbce355b5f972d597b25aaa1c1742b52cffd9caccb22f348114f6be + languageName: node + linkType: hard + "atomic-sleep@npm:^1.0.0": version: 1.0.0 resolution: "atomic-sleep@npm:1.0.0" @@ -4390,6 +4397,17 @@ __metadata: languageName: node linkType: hard +"axios@npm:^1.2.1": + version: 1.2.1 + resolution: "axios@npm:1.2.1" + dependencies: + follow-redirects: ^1.15.0 + form-data: ^4.0.0 + proxy-from-env: ^1.1.0 + checksum: c4dc4e119064c9aed09a3de309bedb797a139a6fb372223aafe3e0c10a7d4a14e4d3e9c9d309467fadb9d2b490b891ee3df96ef5b55716bb971910466ff9f0c5 + languageName: node + linkType: hard + "babel-loader@npm:9.1.0": version: 9.1.0 resolution: "babel-loader@npm:9.1.0" @@ -5041,6 +5059,15 @@ __metadata: languageName: node linkType: hard +"combined-stream@npm:^1.0.8": + version: 1.0.8 + resolution: "combined-stream@npm:1.0.8" + dependencies: + delayed-stream: ~1.0.0 + checksum: 49fa4aeb4916567e33ea81d088f6584749fc90c7abec76fd516bf1c5aa5c79f3584b5ba3de6b86d26ddd64bae5329c4c7479343250cfe71c75bb366eae53bb7c + languageName: node + linkType: hard + "commander@npm:4.1.1": version: 4.1.1 resolution: "commander@npm:4.1.1" @@ -5411,6 +5438,13 @@ __metadata: languageName: node linkType: hard +"delayed-stream@npm:~1.0.0": + version: 1.0.0 + resolution: "delayed-stream@npm:1.0.0" + checksum: 46fe6e83e2cb1d85ba50bd52803c68be9bd953282fa7096f51fc29edd5d67ff84ff753c51966061e5ba7cb5e47ef6d36a91924eddb7f3f3483b1c560f77a0020 + languageName: node + linkType: hard + "delegates@npm:^1.0.0": version: 1.0.0 resolution: "delegates@npm:1.0.0" @@ -6546,6 +6580,16 @@ __metadata: languageName: node linkType: hard +"follow-redirects@npm:^1.15.0": + version: 1.15.2 + resolution: "follow-redirects@npm:1.15.2" + peerDependenciesMeta: + debug: + optional: true + checksum: faa66059b66358ba65c234c2f2a37fcec029dc22775f35d9ad6abac56003268baf41e55f9ee645957b32c7d9f62baf1f0b906e68267276f54ec4b4c597c2b190 + languageName: node + linkType: hard + "fork-ts-checker-webpack-plugin@npm:7.2.13": version: 7.2.13 resolution: "fork-ts-checker-webpack-plugin@npm:7.2.13" @@ -6573,6 +6617,17 @@ __metadata: languageName: node linkType: hard +"form-data@npm:^4.0.0": + version: 4.0.0 + resolution: "form-data@npm:4.0.0" + dependencies: + asynckit: ^0.4.0 + combined-stream: ^1.0.8 + mime-types: ^2.1.12 + checksum: 01135bf8675f9d5c61ff18e2e2932f719ca4de964e3be90ef4c36aacfc7b9cb2fceb5eca0b7e0190e3383fe51c5b37f4cb80b62ca06a99aaabfcfd6ac7c9328c + languageName: node + linkType: hard + "formdata-polyfill@npm:^4.0.10": version: 4.0.10 resolution: "formdata-polyfill@npm:4.0.10" @@ -8199,7 +8254,7 @@ __metadata: languageName: node linkType: hard -"mime-types@npm:^2.1.27, mime-types@npm:^2.1.31, mime-types@npm:~2.1.17, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": +"mime-types@npm:^2.1.12, mime-types@npm:^2.1.27, mime-types@npm:^2.1.31, mime-types@npm:~2.1.17, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" dependencies: @@ -9532,6 +9587,7 @@ __metadata: "@types/resize-observer-browser": ^0.1.7 "@types/validator": ^13.7.10 ackee-tracker: ^5.1.0 + axios: ^1.2.1 bootstrap: ^5.2.3 caniuse-lite: ^1.0.30001441 fuse.js: ^6.6.2 @@ -9905,6 +9961,13 @@ __metadata: languageName: node linkType: hard +"proxy-from-env@npm:^1.1.0": + version: 1.1.0 + resolution: "proxy-from-env@npm:1.1.0" + checksum: ed7fcc2ba0a33404958e34d95d18638249a68c430e30fcb6c478497d72739ba64ce9810a24f53a7d921d0c065e5b78e3822759800698167256b04659366ca4d4 + languageName: node + linkType: hard + "prr@npm:~1.0.1": version: 1.0.1 resolution: "prr@npm:1.0.1"