mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
[feature-136] models management almost finished, still need to add translations
This commit is contained in:
Generated
+17
-6
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -130,6 +130,6 @@ export async function copyDirectoryWithJunctions(src: string, dest: string, opti
|
||||
export interface Progression<T = unknown>{
|
||||
total: number;
|
||||
current: number;
|
||||
extra?: T;
|
||||
data?: T;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<voi
|
||||
|
||||
});
|
||||
|
||||
ipc.on<ModelDownload>("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);
|
||||
|
||||
@@ -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<ArchiveProgress>{
|
||||
public finalize(): Observable<Progression>{
|
||||
|
||||
const progress: ArchiveProgress = {
|
||||
totalFiles: 0,
|
||||
prossesedFiles: 0
|
||||
const progress: Progression = {
|
||||
total: 0,
|
||||
current: 0
|
||||
};
|
||||
|
||||
return new Observable<ArchiveProgress>(observer => {
|
||||
return new Observable<Progression>(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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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<string>{
|
||||
|
||||
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<Progression<BsmLocalModel>>{
|
||||
return new Observable<Progression<BsmLocalModel>>(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<void>{
|
||||
@@ -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<BsmLocalModel[]> = {
|
||||
total: 0,
|
||||
current: 0,
|
||||
extra: null
|
||||
data: null
|
||||
};
|
||||
|
||||
return new Observable<Progression<BsmLocalModel[]>>(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<ArchiveProgress>{
|
||||
// TOTO NOT ASYNC
|
||||
|
||||
public exportModels(output: string, version?: BSVersion, models?: BsmLocalModel[]): Observable<Progression>{
|
||||
|
||||
return new Observable<ArchiveProgress>(subscriber => {
|
||||
return new Observable<Progression>(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){
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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<T = any>(options: string|RequestOptions): Promise<T>{
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let body = ''
|
||||
get(options, (res) => {
|
||||
@@ -25,21 +29,57 @@ export class RequestService {
|
||||
});
|
||||
}
|
||||
|
||||
public downloadFile(url: string, dest: string): Promise<string>{
|
||||
return new Promise((resolve, reject) => {
|
||||
public downloadFile(url: string, dest: string): Observable<Progression<string>>{
|
||||
|
||||
return new Observable<Progression<string>>(subscriber => {
|
||||
const progress: Progression<string> = { 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<Buffer>{
|
||||
|
||||
return new Observable<Buffer>(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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
});
|
||||
}
|
||||
|
||||
+39
-19
@@ -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<void, {version: BSVersion, type: MSModelType}> = ({resolver, data: { version, type }}) => {
|
||||
export const DownloadModelsModal: ModalComponent<void, {version: BSVersion, type: MSModelType, owned: BsmLocalModel[]}> = ({resolver, data: { version, type, owned }}) => {
|
||||
|
||||
const modelsDownloader = useService(ModelsDownloaderService);
|
||||
const modelSaber = useService(ModelSaberService);
|
||||
@@ -42,34 +40,46 @@ export const DownloadModelsModal: ModalComponent<void, {version: BSVersion, type
|
||||
})));
|
||||
|
||||
const t = useTranslation();
|
||||
const color = useThemeColor("first-color");
|
||||
const currentDownload = useObservable(modelsDownloader.currentDownload$(), null);
|
||||
const downloadQueue = useObservable(modelsDownloader.getQueue$(), []);
|
||||
const [msModels, msModels$] = useBehaviorSubject<MSModel[]>([]);
|
||||
const isOnline = useObservable(os.isOnline$, true);
|
||||
const [error, error$] = useBehaviorSubject(false);
|
||||
const [isLoading, isLoading$] = useBehaviorSubject(false);
|
||||
|
||||
console.log(msModels);
|
||||
const [ownedModels, setOwnedModels] = useState<BsmLocalModel[]>(owned ?? []);
|
||||
|
||||
const [currentType, currentType$] = useBehaviorSubject<MSModelType>(type);
|
||||
const [currentSort, currentSort$] = useBehaviorSubject<MSGetSort>(MSGetSort.Date);
|
||||
const [searhInput, searhInput$] = useBehaviorSubject("");
|
||||
const [getQuery, getQuery$] = useBehaviorSubject<MSGetQuery>({ 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<void, {version: BSVersion, type
|
||||
}, [currentType, currentSort]);
|
||||
|
||||
const loadMore = () => {
|
||||
console.log("load more");
|
||||
const currentQuery = getQuery;
|
||||
currentQuery.start += 25;
|
||||
currentQuery.end += 25;
|
||||
@@ -90,8 +99,6 @@ export const DownloadModelsModal: ModalComponent<void, {version: BSVersion, type
|
||||
|
||||
const search = () => {
|
||||
|
||||
const filters = searhInput.split(" ").join(",");
|
||||
|
||||
const getQuery: MSGetQuery = {
|
||||
start: 0, end: 25,
|
||||
type: currentType,
|
||||
@@ -105,12 +112,24 @@ export const DownloadModelsModal: ModalComponent<void, {version: BSVersion, type
|
||||
}
|
||||
|
||||
const handleDownloadModel = useCallback((model: MSModel) => {
|
||||
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(() => (
|
||||
<div className="w-fit flex">
|
||||
<table className="w-fit whitespace-nowrap grow shrink-0 text-base">
|
||||
<table className="w-fit whitespace-nowrap grow shrink-0 text-sm">
|
||||
<tbody>
|
||||
<tr className="font-bold"><td className="mr-3 block">Tag</td><td>Description</td></tr>
|
||||
<tr><td className="mr-3 block">author:</td><td>Only show models by the specified author.</td></tr>
|
||||
@@ -127,7 +146,7 @@ export const DownloadModelsModal: ModalComponent<void, {version: BSVersion, type
|
||||
return (
|
||||
<form className="text-gray-800 dark:text-gray-200 flex flex-col max-w-[95vw] w-[970px] h-[85vh] gap-3" onSubmit={e => {e.preventDefault(); search()}}>
|
||||
<div className="flex h-9 gap-2 shrink-0">
|
||||
<BsmSelect className="bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-1 pb-0.5 text-center" options={modelTypesOptions} onChange={(value) => currentType$.next(value)}/>
|
||||
<BsmSelect className="bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-1 pb-0.5 text-center" options={modelTypesOptions} selected={currentType} onChange={(value) => currentType$.next(value)}/>
|
||||
<div className="h-ful grow relative flex justify-center items-center">
|
||||
<input className="h-full w-full bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-2 pb-0.5" type="text" name="" id="" placeholder="TODO TRANSLATE Rechercher un modèle" value={searhInput} onChange={e => searhInput$.next(e.target.value)}/>
|
||||
<Tippy placement="bottom" content={filterTipsHTML} allowHTML={true} maxWidth={Infinity}>
|
||||
@@ -153,8 +172,9 @@ export const DownloadModelsModal: ModalComponent<void, {version: BSVersion, type
|
||||
key={model.id}
|
||||
{...model}
|
||||
callbackValue={model}
|
||||
isDownloading={equal(currentDownload, {...model, version} as ModelDownload)}
|
||||
onDownload={handleDownloadModel}
|
||||
isDownloading={equal(currentDownload, {model, version} as ModelDownload)}
|
||||
onDownload={!isModelOwned(model) ? handleDownloadModel: undefined}
|
||||
onCancelDownload={modelPendingDownload(model) ? handleCancelDownload : undefined}
|
||||
/>
|
||||
))}
|
||||
<motion.span onViewportEnter={loadMore} className="block w-full h-8"/>
|
||||
|
||||
@@ -59,12 +59,10 @@ function modelItem<T = unknown>(props: Props<T>) {
|
||||
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<T = unknown>(props: Props<T>) {
|
||||
}
|
||||
|
||||
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<T = unknown>(props: Props<T>) {
|
||||
// TODO TRANSLATE ALL TEXT
|
||||
<motion.li className={`relative flex-grow min-w-[14rem] h-56 cursor-pointer ${props.className ?? ""}`} onHoverStart={() => setHovered(() => true)} onHoverEnd={() => setHovered(() => false)} onClick={props.onClick}>
|
||||
<GlowEffect visible={props.selected || hovered}/>
|
||||
<div className="absolute top-0 left-0 w-full h-full rounded-lg overflow-hidden blur-none bg-black">
|
||||
<div className="absolute top-0 left-0 w-full h-full rounded-lg overflow-hidden blur-none bg-black shadow-sm shadow-black">
|
||||
<BsmImage className={`absolute top-0 left-0 w-full h-full object-cover ${props.type === MSModelType.Avatar ? "object-top" : ""}`} image={thumbnailUrl} placeholder={defaultImage} loading="lazy"/>
|
||||
<div className="absolute top-0 right-0 h-full w-0 flex flex-col items-end gap-1 pt-1.5 pr-1.5">
|
||||
{!props.isDownloading ? (
|
||||
actionButtons().map((button, index) => (
|
||||
<BsmButton key={index} className="w-7 h-7 p-1 rounded-md transition-transform duration-150 shadow-black shadow-sm" style={{transitionDelay: `${index * 50}ms`, transform: hovered ? "translate(0%)" : "translate(150%)"}} icon={button.icon} onClick={e => {e.stopPropagation(); e.preventDefault(); button.action();}} withBar={false}/>
|
||||
<BsmButton key={index} className="w-7 h-7 p-1 rounded-md transition-transform duration-150 shadow-black shadow-sm" style={{transitionDelay: `${index * 50}ms`, transform: hovered ? "translate(0%)" : "translate(150%)"}} icon={button.icon} iconColor={button.iconColor} onClick={e => {e.stopPropagation(); e.preventDefault(); button.action();}} withBar={false}/>
|
||||
))
|
||||
): (
|
||||
<BsmBasicSpinner className="w-7 h-7 p-1 rounded-md bg-main-color-2 flex items-center justify-center shadow-black shadow-sm" spinnerClassName="brightness-200" thikness="3.5px" style={{color}}/>
|
||||
)}
|
||||
</div>
|
||||
<motion.div className="absolute cursor-default top-[80%] left-0 w-full h-full p-2 flex flex-col gap-1.5 bg-main-color-3 bg-opacity-60 backdrop-blur-md transition-all delay-150 hover:top-0" onClick={e =>{e.stopPropagation(); e.preventDefault()}}>
|
||||
<motion.div className="absolute cursor-default top-[80%] left-0 w-full h-full px-2 pt-2.5 flex flex-col gap-1.5 bg-main-color-3 bg-opacity-60 backdrop-blur-md transition-all delay-150 hover:top-0" onClick={e =>{e.stopPropagation(); e.preventDefault()}}>
|
||||
<BsmLink className={`block w-fit max-w-full overflow-hidden font-bold whitespace-nowrap text-ellipsis ${props.id ? "cursor-pointer hover:underline" : ""}`} href={modelPageUrl}>{props.name}</BsmLink>
|
||||
<BsmLink className={`block w-fit max-w-full overflow-hidden whitespace-nowrap text-ellipsis brightness-200 ${authorPageUrl ? "cursor-pointer hover:underline" : ""}`} style={{color}} href={authorPageUrl}>{props.author ?? ''}</BsmLink>
|
||||
<ul className="flex flex-row flex-wrap gap-1">
|
||||
|
||||
@@ -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<BsmLocalModel[]>();
|
||||
const [progress$, setProgress$] = useState(new BehaviorSubject(0).asObservable());
|
||||
const progress$ = useConstant(() => new BehaviorSubject(0));
|
||||
const [modelsSelected, modelsSelected$] = useBehaviorSubject<BsmLocalModel[]>([]);
|
||||
|
||||
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 => (
|
||||
<ModelItem
|
||||
{...localModel?.model}
|
||||
key={localModel.model?.hash ?? localModel.hash}
|
||||
key={localModel.path}
|
||||
hash={localModel.model?.hash ?? localModel.hash}
|
||||
path={localModel.path}
|
||||
type={localModel.type}
|
||||
@@ -178,4 +196,4 @@ export const ModelsGrid = forwardRef(({className, version, type, search, active}
|
||||
{renderContent()}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
@@ -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>(MSModelType.Avatar);
|
||||
const [currentTabIndex, setCurrentTabIndex] = useState<number>(0);
|
||||
|
||||
const [search, setSearch] = useState<string>("");
|
||||
|
||||
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 (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center">
|
||||
<div ref={ref} className="w-full h-full flex flex-col items-center justify-center">
|
||||
<div className="w-full shrink-0 flex h-9 justify-center px-40 gap-2 mb-3 text-main-color-1 dark:text-white">
|
||||
<BsmButton className="flex items-center justify-center w-fit rounded-full px-2 py-1 font-bold" icon="add" text="misc.add" typeColor="primary" withBar={false} onClick={e => {e.preventDefault(); openDownloadModal()}}/>
|
||||
<div className="h-full rounded-full bg-light-main-color-2 dark:bg-main-color-2 grow p-[6px]">
|
||||
@@ -57,10 +72,10 @@ export function ModelsPanel({version}: {version?: BSVersion}) {
|
||||
<ModelsTabsNavbar className="flex-shrink-0" version={version} tabIndex={currentTabIndex} onTabChange={(index, tab) => {setModelTypeTab(() => tab.extra), setCurrentTabIndex(() => index)}}/>
|
||||
|
||||
<div className="flex-grow h-full flex flex-col transition-all duration-300" style={{translate: `0 ${0 - currentTabIndex * 100}%`}}>
|
||||
<ModelsGrid ref={avatarsRef} version={version} type={MSModelType.Avatar} active={modelTypeTab === MSModelType.Avatar} search={search}/>
|
||||
<ModelsGrid ref={sabersRef} version={version} type={MSModelType.Saber} active={modelTypeTab === MSModelType.Saber} search={search}/>
|
||||
<ModelsGrid ref={platformsRef} version={version} type={MSModelType.Platfrom} active={modelTypeTab === MSModelType.Platfrom} search={search}/>
|
||||
<ModelsGrid ref={bloqsRef} version={version} type={MSModelType.Bloq} active={modelTypeTab === MSModelType.Bloq} search={search}/>
|
||||
<ModelsGrid ref={modelsGridRefs[0]} version={version} type={MSModelType.Avatar} active={isActive && modelTypeTab === MSModelType.Avatar} search={search}/>
|
||||
<ModelsGrid ref={modelsGridRefs[1]} version={version} type={MSModelType.Saber} active={isActive && modelTypeTab === MSModelType.Saber} search={search}/>
|
||||
<ModelsGrid ref={modelsGridRefs[2]} version={version} type={MSModelType.Platfrom} active={isActive && modelTypeTab === MSModelType.Platfrom} search={search}/>
|
||||
<ModelsGrid ref={modelsGridRefs[3]} version={version} type={MSModelType.Bloq} active={isActive && modelTypeTab === MSModelType.Bloq} search={search}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
import equal from "fast-deep-equal";
|
||||
import { ComponentProps } from "react"
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook"
|
||||
|
||||
type Props<T> = Omit<ComponentProps<"select">, "onChange"> & {
|
||||
options?: BsmSelectOption<T>[],
|
||||
selected?: T,
|
||||
onChange?: (value: T) => void,
|
||||
}
|
||||
|
||||
export function BsmSelect<T = unknown>({className, style, options, onChange}: Props<T>) {
|
||||
export function BsmSelect<T = unknown>(props: Props<T>) {
|
||||
|
||||
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 (
|
||||
<select className={className} style={style} onChange={handleChange}>
|
||||
{options && options.map((option, index) => (
|
||||
<select {...props} onChange={handleChange} defaultValue={props.options?.findIndex(opt => equal(opt.value, props.selected))}>
|
||||
{props.options && props.options.map((option, index) => (
|
||||
<option key={index} value={index}>{t(option.text)}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
@@ -83,7 +83,7 @@ export function VersionViewer() {
|
||||
<MapsPlaylistsPanel version={state}/>
|
||||
</div>
|
||||
<div className="w-full shrink-0 px-3 pb-3 flex flex-col items-center">
|
||||
<ModelsPanel version={state}/>
|
||||
<ModelsPanel version={state} isActive={currentTabIndex === 2} goToMods={() => setCurrentTabIndex(() => 3)}/>
|
||||
</div>
|
||||
<ModsSlide version={state} onDisclamerDecline={handleModsDisclaimerDecline}/>
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,7 @@ export class ModelDownloaderService {
|
||||
}
|
||||
|
||||
|
||||
public async oneClickInstallModel(model: MSModel): Promise<boolean>{
|
||||
public async oneClickInstallModel(model: MSModel): Promise<boolean>{ // TODO : Put this function in the new ModelDownloaderService
|
||||
|
||||
this.progress.showFake(0.04);
|
||||
|
||||
|
||||
@@ -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<ModelDownload>(null);
|
||||
private readonly lastDownload$ = new BehaviorSubject<BsmLocalModel>(null);
|
||||
private readonly queue$ = new BehaviorSubject<ModelDownload[]>([]);
|
||||
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<Progression<BsmLocalModel>>("download-model", {args: download});
|
||||
|
||||
if(!this.progress.isVisible){
|
||||
const progress$: Observable<ProgressionInterface> = 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<boolean>{
|
||||
public isDownloading$(download: ModelDownload): Observable<boolean>{
|
||||
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<boolean>{
|
||||
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<boolean>{
|
||||
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<ModalResponse<void>>{
|
||||
return this.modal.openModal(DownloadModelsModal, {version, type}); // TODO MODELS
|
||||
public getQueue$(): Observable<ModelDownload[]>{
|
||||
return this.queue$.asObservable();
|
||||
}
|
||||
|
||||
public openDownloadModelsModal(version: BSVersion, type?: MSModelType, owned?: BsmLocalModel[]): Promise<ModalResponse<void>>{
|
||||
return this.modal.openModal(DownloadModelsModal, {version, type, owned}); // TODO MODELS
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export type ModelDownload = Partial<MSModel> & Pick<MSModel, "hash"> & {version: BSVersion};
|
||||
export type ModelDownloaded = BsmLocalModel & {version: BSVersion};
|
||||
export type ModelDownload = {
|
||||
model: MSModel,
|
||||
version: BSVersion
|
||||
}
|
||||
@@ -110,9 +110,9 @@ export class ModelsManagerService {
|
||||
|
||||
if(!resFile.success){ return; }
|
||||
|
||||
const exportProgress$: Observable<ProgressionInterface> = this.ipc.sendV2<ArchiveProgress, {version: BSVersion, models: BsmLocalModel[], outPath: string}>("export-models", {args: {version, models, outPath: resFile.data}}).pipe(
|
||||
const exportProgress$: Observable<ProgressionInterface> = this.ipc.sendV2<Progression, {version: BSVersion, models: BsmLocalModel[], outPath: string}>("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
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user