From bdc893d8a5698352ad96ba581634068ba4229d42 Mon Sep 17 00:00:00 2001 From: MathieuG-P <40181755+Zagrios@users.noreply.github.com> Date: Fri, 19 May 2023 19:06:59 +0200 Subject: [PATCH] [feature-136] models management almost finished, still need to add translations --- package-lock.json | 23 +++++-- package.json | 1 + src/main/helpers/fs.helpers.ts | 2 +- src/main/ipcs/bs-model-ipcs.ts | 6 ++ src/main/models/archive.class.ts | 15 ++-- .../local-maps-manager.service.ts | 4 +- .../local-models-manager.service.ts | 61 +++++++++++----- .../local-playlists-manager.service.ts | 4 +- .../services/mods/bs-mods-manager.service.ts | 3 +- src/main/services/request.service.ts | 64 +++++++++++++---- .../model-saber/model-saber.service.ts | 9 ++- .../download-models-modal.component.tsx | 58 +++++++++++----- .../model-item.component.tsx | 16 ++--- .../models-grid.component.tsx | 36 +++++++--- .../models-panel.component.tsx | 45 ++++++++---- .../shared/bsm-select.component.tsx | 10 +-- .../pages/version-viewer.component.tsx | 2 +- .../services/model-downloader.service.ts | 2 +- .../models-downloader.service.ts | 69 ++++++++++++++----- .../models-manager.service.ts | 4 +- .../models/bsm-local-model.interface.ts | 2 + 21 files changed, 308 insertions(+), 128 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7f253cbe..0d606d2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,6 +40,7 @@ "rxjs": "^7.8.0", "sanitize-filename": "^1.6.3", "semver": "^7.3.8", + "striptags": "^4.0.0-alpha.4", "tailwind-scrollbar-hide": "^1.1.7", "tailwindcss-scoped-groups": "^2.0.0", "tippy.js": "^6.3.7", @@ -9553,9 +9554,9 @@ } }, "node_modules/html-tags": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.2.0.tgz", - "integrity": "sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", "dev": true, "engines": { "node": ">=8" @@ -16737,6 +16738,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/striptags": { + "version": "4.0.0-alpha.4", + "resolved": "https://registry.npmjs.org/striptags/-/striptags-4.0.0-alpha.4.tgz", + "integrity": "sha512-/0jWyVWhpg9ciRHfjKYBpMHXct/HrFRfsR2HU77nGPbc8SPcVSIHZlZR/0TG3MyPq2C+HiHuwx8BlbcdI/cNbw==" + }, "node_modules/style-loader": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz", @@ -26005,9 +26011,9 @@ } }, "html-tags": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.2.0.tgz", - "integrity": "sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", "dev": true }, "html-webpack-plugin": { @@ -31422,6 +31428,11 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, + "striptags": { + "version": "4.0.0-alpha.4", + "resolved": "https://registry.npmjs.org/striptags/-/striptags-4.0.0-alpha.4.tgz", + "integrity": "sha512-/0jWyVWhpg9ciRHfjKYBpMHXct/HrFRfsR2HU77nGPbc8SPcVSIHZlZR/0TG3MyPq2C+HiHuwx8BlbcdI/cNbw==" + }, "style-loader": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz", diff --git a/package.json b/package.json index 6354d0f0..febf9c17 100644 --- a/package.json +++ b/package.json @@ -259,6 +259,7 @@ "rxjs": "^7.8.0", "sanitize-filename": "^1.6.3", "semver": "^7.3.8", + "striptags": "^4.0.0-alpha.4", "tailwind-scrollbar-hide": "^1.1.7", "tailwindcss-scoped-groups": "^2.0.0", "tippy.js": "^6.3.7", diff --git a/src/main/helpers/fs.helpers.ts b/src/main/helpers/fs.helpers.ts index 4dc75ebf..1a46beea 100644 --- a/src/main/helpers/fs.helpers.ts +++ b/src/main/helpers/fs.helpers.ts @@ -130,6 +130,6 @@ export async function copyDirectoryWithJunctions(src: string, dest: string, opti export interface Progression{ total: number; current: number; - extra?: T; + data?: T; } diff --git a/src/main/ipcs/bs-model-ipcs.ts b/src/main/ipcs/bs-model-ipcs.ts index 19fd4a1f..bc7f3873 100644 --- a/src/main/ipcs/bs-model-ipcs.ts +++ b/src/main/ipcs/bs-model-ipcs.ts @@ -8,6 +8,7 @@ import { BSVersion } from "shared/bs-version.interface"; import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"; import { from } from "rxjs"; import { BsmLocalModel } from "shared/models/models/bsm-local-model.interface"; +import { ModelDownload } from "renderer/services/models-management/models-downloader.service"; const ipc = IpcService.getInstance(); @@ -67,6 +68,11 @@ ipcMain.on("is-models-deep-links-enabled", async (event, request: IpcRequest("download-model", async (req, reply) => { + const models = LocalModelsManagerService.getInstance(); + reply(models.downloadModel(req.args.model, req.args.version)); +}); + ipc.on<{version: BSVersion, type: MSModelType}>("get-version-models", async (req, reply) => { const models = LocalModelsManagerService.getInstance(); const res = await models.getModels(req.args.type, req.args.version); diff --git a/src/main/models/archive.class.ts b/src/main/models/archive.class.ts index 841dcbcb..38bfffba 100644 --- a/src/main/models/archive.class.ts +++ b/src/main/models/archive.class.ts @@ -5,6 +5,7 @@ import recursive from "recursive-readdir"; import { lstatSync } from "fs"; import * as _path from "path"; import { ArchiveProgress } from "shared/models/archive.interface"; +import { Progression } from "main/helpers/fs.helpers"; export class Archive{ @@ -49,21 +50,21 @@ export class Archive{ } - public finalize(): Observable{ + public finalize(): Observable{ - const progress: ArchiveProgress = { - totalFiles: 0, - prossesedFiles: 0 + const progress: Progression = { + total: 0, + current: 0 }; - return new Observable(observer => { + return new Observable(observer => { (async () => { - progress.totalFiles = await this.loadTotalFiles(); + progress.total = await this.loadTotalFiles(); this.archive.on("entry", e => { if(e.stats.isDirectory()){ return; } - progress.prossesedFiles++; + progress.current++; observer.next(progress); }); diff --git a/src/main/services/additional-content/local-maps-manager.service.ts b/src/main/services/additional-content/local-maps-manager.service.ts index 308157ad..a7f50259 100644 --- a/src/main/services/additional-content/local-maps-manager.service.ts +++ b/src/main/services/additional-content/local-maps-manager.service.ts @@ -16,7 +16,7 @@ import log from 'electron-log'; import { WindowManagerService } from "../window-manager.service"; import { ipcMain } from "electron"; import { IpcRequest } from 'shared/models/ipc'; -import { Observable } from "rxjs"; +import { Observable, lastValueFrom } from "rxjs"; import { Archive } from "../../models/archive.class"; import { deleteFolder, ensureFolderExist, getFoldersInFolder, pathExist } from "../../helpers/fs.helpers"; import { readFile } from "fs/promises"; @@ -116,7 +116,7 @@ export class LocalMapsManagerService { await ensureFolderExist(this.utils.getTempPath()); const dest = path.join(tempPath, fileName); - const zipPath = await this.reqService.downloadFile(zipUrl, dest); + const zipPath = (await lastValueFrom(this.reqService.downloadFile(zipUrl, dest))).data; const zip = new StreamZip.async({file : zipPath}); return {zip, zipPath}; diff --git a/src/main/services/additional-content/local-models-manager.service.ts b/src/main/services/additional-content/local-models-manager.service.ts index df74310a..525088ad 100644 --- a/src/main/services/additional-content/local-models-manager.service.ts +++ b/src/main/services/additional-content/local-models-manager.service.ts @@ -14,7 +14,7 @@ import sanitize from "sanitize-filename"; import { Progression, ensureFolderExist, unlinkPath } from "../../helpers/fs.helpers"; import { MODEL_FILE_EXTENSIONS, MODEL_TYPES, MODEL_TYPE_FOLDERS } from "../../../shared/models/models/constants"; import { InstallationLocationService } from "../installation-location.service"; -import { Observable, lastValueFrom } from "rxjs"; +import { Observable, Subscription, lastValueFrom, map } from "rxjs"; import { readdir } from "fs/promises"; import md5File from "md5-file"; import { allSettled } from "../../../shared/helpers/promise.helpers"; @@ -85,13 +85,42 @@ export class LocalModelsManagerService { } - public async downloadModel(model: MSModel, version: BSVersion): Promise{ - - const modelFolder = await this.getModelFolderPath(model.type, version); - const modelDest = path.join(modelFolder, sanitize(path.basename(model.download))); + public downloadModel(model: MSModel, version: BSVersion): Observable>{ + return new Observable>(subscriber => { - return this.request.downloadFile(model.download, modelDest); + const subs: Subscription[] = []; + (async () => { + + const modelFolder = await this.getModelFolderPath(model.type, version); + const modelDest = path.join(modelFolder, sanitize(path.basename(model.download))); + + let url = model.download.split("/"); + url[url.length - 1] = encodeURIComponent(url[url.length - 1]); + + const download$ = this.request.downloadFile(url.join("/"), modelDest); + + subs.push(download$.subscribe({ next: value => subscriber.next({...value, data: undefined}), error: e => subscriber.error(e) })); + + const downloaded = await lastValueFrom(download$); + + const res: BsmLocalModel = { + path: downloaded.data, + fileName: path.basename(downloaded.data), + hash: await md5File(downloaded.data), + type: model.type, + model, + version + } + + subscriber.next({...downloaded, data: res}); + + })().catch(err => subscriber.error(err)).then(() => subscriber.complete()); + + return () => { + subs.forEach(sub => sub.unsubscribe()); + } + }); } public async oneClickDownloadModel(model: MSModel): Promise{ @@ -104,13 +133,13 @@ export class LocalModelsManagerService { const fisrtVersion = versions.shift(); - const downloaded = await this.downloadModel(model, fisrtVersion); + const downloaded = await lastValueFrom(this.downloadModel(model, fisrtVersion)); for(const version of versions){ - const modelDest = path.join(await this.getModelFolderPath(model.type, version), path.basename(downloaded)); + const modelDest = path.join(await this.getModelFolderPath(model.type, version), path.basename(downloaded.data.path)); - copyFileSync(downloaded, modelDest); + copyFileSync(downloaded.data.path, modelDest); } @@ -133,7 +162,7 @@ export class LocalModelsManagerService { const progression: Progression = { total: 0, current: 0, - extra: null + data: null }; return new Observable>(subscriber => { @@ -149,7 +178,7 @@ export class LocalModelsManagerService { path: modelPath, fileName: path.basename(modelPath, MODEL_FILE_EXTENSIONS[type]), model: await this.modelSaber.getModelByHash(hash), - type, hash + type, hash, version } progression.current++; @@ -159,7 +188,7 @@ export class LocalModelsManagerService { return localModel; })); - progression.extra = models; + progression.data = models; subscriber.next(progression); })().catch(e => subscriber.error(e)).finally(() => subscriber.complete()); @@ -167,11 +196,9 @@ export class LocalModelsManagerService { } - public exportModels(output: string, version?: BSVersion, models?: BsmLocalModel[]): Observable{ - // TOTO NOT ASYNC - + public exportModels(output: string, version?: BSVersion, models?: BsmLocalModel[]): Observable{ - return new Observable(subscriber => { + return new Observable(subscriber => { const archive = new Archive(output); @@ -199,7 +226,7 @@ export class LocalModelsManagerService { const progression: Progression = { total: models.length, current: 0, - extra: null + data: null }; for(const model of models){ diff --git a/src/main/services/additional-content/local-playlists-manager.service.ts b/src/main/services/additional-content/local-playlists-manager.service.ts index 4dc06018..e78d8bed 100644 --- a/src/main/services/additional-content/local-playlists-manager.service.ts +++ b/src/main/services/additional-content/local-playlists-manager.service.ts @@ -1,5 +1,5 @@ import path from "path"; -import { BehaviorSubject, Observable } from "rxjs"; +import { BehaviorSubject, Observable, lastValueFrom } from "rxjs"; import { BSVersion } from "shared/bs-version.interface"; import { BSLocalVersionService } from "../bs-local-version.service"; import { DeepLinkService } from "../deep-link.service"; @@ -97,7 +97,7 @@ export class LocalPlaylistsManagerService { copyFileSync(bpListUrlOrPath, bpListDest); } else{ - await this.request.downloadFile(bpListUrlOrPath, bpListDest); + await lastValueFrom(this.request.downloadFile(bpListUrlOrPath, bpListDest)); } return bpListDest; diff --git a/src/main/services/mods/bs-mods-manager.service.ts b/src/main/services/mods/bs-mods-manager.service.ts index a13f6fad..74b2d535 100644 --- a/src/main/services/mods/bs-mods-manager.service.ts +++ b/src/main/services/mods/bs-mods-manager.service.ts @@ -12,6 +12,7 @@ import { spawn } from "child_process"; import { BS_EXECUTABLE } from "../../constants"; import log from "electron-log"; import { deleteFolder, ensureFolderExist, pathExist, unlinkPath } from "../../helpers/fs.helpers"; +import { lastValueFrom } from "rxjs"; export class BsModsManagerService { @@ -102,7 +103,7 @@ export class BsModsManagerService { await ensureFolderExist(this.utilsService.getTempPath()); const dest = path.join(tempPath, fileName); - const zipPath = await this.requestService.downloadFile(zipUrl, dest); + const zipPath = (await lastValueFrom(this.requestService.downloadFile(zipUrl, dest))).data; const zip = new StreamZip.async({file : zipPath}); return {zip, zipPath}; diff --git a/src/main/services/request.service.ts b/src/main/services/request.service.ts index 00b4544e..b7a2cf90 100644 --- a/src/main/services/request.service.ts +++ b/src/main/services/request.service.ts @@ -1,5 +1,8 @@ import { RequestOptions, get } from "https"; import { createWriteStream, unlink } from "fs"; +import { Progression, unlinkPath } from "main/helpers/fs.helpers"; +import { Observable, buffer, shareReplay, tap } from "rxjs"; +import log from "electron-log"; export class RequestService { @@ -13,6 +16,7 @@ export class RequestService { private constructor(){} public get(options: string|RequestOptions): Promise{ + return new Promise((resolve, reject) => { let body = '' get(options, (res) => { @@ -25,21 +29,57 @@ export class RequestService { }); } - public downloadFile(url: string, dest: string): Promise{ - return new Promise((resolve, reject) => { + public downloadFile(url: string, dest: string): Observable>{ + + return new Observable>(subscriber => { + const progress: Progression = { current: 0, total: 0 }; const file = createWriteStream(dest); - get(url, res => { - res.pipe(file); - file.on("close", () => { - file.close(() => resolve(dest)) - }).on("error", err => { - unlink(dest, () => reject(err)); - }); - }).on("error", err => { - unlink(dest, () => reject(err)); + + file.on("close", () => { + progress["data"] = dest; + subscriber.next(progress); subscriber.complete(); }); - }); + file.on("error", err => unlink(dest, () => subscriber.error(err))); + + const req = get(url, res => { + + progress.total = parseInt(res.headers?.["content-length"] || "0", 10); + + res.on("data", chunk => { + progress.current += chunk.length; + subscriber.next(progress); + }); + + res.pipe(file); + + }); + + req.on("error", err => { subscriber.error(err); }); + + }).pipe(tap({error: e => log.error(e)}), shareReplay(1)); + + } + + public downloadBuffer(url: string): Observable{ + + return new Observable(subscriber => { + const allChunks: Buffer[] = []; + + const req = get(url, res => { + res.on("data", chunk => { + allChunks.push(chunk); + }); + res.on('end', () => { + subscriber.next(Buffer.concat(allChunks)); + subscriber.complete(); + }); + res.on('error', (err) => subscriber.error(err)) + }); + + req.on("error", err => { subscriber.error(err); }); + + }).pipe(tap({error: e => log.error(e)}), shareReplay(1)); } } \ No newline at end of file diff --git a/src/main/services/thrid-party/model-saber/model-saber.service.ts b/src/main/services/thrid-party/model-saber/model-saber.service.ts index 229eb02c..c95df84f 100644 --- a/src/main/services/thrid-party/model-saber/model-saber.service.ts +++ b/src/main/services/thrid-party/model-saber/model-saber.service.ts @@ -2,6 +2,7 @@ import { Observable } from "rxjs"; import { MSGetQuery, MSGetQueryFilterType, MSModel, MSModelPlatform } from "../../../../shared/models/models/model-saber.model"; import { ModelSaberApiService } from "./model-saber-api.service"; import log from "electron-log"; +import striptags from "striptags" export class ModelSaberService { @@ -71,6 +72,8 @@ export class ModelSaberService { const model = Array.from(Object.values(res.data)).at(0); + model.name = striptags(model.name ?? ""); + this.modelsHashCache.set(hash, model); return model @@ -87,7 +90,11 @@ export class ModelSaberService { (async () => { const res = await this.modelSaberApi.searchModel(query); if(res.status !== 200){ observer.error(res.status); } - observer.next(Object.values(res.data)); + observer.next(Object.values(res.data).map(model => { + if(!model || !model.name){ return model; } + (model as MSModel).name = striptags(model.name); + return model; + })); })().catch(e => observer.error(e)).then(() => observer.complete()); }); } diff --git a/src/renderer/components/modal/modal-types/models/download-models-modal.component.tsx b/src/renderer/components/modal/modal-types/models/download-models-modal.component.tsx index ac5a174f..8b1c759c 100644 --- a/src/renderer/components/modal/modal-types/models/download-models-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/models/download-models-modal.component.tsx @@ -5,9 +5,8 @@ import { ModalComponent } from "renderer/services/modale.service"; import { BSVersion } from "shared/bs-version.interface"; import BeatWaitingImg from "../../../../../../assets/images/apngs/beat-waiting.png"; import BeatConflictImg from "../../../../../../assets/images/apngs/beat-conflict.png"; -import { BsmLocalModel } from "shared/models/models/bsm-local-model.interface"; import { MSGetQuery, MSGetSort, MSGetSortDirection, MSModel, MSModelPlatform, MSModelType } from "shared/models/models/model-saber.model"; -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useState } from "react"; import { ModelItem } from "renderer/components/models-management/model-item.component"; import { useService } from "renderer/hooks/use-service.hook"; import { ModelDownload, ModelsDownloaderService } from "renderer/services/models-management/models-downloader.service"; @@ -21,11 +20,10 @@ import { MODEL_TYPES, MS_GET_QUERY_SORTS } from "shared/models/models/constants" import { useOnUpdate } from "renderer/hooks/use-on-update.hook"; import equal from "fast-deep-equal"; import { catchError, of } from "rxjs"; -import { BsmIcon } from "renderer/components/svgs/bsm-icon.component"; import Tippy from "@tippyjs/react"; -import { useThemeColor } from "renderer/hooks/use-theme-color.hook"; +import { BsmLocalModel } from "shared/models/models/bsm-local-model.interface"; -export const DownloadModelsModal: ModalComponent = ({resolver, data: { version, type }}) => { +export const DownloadModelsModal: ModalComponent = ({resolver, data: { version, type, owned }}) => { const modelsDownloader = useService(ModelsDownloaderService); const modelSaber = useService(ModelSaberService); @@ -42,34 +40,46 @@ export const DownloadModelsModal: ModalComponent([]); const isOnline = useObservable(os.isOnline$, true); const [error, error$] = useBehaviorSubject(false); const [isLoading, isLoading$] = useBehaviorSubject(false); - - console.log(msModels); + const [ownedModels, setOwnedModels] = useState(owned ?? []); const [currentType, currentType$] = useBehaviorSubject(type); const [currentSort, currentSort$] = useBehaviorSubject(MSGetSort.Date); const [searhInput, searhInput$] = useBehaviorSubject(""); const [getQuery, getQuery$] = useBehaviorSubject({ type: currentType, platform: MSModelPlatform.PC, start: 0, end: 25, sort: currentSort, sortDirection: MSGetSortDirection.Descending }); + useOnUpdate(() => { + const sub = modelsDownloader.onModelsDownloaded((model => { + if(!equal(model.version, version)) { return; } + setOwnedModels(prev => [...prev ?? [], model]); + })); + + return () => sub.unsubscribe(); + }); + useOnUpdate(() => { error$.next(false); + isLoading$.next(true); + const sub = modelSaber.searchModels(getQuery).pipe(catchError(() => { error$.next(true); return of([]); - })).subscribe(models => msModels$.next([...msModels, ...models])); + })).subscribe(models => { + msModels$.next([...msModels, ...models]); + isLoading$.next(false); + }); + return () => sub.unsubscribe(); }, [getQuery]) useOnUpdate(() => { msModels$.next([]); - - getQuery$.next({ ...getQuery$.value, start: 0, @@ -81,7 +91,6 @@ export const DownloadModelsModal: ModalComponent { - console.log("load more"); const currentQuery = getQuery; currentQuery.start += 25; currentQuery.end += 25; @@ -90,8 +99,6 @@ export const DownloadModelsModal: ModalComponent { - const filters = searhInput.split(" ").join(","); - const getQuery: MSGetQuery = { start: 0, end: 25, type: currentType, @@ -105,12 +112,24 @@ export const DownloadModelsModal: ModalComponent { - modelsDownloader.addModelToDownload({...model, version}); + modelsDownloader.addModelToDownload({model, version}); }, []); + const handleCancelDownload = useCallback((model: MSModel) => { + modelsDownloader.removeFromDownloadQueue({model, version}); + }, []); + + const modelPendingDownload = (model: MSModel) => { + return downloadQueue.some(download => download.model.id === model.id && equal(download.version, version)); + } + + const isModelOwned = (model: MSModel) => { + return !!ownedModels.some(owned => owned.hash === model.hash); + } + const filterTipsHTML = useConstant(() => (
- +
@@ -127,7 +146,7 @@ export const DownloadModelsModal: ModalComponent {e.preventDefault(); search()}}>
- currentType$.next(value)}/> + currentType$.next(value)}/>
searhInput$.next(e.target.value)}/> @@ -153,8 +172,9 @@ export const DownloadModelsModal: ModalComponent ))} diff --git a/src/renderer/components/models-management/model-item.component.tsx b/src/renderer/components/models-management/model-item.component.tsx index 50a6d7e1..c1932b5e 100644 --- a/src/renderer/components/models-management/model-item.component.tsx +++ b/src/renderer/components/models-management/model-item.component.tsx @@ -59,12 +59,10 @@ function modelItem(props: Props) { return [...new Set(props.tags)]; })(); - console.log(thumbnailUrl, "la"); - - const actionButtons = (): {text: string, icon: BsmIconType, action: () => void}[] => { - const buttons: {text: string, icon: BsmIconType, action: () => void}[] = []; + const actionButtons = (): {text: string, icon: BsmIconType, action: () => void, iconColor?: string}[] => { + const buttons: {text: string, icon: BsmIconType, action: () => void, iconColor?: string}[] = []; - if(props.onDownload){ + if(props.onDownload && !props.onCancelDownload){ buttons.push({ text: "Download", icon: "download", action: () => props.onDownload(props.callbackValue) }); // TODO TRANSLATE } @@ -73,7 +71,7 @@ function modelItem(props: Props) { } if(props.onCancelDownload){ - buttons.push({ text: "Cancel download", icon: "cross", action: () => props.onCancelDownload(props.callbackValue) }); // TODO TRANSLATE + buttons.push({ text: "Cancel download", icon: "cross", action: () => props.onCancelDownload(props.callbackValue), iconColor: "red" }); // TODO TRANSLATE } return buttons; @@ -91,18 +89,18 @@ function modelItem(props: Props) { // TODO TRANSLATE ALL TEXT setHovered(() => true)} onHoverEnd={() => setHovered(() => false)} onClick={props.onClick}> -
+
{!props.isDownloading ? ( actionButtons().map((button, index) => ( - {e.stopPropagation(); e.preventDefault(); button.action();}} withBar={false}/> + {e.stopPropagation(); e.preventDefault(); button.action();}} withBar={false}/> )) ): ( )}
- {e.stopPropagation(); e.preventDefault()}}> + {e.stopPropagation(); e.preventDefault()}}> {props.name} {props.author ?? ''}
    diff --git a/src/renderer/components/models-management/models-grid.component.tsx b/src/renderer/components/models-management/models-grid.component.tsx index caed6159..4bf2f920 100644 --- a/src/renderer/components/models-management/models-grid.component.tsx +++ b/src/renderer/components/models-management/models-grid.component.tsx @@ -18,6 +18,8 @@ import { BsmButton } from "../shared/bsm-button.component"; import equal from "fast-deep-equal"; import { VersionLinkerAction } from "renderer/services/version-folder-linker.service"; import { MODEL_TYPE_FOLDERS } from "shared/models/models/constants"; +import { useService } from "renderer/hooks/use-service.hook"; +import { ModelsDownloaderService } from "renderer/services/models-management/models-downloader.service"; type Props = { className?: string, @@ -29,13 +31,13 @@ type Props = { export const ModelsGrid = forwardRef(({className, version, type, search, active}: Props, forwardRef) => { - const modelsManager = useConstant(() => ModelsManagerService.getInstance()); + const modelsManager = useService(ModelsManagerService); + const modelsDownloader = useService(ModelsDownloaderService); const ref = useRef(); - const isVisible = useInView(ref, {once: true, amount: .1}); const [models, setModelsLoadObservable,, setModels] = useSwitchableObservable(); - const [progress$, setProgress$] = useState(new BehaviorSubject(0).asObservable()); + const progress$ = useConstant(() => new BehaviorSubject(0)); const [modelsSelected, modelsSelected$] = useBehaviorSubject([]); const isLoading = !models; @@ -63,29 +65,45 @@ export const ModelsGrid = forwardRef(({className, version, type, search, active} }), [modelsSelected, models]); useOnUpdate(() => { - if(!isVisible || !active){ return; } + if(!active){ return; } + if(models && models.length){ return; } loadModels(); + }, [active]); + + useOnUpdate(() => { + if(!active){ + setModels(() => null); + } + else { + loadModels(); + } const onLinkStateChangeCb = (action: VersionLinkerAction) => { if(!equal(version, action.version) || !action.relativeFolder.includes(MODEL_TYPE_FOLDERS[type])){ return; } loadModels(); } + const sub = modelsDownloader.onModelsDownloaded(localModel => { + if(localModel.type !== type || !equal(localModel.version, version)){ return; } + setModels(models => [localModel, ...models ?? []]); + }); + modelsManager.onModelsFolderLinked(onLinkStateChangeCb); modelsManager.onModelsFolderUnlinked(onLinkStateChangeCb); return () => { modelsManager.removeModelsFolderLinkedListener(onLinkStateChangeCb); modelsManager.removeModelsFolderUnlinkedListener(onLinkStateChangeCb); + sub.unsubscribe(); } - }, [version, isVisible, type]); + }, [version, type]); const loadModels = () => { const modelsObs$ = modelsManager.$getModels(type, version); setModels(() => null); - setModelsLoadObservable(() => modelsObs$.pipe(map(models => models?.extra), distinctUntilChanged())); - setProgress$(modelsObs$.pipe(map(models => Math.floor((models.current / models.total) * 100)), startWith(0), distinctUntilChanged())) + setModelsLoadObservable(() => modelsObs$.pipe(map(models => models?.data), distinctUntilChanged())); + modelsObs$.pipe(map(models => Math.floor((models.current / models.total) * 100)), startWith(0), distinctUntilChanged()).subscribe({next: v => progress$.next(v)}); } const handleModelClick = (model: BsmLocalModel) => { @@ -159,7 +177,7 @@ export const ModelsGrid = forwardRef(({className, version, type, search, active} {filtredModels().map(localModel => ( ) -}) +}); diff --git a/src/renderer/components/models-management/models-panel.component.tsx b/src/renderer/components/models-management/models-panel.component.tsx index 67cbad2c..2af36d7a 100644 --- a/src/renderer/components/models-management/models-panel.component.tsx +++ b/src/renderer/components/models-management/models-panel.component.tsx @@ -1,42 +1,57 @@ import { BSVersion } from "shared/bs-version.interface"; -import { useRef, useState, MutableRefObject } from "react"; +import { useRef, useState } from "react"; import { ModelsTabsNavbar } from "./models-tabs-navbar.component"; import { ModelsGrid } from "./models-grid.component"; import { MSModelType } from "shared/models/models/model-saber.model"; import { BsmDropdownButton, DropDownItem } from "../shared/bsm-dropdown-button.component"; -import { useConstant } from "renderer/hooks/use-constant.hook"; import { ModelsManagerService } from "renderer/services/models-management/models-manager.service"; import { BsmLocalModel } from "shared/models/models/bsm-local-model.interface"; import { BsmButton } from "../shared/bsm-button.component"; import { useService } from "renderer/hooks/use-service.hook"; import { ModelsDownloaderService } from "renderer/services/models-management/models-downloader.service"; +import { useOnUpdate } from "renderer/hooks/use-on-update.hook"; +import { NotificationService } from "renderer/services/notification.service"; +import { ConfigurationService } from "renderer/services/configuration.service"; -export function ModelsPanel({version}: {version?: BSVersion}) { +export function ModelsPanel({version, isActive, goToMods}: {version?: BSVersion, isActive: boolean, goToMods: () => void}) { const modelsManager = useService(ModelsManagerService); - const modelDownloader = useService(ModelsDownloaderService); + const modelDownloader = useService(ModelsDownloaderService); + const notification = useService(NotificationService); + const config = useService(ConfigurationService); - const [avatarsRef, sabersRef, platformsRef, bloqsRef] = [useRef(null), useRef(null), useRef(null), useRef(null)]; + const ref = useRef(); + + const modelsGridRefs = [useRef(null), useRef(null), useRef(null), useRef(null)]; const [modelTypeTab, setModelTypeTab] = useState(MSModelType.Avatar); const [currentTabIndex, setCurrentTabIndex] = useState(0); const [search, setSearch] = useState(""); - - const getActiveTabRef = () => [avatarsRef, sabersRef, platformsRef, bloqsRef][currentTabIndex]; + + useOnUpdate(() => { + if(!isActive){ return; } + //if(config.get("prevented-for-mods-models")){ return; } + config.set("prevented-for-mods-models", true); + notification.notifyWarning({ title: "Mods needed", desc: "Be sure to have the mods installed to use models in BeatSaber", actions: [{id: "0", title: "Go to mods"}], duration: 9_000 }).then(res => { + if(res !== "0"){ return; } + goToMods(); + }); + }, [isActive]); const exportModels = () => { - const selectedModels = [avatarsRef, sabersRef, platformsRef, bloqsRef].map(ref =>(ref.current?.getSelectedModels() as BsmLocalModel[])).flat() + const selectedModels = modelsGridRefs.map(ref =>(ref.current?.getSelectedModels() as BsmLocalModel[])).flat() modelsManager.exportModels(selectedModels, version); } const deleteModels = () => { - const activeTab = getActiveTabRef(); + const activeTab = modelsGridRefs[currentTabIndex]; activeTab.current?.deleteSelectedModels(); } const openDownloadModal = () => { - modelDownloader.openDownloadModelsModal(version, modelTypeTab); + const allLoadedModels = modelsGridRefs.map(ref =>(ref.current?.getModels() as BsmLocalModel[])).flat(); + modelDownloader.openDownloadModelsModal(version, modelTypeTab, allLoadedModels); } const threeDotsItems: DropDownItem[] = [ @@ -45,7 +60,7 @@ export function ModelsPanel({version}: {version?: BSVersion}) { ]; return ( -
    +
    {e.preventDefault(); openDownloadModal()}}/>
    @@ -57,10 +72,10 @@ export function ModelsPanel({version}: {version?: BSVersion}) { {setModelTypeTab(() => tab.extra), setCurrentTabIndex(() => index)}}/>
    - - - - + + + +
    diff --git a/src/renderer/components/shared/bsm-select.component.tsx b/src/renderer/components/shared/bsm-select.component.tsx index 0affe993..573dc785 100644 --- a/src/renderer/components/shared/bsm-select.component.tsx +++ b/src/renderer/components/shared/bsm-select.component.tsx @@ -1,23 +1,25 @@ +import equal from "fast-deep-equal"; import { ComponentProps } from "react" import { useTranslation } from "renderer/hooks/use-translation.hook" type Props = Omit, "onChange"> & { options?: BsmSelectOption[], + selected?: T, onChange?: (value: T) => void, } -export function BsmSelect({className, style, options, onChange}: Props) { +export function BsmSelect(props: Props) { const t = useTranslation(); const handleChange: ComponentProps<"select">["onChange"] = (e) => { e.preventDefault(); - onChange?.(options?.[parseInt(e.target.value)]?.value); + props.onChange?.(props.options?.[parseInt(e.target.value)]?.value); } return ( - equal(opt.value, props.selected))}> + {props.options && props.options.map((option, index) => ( ))} diff --git a/src/renderer/pages/version-viewer.component.tsx b/src/renderer/pages/version-viewer.component.tsx index b2126f7b..05aa4673 100644 --- a/src/renderer/pages/version-viewer.component.tsx +++ b/src/renderer/pages/version-viewer.component.tsx @@ -83,7 +83,7 @@ export function VersionViewer() {
    - + setCurrentTabIndex(() => 3)}/>
    diff --git a/src/renderer/services/model-downloader.service.ts b/src/renderer/services/model-downloader.service.ts index 7381b913..b12bf3c9 100644 --- a/src/renderer/services/model-downloader.service.ts +++ b/src/renderer/services/model-downloader.service.ts @@ -21,7 +21,7 @@ export class ModelDownloaderService { } - public async oneClickInstallModel(model: MSModel): Promise{ + public async oneClickInstallModel(model: MSModel): Promise{ // TODO : Put this function in the new ModelDownloaderService this.progress.showFake(0.04); diff --git a/src/renderer/services/models-management/models-downloader.service.ts b/src/renderer/services/models-management/models-downloader.service.ts index 49d4deab..e90e459b 100644 --- a/src/renderer/services/models-management/models-downloader.service.ts +++ b/src/renderer/services/models-management/models-downloader.service.ts @@ -1,10 +1,13 @@ -import { BehaviorSubject, Observable, Subscription, distinctUntilChanged, filter, map, shareReplay } from "rxjs"; +import { BehaviorSubject, Observable, Subscription, distinctUntilChanged, filter, lastValueFrom, map, shareReplay, startWith } from "rxjs"; import { BSVersion } from "shared/bs-version.interface"; import { BsmLocalModel } from "shared/models/models/bsm-local-model.interface"; import { MSModel, MSModelType } from "shared/models/models/model-saber.model"; import { ModalResponse, ModalService } from "../modale.service"; import { IpcService } from "../ipc.service"; import { DownloadModelsModal } from "renderer/components/modal/modal-types/models/download-models-modal.component"; +import { ProgressBarService } from "../progress-bar.service"; +import { Progression } from "main/helpers/fs.helpers"; +import { ProgressionInterface } from "shared/models/progress-bar"; export class ModelsDownloaderService { @@ -16,9 +19,10 @@ export class ModelsDownloaderService { } private readonly ipc: IpcService; - private readonly modal: ModalService + private readonly modal: ModalService; + private readonly progress: ProgressBarService; - private readonly lastDownloadedModel$ = new BehaviorSubject(null); + private readonly lastDownload$ = new BehaviorSubject(null); private readonly queue$ = new BehaviorSubject([]); private readonly _currentDownload$ = this.queue$.pipe(map(queue => queue.at(0)), distinctUntilChanged(), shareReplay(1)); @@ -26,39 +30,60 @@ export class ModelsDownloaderService { this.ipc = IpcService.getInstance(); this.modal = ModalService.getInsance(); + this.progress = ProgressBarService.getInstance(); this._currentDownload$.pipe(filter(v => !!v)).subscribe(model => this.downloadModel(model)); + this._currentDownload$.pipe(filter(v => !v)).subscribe(() => this.lastDownload$.next(null)); } - private downloadModel(model: ModelDownload){ - // TODO: Download model + private async downloadModel(download: ModelDownload){ + + const download$ = this.ipc.sendV2>("download-model", {args: download}); + + if(!this.progress.isVisible){ + const progress$: Observable = download$.pipe(map(progress => { + return { + progression: progress.total ? (progress.current / progress.total) * 100 : 0, + label: download.model.name + } + }), startWith({progression: .1, label: download.model.name})); + this.progress.show(progress$, true); + } + + const downloaded = await lastValueFrom(download$); + this.lastDownload$.next(downloaded.data); + + this.progress.hide(true); + + this.queue$.next(this.queue$.value.filter(m => m.model.hash !== download.model.hash)); + } public addModelToDownload(model: ModelDownload): ModelDownload{ const queue = this.queue$.value; queue.push(model); - this.queue$.next(queue); + this.queue$.next([...queue]); return model; } - public removeFromDownloadQueue(model: ModelDownload){ + public removeFromDownloadQueue(download: ModelDownload){ const queue = this.queue$.value; - const index = queue.findIndex(m => m.hash === model.hash); + const index = queue.findIndex(m => m.model.hash === download.model.hash); if(index === -1){ return; } queue.splice(index, 1); - this.queue$.next(queue); + this.queue$.next([...queue]); } - public onModelsDownloaded$(cb: (model: ModelDownload) => void): Subscription{ - return this.lastDownloadedModel$.pipe(filter(v => !!v)).subscribe(cb); + public onModelsDownloaded(cb: (model: BsmLocalModel) => void): Subscription{ + return this.lastDownload$.pipe(filter(download => !!download)).subscribe(cb); } - public isDownloading$(model: ModelDownload): Observable{ + public isDownloading$(download: ModelDownload): Observable{ return this._currentDownload$.pipe(map(current => { if(!current){ return false; } - return current.hash === model.hash && current.version === model.version + return current.model.hash === download.model.hash && current.version === download.version }), distinctUntilChanged()); } @@ -66,15 +91,21 @@ export class ModelsDownloaderService { return this._currentDownload$; } - public isPending$(model: ModelDownload): Observable{ - return this.queue$.pipe(map(queue => queue.at(0).hash !== model.hash && queue.at(0).version !== model.version && queue.some(m => m.hash === model.hash && m.version === model.version)), distinctUntilChanged()); + public isPending$(download: ModelDownload): Observable{ + return this.queue$.pipe(map(queue => queue.at(0).model.hash !== download.model.hash && queue.at(0).version !== download.version && queue.some(d => d.model.hash === download.model.hash && d.version === download.version)), distinctUntilChanged()); } - public openDownloadModelsModal(version: BSVersion, type?: MSModelType): Promise>{ - return this.modal.openModal(DownloadModelsModal, {version, type}); // TODO MODELS + public getQueue$(): Observable{ + return this.queue$.asObservable(); + } + + public openDownloadModelsModal(version: BSVersion, type?: MSModelType, owned?: BsmLocalModel[]): Promise>{ + return this.modal.openModal(DownloadModelsModal, {version, type, owned}); // TODO MODELS } } -export type ModelDownload = Partial & Pick & {version: BSVersion}; -export type ModelDownloaded = BsmLocalModel & {version: BSVersion}; \ No newline at end of file +export type ModelDownload = { + model: MSModel, + version: BSVersion +} \ No newline at end of file diff --git a/src/renderer/services/models-management/models-manager.service.ts b/src/renderer/services/models-management/models-manager.service.ts index 20776aea..57dc202b 100644 --- a/src/renderer/services/models-management/models-manager.service.ts +++ b/src/renderer/services/models-management/models-manager.service.ts @@ -110,9 +110,9 @@ export class ModelsManagerService { if(!resFile.success){ return; } - const exportProgress$: Observable = this.ipc.sendV2("export-models", {args: {version, models, outPath: resFile.data}}).pipe( + const exportProgress$: Observable = this.ipc.sendV2("export-models", {args: {version, models, outPath: resFile.data}}).pipe( map(p => { - return { progression: (p.prossesedFiles / p.totalFiles) * 100, label: `${p.prossesedFiles} / ${p.totalFiles}` } as ProgressionInterface + return { progression: (p.current / p.total) * 100, label: `${p.current} / ${p.total}` } as ProgressionInterface }) ); diff --git a/src/shared/models/models/bsm-local-model.interface.ts b/src/shared/models/models/bsm-local-model.interface.ts index 0cc9d15f..ca1e5bd2 100644 --- a/src/shared/models/models/bsm-local-model.interface.ts +++ b/src/shared/models/models/bsm-local-model.interface.ts @@ -1,3 +1,4 @@ +import { BSVersion } from "shared/bs-version.interface"; import { MSModel, MSModelType } from "./model-saber.model"; export interface BsmLocalModel{ @@ -6,4 +7,5 @@ export interface BsmLocalModel{ readonly hash: string; readonly type: MSModelType; readonly model?: MSModel + readonly version?: BSVersion; } \ No newline at end of file
TagDescription
author:Only show models by the specified author.