[feature-136] models management almost finished, still need to add translations

This commit is contained in:
MathieuG-P
2023-05-19 19:06:59 +02:00
parent 76bdde0fe8
commit bdc893d8a5
21 changed files with 308 additions and 128 deletions
@@ -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
})
);