import { BSVersion } from "shared/bs-version.interface"; import { forwardRef, useCallback, useImperativeHandle, useState } from "react"; import { MSModelType } from "shared/models/models/model-saber.model"; import { useOnUpdate } from "renderer/hooks/use-on-update.hook"; import { useConstant } from "renderer/hooks/use-constant.hook"; import { ModelsManagerService } from "renderer/services/models-management/models-manager.service"; import { useSwitchableObservable } from "renderer/hooks/use-switchable-observable.hook"; import { BsmLocalModel } from "shared/models/models/bsm-local-model.interface"; import { ModelItem } from "./model-item.component"; import { BsmImage } from "../shared/bsm-image.component"; import BeatConflict from "../../../../assets/images/apngs/beat-conflict.png"; import { BehaviorSubject, distinctUntilChanged, map, startWith } from "rxjs"; 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"; import { useTranslation } from "renderer/hooks/use-translation.hook"; import { BsContentLoader } from "../shared/bs-content-loader.component"; import { VirtualScroll } from "../shared/virtual-scroll/virtual-scroll.component"; import { noop } from "shared/helpers/function.helpers"; type Props = { className?: string; version?: BSVersion; type: MSModelType; search?: string; active: boolean; downloadModels?: () => void; }; export const ModelsGrid = forwardRef(({ className, version, type, search, active, downloadModels }, forwardRef) => { const modelsManager = useService(ModelsManagerService); const modelsDownloader = useService(ModelsDownloaderService); const t = useTranslation(); const [models, setModelsLoadObservable, , setModels] = useSwitchableObservable(); const [renderableModels, setRenderableModels] = useState([]); const progress$ = useConstant(() => new BehaviorSubject(0)); const [modelsSelected, setModelsSelected] = useState([]); const isLoading = !models; const hasModels = !!models?.length; useImperativeHandle( forwardRef, () => ({ getModels: () => { return models ?? []; }, getSelectedModels: () => { return modelsSelected; }, reloadModels: () => { loadModels(); }, deleteSelectedModels: () => { modelsManager.deleteModels(!modelsSelected?.length ? models : modelsSelected, version).then(deletedModels => { if (!deletedModels?.length) { return; } const newModels = models.filter(m => !deletedModels.some(d => d.hash === m.hash)); setModels(() => newModels); setModelsSelected(() => []); }); }, }), [modelsSelected, models] ); useOnUpdate(() => setModelsSelected([]), [version]); useOnUpdate(() => setRenderableModels(() => ( models?.map(model => ({ model, selected: modelsSelected.some(m => m.hash === model.hash) })) ?? []) ), [modelsSelected, models]); useOnUpdate(() => { if (!active) { return; } if (models?.length) { return; } loadModels(); }, [active]); useOnUpdate(() => { if (!active) { setModels(() => null); } else { loadModels(); } }, [version, type]); useOnUpdate(() => { if (!active && !models) { return noop; } 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, type, active]); const loadModels = () => { const modelsObs$ = modelsManager.$getModels(type, version); setModels(() => null); 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) => { setModelsSelected(prev => { const newModels = [...prev]; const index = newModels.findIndex(m => m.hash === model.hash); if (index === -1) { newModels.push(model); } else { newModels.splice(index, 1); } return newModels; }); }; const filtredModels = () => { if (!active) { return renderableModels; } const lowerSearch = search?.toLowerCase(); return renderableModels?.filter(model => { const findedInRawValues = Object.values(model.model).some(value => { if (typeof value !== "string" && typeof value !== "number") { return false; } return value.toString().toLowerCase().includes(lowerSearch); }); if (findedInRawValues) { return true; } if (!model.model.model) { return false; } return Object.values(model.model.model).some(value => { if (typeof value !== "string" && typeof value !== "number" && !Array.isArray(value)) { return false; } if (Array.isArray(value)) { return value.some(v => v.toString().toLowerCase().includes(lowerSearch)); } return value.toString().toLowerCase().includes(lowerSearch); }); }); }; const handleDelete = (model: BsmLocalModel) => { modelsManager.deleteModels([model], version).then(deleted => { if (!deleted?.length) { return; } setModels(prev => prev.filter(m => m.hash !== model.hash)); setModelsSelected(modelsSelected => modelsSelected.filter(m => m.hash !== model.hash)); }); }; const renderModel = useCallback((renderableModel: RenderableModel) => { const { model } = renderableModel; return ( handleModelClick(model)} onDelete={() => handleDelete(model)} /> ) }, [version]); const renderContent = () => { if (isLoading) { return ( ); } if (!hasModels) { return (
{t("models.panel.grid.no-models")} { e.preventDefault(); downloadModels?.(); }} />
); } return ( rowModels.map(m => m.model.path).join("-")} renderItem={renderModel} /> ); }; return (
{renderContent()}
); }); type RenderableModel = { model: BsmLocalModel; selected: boolean; };