mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
[feature-136] can now search models from the download modal
This commit is contained in:
@@ -2,6 +2,10 @@ import { ipcMain } from "electron";
|
||||
import { IpcRequest } from "shared/models/ipc";
|
||||
import { ModelSaberService } from "../services/thrid-party/model-saber/model-saber.service";
|
||||
import { UtilsService } from "../services/utils.service";
|
||||
import { IpcService } from "../services/ipc.service";
|
||||
import { MSGetQuery } from "shared/models/models/model-saber.model";
|
||||
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
ipcMain.on("ms-get-model-by-id", async (event, request: IpcRequest<string|number>) => {
|
||||
const utils = UtilsService.getInstance();
|
||||
@@ -12,4 +16,9 @@ ipcMain.on("ms-get-model-by-id", async (event, request: IpcRequest<string|number
|
||||
}).catch(e => {
|
||||
utils.ipcSend(request.responceChannel, {success: false, error: e});
|
||||
})
|
||||
});
|
||||
|
||||
ipc.on<MSGetQuery>("search-models", async (req, reply) => {
|
||||
const ms = ModelSaberService.getInstance();
|
||||
reply(ms.searchModels(req.args));
|
||||
});
|
||||
@@ -106,7 +106,5 @@ else{
|
||||
log.error(args?.args);
|
||||
});
|
||||
|
||||
LocalModelsManagerService.getInstance().getModels(MSModelType.Saber, null).toPromise().catch(console.error).then(console.log);
|
||||
|
||||
}).catch(log.error);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { RequestService } from "../request.service";
|
||||
import { UtilsService } from "../utils.service";
|
||||
import { LocalMapsManagerService } from "./local-maps-manager.service";
|
||||
import log from "electron-log"
|
||||
import { isValidUrl } from "../../helpers/url.helpers";
|
||||
import { isValidUrl } from "../../../shared/helpers/url.helpers";
|
||||
import { ipcMain } from "electron";
|
||||
import { WindowManagerService } from "../window-manager.service";
|
||||
import { IpcRequest } from "shared/models/ipc";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
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";
|
||||
@@ -81,4 +82,14 @@ export class ModelSaberService {
|
||||
|
||||
}
|
||||
|
||||
public searchModels(query: MSGetQuery): Observable<MSModel[]>{
|
||||
return new Observable<MSModel[]>(observer => {
|
||||
(async () => {
|
||||
const res = await this.modelSaberApi.searchModel(query);
|
||||
if(res.status !== 200){ observer.error(res.status); }
|
||||
observer.next(Object.values(res.data));
|
||||
})().catch(e => observer.error(e)).then(() => observer.complete());
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -48,7 +48,7 @@ export const DownloadMapsModal: ModalComponent<void, {version: BSVersion, ownedM
|
||||
|
||||
const loaderRef = useRef(null);
|
||||
|
||||
const sortOptions: BsmSelectOption[] = (() => {
|
||||
const sortOptions: BsmSelectOption<SearchOrder>[] = (() => {
|
||||
return BSV_SORT_ORDER.map(sort => ({text: `beat-saver.maps-sorts.${sort}`, value: sort}));
|
||||
})();
|
||||
|
||||
@@ -135,10 +135,10 @@ export const DownloadMapsModal: ModalComponent<void, {version: BSVersion, ownedM
|
||||
mapsDownloader.removeMapToDownload({map, version});
|
||||
}, []);
|
||||
|
||||
const handleSortChange = (newSort: string) => {
|
||||
setSortOrder(() => newSort as SearchOrder);
|
||||
const handleSortChange = (newSort: SearchOrder) => {
|
||||
setSortOrder(() => newSort);
|
||||
setMaps(() => []);
|
||||
setSearchParams(() => ({...searchParams, sortOrder: (newSort as SearchOrder)}));
|
||||
setSearchParams(() => ({...searchParams, sortOrder: (newSort)}));
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
@@ -167,7 +167,7 @@ export const DownloadMapsModal: ModalComponent<void, {version: BSVersion, ownedM
|
||||
</BsmDropdownButton>
|
||||
<input className="h-full bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-2 grow pb-0.5" type="text" name="" id="" placeholder={t("pages.version-viewer.maps.search-bar.search-placeholder")} value={query} onChange={e => setQuery(e.target.value)}/>
|
||||
<BsmButton className="shrink-0 rounded-full py-1 px-3 !bg-light-main-color-1 dark:!bg-main-color-1 flex justify-center items-center capitalize" icon="search" text="modals.download-maps.search-btn" withBar={false} onClick={e => {e.preventDefault(); handleSearch()}}/>
|
||||
<BsmSelect className="bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-1 pb-0.5 text-center" options={sortOptions} onChange={handleSortChange}/>
|
||||
<BsmSelect className="bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-1 pb-0.5 text-center" options={sortOptions} onChange={sort => handleSortChange(sort)}/>
|
||||
</div>
|
||||
<ul className="w-full grow flex content-start flex-wrap gap-2 pt-1.5 px-2 overflow-y-scroll overflow-x-hidden scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900 z-0" >
|
||||
{maps.length === 0 ? (
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import { BsmSelect, BsmSelectOption } from "renderer/components/shared/bsm-select.component";
|
||||
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 { 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";
|
||||
import { ModelSaberService } from "renderer/services/thrird-partys/model-saber.service";
|
||||
import { useBehaviorSubject } from "renderer/hooks/use-behavior-subject.hook";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { OsDiagnosticService } from "renderer/services/os-diagnostic.service";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { useConstant } from "renderer/hooks/use-constant.hook";
|
||||
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";
|
||||
|
||||
export const DownloadModelsModal: ModalComponent<void, {version: BSVersion, type: MSModelType}> = ({resolver, data: { version, type }}) => {
|
||||
|
||||
const modelsDownloader = useService(ModelsDownloaderService);
|
||||
const modelSaber = useService(ModelSaberService);
|
||||
const os = useService(OsDiagnosticService);
|
||||
|
||||
const modelTypesOptions = useConstant<BsmSelectOption<MSModelType>[]>(() => MODEL_TYPES.map(type => ({
|
||||
text: type.toString(), // TODO TRANSLATE
|
||||
value: type
|
||||
})));
|
||||
|
||||
const querySortsOptions = useConstant<BsmSelectOption<MSGetSort>[]>(() => MS_GET_QUERY_SORTS.map(sort => ({
|
||||
text: sort.toString(), // TODO TRANSLATE
|
||||
value: sort
|
||||
})));
|
||||
|
||||
const t = useTranslation();
|
||||
const currentDownload = useObservable(modelsDownloader.currentDownload$(), null);
|
||||
const [msModels, msModels$] = useBehaviorSubject<MSModel[]>([]);
|
||||
const isOnline = useObservable(os.isOnline$, true);
|
||||
const [isLoading, isLoading$] = useBehaviorSubject(false);
|
||||
|
||||
console.log(msModels);
|
||||
|
||||
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 = modelSaber.searchModels(getQuery).subscribe(models => msModels$.next([...msModels, ...models]));
|
||||
return () => sub.unsubscribe();
|
||||
}, [getQuery])
|
||||
|
||||
useOnUpdate(() => {
|
||||
msModels$.next([]);
|
||||
|
||||
|
||||
|
||||
getQuery$.next({
|
||||
...getQuery$.value,
|
||||
start: 0,
|
||||
end: 25,
|
||||
type: currentType,
|
||||
sort: currentSort
|
||||
});
|
||||
|
||||
}, [currentType, currentSort]);
|
||||
|
||||
const loadMore = () => {
|
||||
console.log("load more");
|
||||
const currentQuery = getQuery;
|
||||
currentQuery.start += 25;
|
||||
currentQuery.end += 25;
|
||||
getQuery$.next({...currentQuery});
|
||||
}
|
||||
|
||||
const search = () => {
|
||||
|
||||
const filters = searhInput.split(" ").join(",");
|
||||
|
||||
const getQuery: MSGetQuery = {
|
||||
start: 0, end: 25,
|
||||
type: currentType,
|
||||
sort: currentSort,
|
||||
filter: modelSaber.parseFilter(searhInput)
|
||||
};
|
||||
|
||||
msModels$.next([]);
|
||||
getQuery$.next(getQuery);
|
||||
|
||||
}
|
||||
|
||||
const handleDownloadModel = useCallback((model: MSModel) => {
|
||||
modelsDownloader.addModelToDownload({...model, version});
|
||||
}, []);
|
||||
|
||||
|
||||
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)}/>
|
||||
<input className="h-full bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-2 grow pb-0.5" type="text" name="" id="" placeholder="TODO TRANSLATE Rechercher un modèle" value={searhInput} onChange={e => searhInput$.next(e.target.value)}/>
|
||||
<BsmButton className="shrink-0 rounded-full py-1 px-3 !bg-light-main-color-1 dark:!bg-main-color-1 flex justify-center items-center capitalize" icon="search" type="submit" text="modals.download-maps.search-btn" withBar={false} onClick={e => {e.preventDefault(); search()}}/>
|
||||
<BsmSelect className="bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-1 pb-0.5 text-center" options={querySortsOptions} onChange={(value) => currentSort$.next(value)}/>
|
||||
</div>
|
||||
<ul className="w-full grow flex content-start flex-wrap gap-4 pt-1.5 px-2 overflow-y-scroll overflow-x-hidden scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900 z-0" >
|
||||
{msModels.length === 0 ? (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center">
|
||||
<img className={`w-32 h-32 ${isLoading && "spin-loading"}`} src={isLoading ? BeatWaitingImg : BeatConflictImg} alt=" "/>
|
||||
<span className="text-lg">{t(isLoading ? "Chargement des modèles" : isOnline ? "Aucun modèles trouvés" : "Pas d'internet")}</span> {/** TODO TRANSLATE **/}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{msModels.map(model => (
|
||||
<ModelItem
|
||||
key={model.id}
|
||||
{...model}
|
||||
callbackValue={model}
|
||||
isDownloading={equal(currentDownload, {...model, version} as ModelDownload)}
|
||||
onDownload={handleDownloadModel}
|
||||
/>
|
||||
))}
|
||||
<motion.span onViewportEnter={loadMore} className="block w-full h-8"/>
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -12,26 +12,26 @@ import { followCursor } from "tippy.js"
|
||||
import defaultImage from '../../../../assets/images/default-version-img.jpg'
|
||||
import { BsmLocalModel } from "shared/models/models/bsm-local-model.interface";
|
||||
import { BsmIconType } from "../svgs/bsm-icon.component";
|
||||
import { Observable, of } from "rxjs";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { BsmButton } from "../shared/bsm-button.component";
|
||||
import { BsmBasicSpinner } from "../shared/bsm-basic-spinner/bsm-basic-spinner.component";
|
||||
import { isValidUrl } from "shared/helpers/url.helpers";
|
||||
|
||||
type Props = {
|
||||
type Props<T> = {
|
||||
selected?: boolean,
|
||||
hash: string,
|
||||
id: number,
|
||||
isDownloading?: Observable<boolean>
|
||||
onDelete?: () => void,
|
||||
onDownload?: () => void,
|
||||
onCancelDownload?: () => void
|
||||
id?: number,
|
||||
isDownloading?: boolean,
|
||||
callbackValue?: T
|
||||
onDelete?: (value: T) => void,
|
||||
onDownload?: (value: T) => void,
|
||||
onCancelDownload?: (value: T) => void
|
||||
} & Partial<MSModel> & Partial<BsmLocalModel> & Omit<React.ComponentProps<"li">, "id">
|
||||
|
||||
export const ModelItem = memo((props: Props) => {
|
||||
function modelItem<T = unknown>(props: Props<T>) {
|
||||
|
||||
const color = useThemeColor("first-color");
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const [idContentCopied, setIdContentCopied] = useState<string>(null);
|
||||
const isDownloading = useObservable(props.isDownloading ?? of(false), false);
|
||||
|
||||
const modelPageUrl = (() => {
|
||||
if(!props.id){ return null; }
|
||||
@@ -41,25 +41,39 @@ export const ModelItem = memo((props: Props) => {
|
||||
})();
|
||||
|
||||
const authorPageUrl = (() => {
|
||||
if(!props.discordid){ return null; }
|
||||
if(!props.discord){ return null; }
|
||||
const url = new URL("Profile", MODEL_SABER_URL);
|
||||
url.searchParams.set("user", props.discord.toString());
|
||||
return url.toString();
|
||||
})();
|
||||
|
||||
const thumbnailUrl = (() => {
|
||||
if(!props.thumbnail){ return null; }
|
||||
if(isValidUrl(props.thumbnail)){ return props.thumbnail; }
|
||||
const [file, ext] = props.thumbnail.split(".");
|
||||
return props.download.split("/").slice(0, -1).join("/") + `/${file}.${ext.toLowerCase()}`;
|
||||
})();
|
||||
|
||||
const modelTags = (() => {
|
||||
if(!props.tags){ return null; }
|
||||
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}[] = [];
|
||||
|
||||
if(props.onDownload){
|
||||
buttons.push({ text: "Download", icon: "download", action: props.onDownload }); // TODO TRANSLATE
|
||||
buttons.push({ text: "Download", icon: "download", action: () => props.onDownload(props.callbackValue) }); // TODO TRANSLATE
|
||||
}
|
||||
|
||||
if(props.onDelete){
|
||||
buttons.push({ text: "Delete", icon: "trash", action: props.onDelete }); // TODO TRANSLATE
|
||||
buttons.push({ text: "Delete", icon: "trash", action: () => props.onDelete(props.callbackValue) }); // TODO TRANSLATE
|
||||
}
|
||||
|
||||
if(props.onCancelDownload){
|
||||
buttons.push({ text: "Cancel download", icon: "cross", action: props.onCancelDownload }); // TODO TRANSLATE
|
||||
buttons.push({ text: "Cancel download", icon: "cross", action: () => props.onCancelDownload(props.callbackValue) }); // TODO TRANSLATE
|
||||
}
|
||||
|
||||
return buttons;
|
||||
@@ -77,28 +91,22 @@ export const ModelItem = memo((props: Props) => {
|
||||
// TODO TRANSLATE ALL TEXT
|
||||
<motion.li className={`relative flex-grow min-w-[13rem] h-52 cursor-pointer ${props.className ?? ""}`} onHoverStart={() => setHovered(() => true)} onHoverEnd={() => setHovered(() => false)} onClick={props.onClick}>
|
||||
<GlowEffect visible={props.selected || hovered}/>
|
||||
<div>
|
||||
{!isDownloading ? (
|
||||
<div>
|
||||
{actionButtons().map((button, index) => (
|
||||
<Tippy key={index} placement="top" content={button.text} followCursor="horizontal" plugins={[followCursor]} hideOnClick={false}>
|
||||
<BsmButton className="absolute top-0 right-0 w-6 h-6 p-1 bg-main-color-3 bg-opacity-60 backdrop-blur-md rounded-md text-white" onClick={e => {e.stopPropagation(); e.preventDefault(); button.action();}}/>
|
||||
</Tippy>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="absolute top-0 left-0 w-full h-full rounded-lg overflow-hidden blur-none">
|
||||
<BsmImage className={`absolute top-0 left-0 w-full h-full object-cover ${props.type === MSModelType.Avatar ? "object-top" : ""}`} image={props.thumbnail} placeholder={defaultImage} loading="lazy"/>
|
||||
<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 hover:top-0" onClick={e =>{e.stopPropagation(); e.preventDefault()}}>
|
||||
<div className="absolute top-0 left-0 w-full h-full rounded-lg overflow-hidden blur-none bg-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}/>
|
||||
))
|
||||
): (
|
||||
<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()}}>
|
||||
<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">
|
||||
{props.tags?.map(tag => (
|
||||
{modelTags?.map(tag => (
|
||||
<li key={tag} className="inline-block mr-1 text-xs bg-white text-black px-1 rounded-full p-0.5">{tag}</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -121,4 +129,8 @@ export const ModelItem = memo((props: Props) => {
|
||||
</div>
|
||||
</motion.li>
|
||||
)
|
||||
}, equal);
|
||||
};
|
||||
|
||||
const typedMemo: <T, P>(c: T, propsAreEqual?: (prevProps: Readonly<P>, nextProps: Readonly<P>) => boolean) => T = memo;
|
||||
|
||||
export const ModelItem = typedMemo(modelItem, equal);
|
||||
|
||||
@@ -126,6 +126,13 @@ export const ModelsGrid = forwardRef(({className, version, type, search, active}
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = (model: BsmLocalModel) => {
|
||||
modelsManager.deleteModels([model], version).then(deleted => {
|
||||
if(!deleted){ return; }
|
||||
setModels(prev => prev.filter(m => m.hash !== model.hash));
|
||||
});
|
||||
}
|
||||
|
||||
const renderContent = () => {
|
||||
if(isLoading){
|
||||
return (
|
||||
@@ -151,19 +158,15 @@ export const ModelsGrid = forwardRef(({className, version, type, search, active}
|
||||
<ul className="flex flex-wrap shrink-0 justify-start content-start w-full h-full overflow-y-scroll overflow-x-hidden p-4 gap-4 scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900">
|
||||
{filtredModels().map(localModel => (
|
||||
<ModelItem
|
||||
{...localModel?.model}
|
||||
key={localModel.model?.hash ?? localModel.hash}
|
||||
hash={localModel.model?.hash ?? localModel.hash}
|
||||
id={localModel.model?.id}
|
||||
path={localModel.path}
|
||||
type={localModel.type}
|
||||
name={localModel.model?.name ?? localModel.fileName}
|
||||
thumbnail={localModel.model?.thumbnail}
|
||||
author={localModel.model?.author}
|
||||
discord={localModel.model?.discord}
|
||||
discordid={localModel.model?.discordid}
|
||||
tags={localModel.model?.tags}
|
||||
selected={modelsSelected.some(m => m.hash === localModel.hash)}
|
||||
path={localModel.path}
|
||||
onClick={() => handleModelClick(localModel)}
|
||||
onDelete={() => handleDelete(localModel)}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -8,14 +8,22 @@ 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";
|
||||
|
||||
export function ModelsPanel({version}: {version?: BSVersion}) {
|
||||
const modelsManager = useConstant(() => ModelsManagerService.getInstance());
|
||||
|
||||
const modelsManager = useService(ModelsManagerService);
|
||||
const modelDownloader = useService(ModelsDownloaderService);
|
||||
|
||||
const [avatarsRef, sabersRef, platformsRef, bloqsRef] = [useRef(null), useRef(null), useRef(null), useRef(null)];
|
||||
const [modelTypeTab, setModelTypeTab] = useState<ModelTabType>(ModelTabType.Avatars);
|
||||
|
||||
const [modelTypeTab, setModelTypeTab] = useState<MSModelType>(MSModelType.Avatar);
|
||||
const [currentTabIndex, setCurrentTabIndex] = useState<number>(0);
|
||||
|
||||
const [search, setSearch] = useState<string>("");
|
||||
|
||||
const getActiveTabRef = () => [avatarsRef, sabersRef, platformsRef, bloqsRef][modelTypeTab];
|
||||
const getActiveTabRef = () => [avatarsRef, sabersRef, platformsRef, bloqsRef][currentTabIndex];
|
||||
|
||||
const exportModels = () => {
|
||||
const selectedModels = [avatarsRef, sabersRef, platformsRef, bloqsRef].map(ref =>(ref.current?.getSelectedModels() as BsmLocalModel[])).flat()
|
||||
@@ -27,6 +35,10 @@ export function ModelsPanel({version}: {version?: BSVersion}) {
|
||||
activeTab.current?.deleteSelectedModels();
|
||||
}
|
||||
|
||||
const openDownloadModal = () => {
|
||||
modelDownloader.openDownloadModelsModal(version, modelTypeTab);
|
||||
}
|
||||
|
||||
const threeDotsItems: DropDownItem[] = [
|
||||
{text: "TODO TRANSLATE EXPORT", onClick: exportModels, icon: "export"},
|
||||
{text: "TODO TRANSLATE DELETE", onClick: deleteModels, icon: "trash"},
|
||||
@@ -35,29 +47,22 @@ export function ModelsPanel({version}: {version?: BSVersion}) {
|
||||
return (
|
||||
<div 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}/>
|
||||
<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]">
|
||||
<input type="text" className="h-full w-full bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-2" placeholder={"TODO TRANSLATE"} onChange={e => setSearch(e.target.value)} tabIndex={-1}/>
|
||||
</div>
|
||||
<BsmDropdownButton items={threeDotsItems} className="h-full flex aspect-square relative rounded-full z-[1] bg-light-main-color-1 dark:bg-main-color-3" buttonClassName="rounded-full h-full w-full p-[6px]" icon="three-dots" withBar={false} menuTranslationY="6px" align="center"/>
|
||||
</div>
|
||||
<div className="w-full h-full flex flex-row bg-light-main-color-3 dark:bg-main-color-2 rounded-md shadow-black shadow-md overflow-hidden">
|
||||
<ModelsTabsNavbar className="flex-shrink-0" version={version} tabIndex={modelTypeTab} onTabChange={setModelTypeTab}/>
|
||||
<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 - modelTypeTab * 100}%`}}>
|
||||
<ModelsGrid ref={avatarsRef} version={version} type={MSModelType.Avatar} active={modelTypeTab === ModelTabType.Avatars} search={search}/>
|
||||
<ModelsGrid ref={sabersRef} version={version} type={MSModelType.Saber} active={modelTypeTab === ModelTabType.Sabers} search={search}/>
|
||||
<ModelsGrid ref={platformsRef} version={version} type={MSModelType.Platfrom} active={modelTypeTab === ModelTabType.Platforms} search={search}/>
|
||||
<ModelsGrid ref={bloqsRef} version={version} type={MSModelType.Bloq} active={modelTypeTab === ModelTabType.Bloqs} search={search}/>
|
||||
<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}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
enum ModelTabType {
|
||||
Avatars = 0,
|
||||
Sabers = 1,
|
||||
Platforms = 2,
|
||||
Bloqs = 3,
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ type Props = {
|
||||
className?: string,
|
||||
version?: BSVersion
|
||||
tabIndex: number,
|
||||
onTabChange: (index: number) => void
|
||||
onTabChange: (index: number, tab: BsContentNavBarTab<MSModelType>) => void
|
||||
}
|
||||
|
||||
export function ModelsTabsNavbar({className, version, tabIndex, onTabChange}: Props) {
|
||||
@@ -70,10 +70,10 @@ function ModelTab({version, modelType, active, onClick}: TabProps){
|
||||
}
|
||||
|
||||
return (
|
||||
<li className={`relative w-full cursor-pointer flex-1 text-center text-lg font-bold hover:backdrop-brightness-75 flex justify-center items-center content-center px-7 ${active ? "backdrop-brightness-75" : ""}`} onClick={onClick}>
|
||||
<div className="flex flex-col gap-0.5 justify-start items-center">
|
||||
<li className={`relative w-full cursor-pointer flex-1 text-center text-lg font-bold flex justify-center items-center content-center px-7 dim-on-hover ${active ? "dim" : ""}`} onClick={onClick}>
|
||||
<div className="flex flex-col gap-0.5 justify-start items-center text-main-color-1 dark:text-gray-200">
|
||||
<BsmIcon icon={modelType} className="w-7 h-7"/>
|
||||
<span className="text-main-color-1 dark:text-gray-200 font-thin italic text-xs">{modelType}</span>
|
||||
<span className=" font-thin italic text-xs">{modelType}</span>
|
||||
</div>
|
||||
<div className="flex items-center absolute top-1.5 left-1.5">
|
||||
{!!version && (
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
import { ChangeEvent, CSSProperties } from "react"
|
||||
import { ComponentProps } from "react"
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook"
|
||||
|
||||
type Props = {
|
||||
className?: string,
|
||||
style?: CSSProperties,
|
||||
options?: BsmSelectOption[],
|
||||
onChange?: (value: string) => void
|
||||
type Props<T> = Omit<ComponentProps<"select">, "onChange"> & {
|
||||
options?: BsmSelectOption<T>[],
|
||||
onChange?: (value: T) => void,
|
||||
}
|
||||
|
||||
export function BsmSelect({className, style, options, onChange}: Props) {
|
||||
export function BsmSelect<T = unknown>({className, style, options, onChange}: Props<T>) {
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
const handleChange = (event: ChangeEvent<HTMLSelectElement>) => {
|
||||
onChange?.(event.target.value);
|
||||
const handleChange: ComponentProps<"select">["onChange"] = (e) => {
|
||||
e.preventDefault();
|
||||
onChange?.(options?.[parseInt(e.target.value)]?.value);
|
||||
}
|
||||
|
||||
return (
|
||||
<select className={className} style={style} onChange={handleChange}>
|
||||
{options && options.map(option => (
|
||||
<option key={option.value} value={option.value}>{t(option.text)}</option>
|
||||
{options && options.map((option, index) => (
|
||||
<option key={index} value={index}>{t(option.text)}</option>
|
||||
))}
|
||||
</select>
|
||||
)
|
||||
}
|
||||
|
||||
export interface BsmSelectOption{
|
||||
export interface BsmSelectOption<T>{
|
||||
text: string,
|
||||
value: string
|
||||
value: T
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import { useObservable } from "./use-observable.hook";
|
||||
import { useConstant } from "./use-constant.hook";
|
||||
|
||||
export function useBehaviorSubject<T>(value: T): [T, BehaviorSubject<T>]{
|
||||
const subject = useConstant(() => new BehaviorSubject(value));
|
||||
const subjectValue = useObservable(subject);
|
||||
const subject$ = useConstant(() => new BehaviorSubject(value));
|
||||
const subjectValue = useObservable(subject$, value);
|
||||
|
||||
return [subjectValue, subject];
|
||||
return [subjectValue, subject$];
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { useConstant } from "./use-constant.hook";
|
||||
import { Service } from "shared/models/service.interface";
|
||||
|
||||
export function useService<T>(s: Service<T>): T {
|
||||
const service = useConstant(() => s.getInstance());
|
||||
return service;
|
||||
}
|
||||
@@ -52,6 +52,28 @@
|
||||
animation: glowing 20s linear infinite;
|
||||
}
|
||||
|
||||
.dim > * { z-index: 1; }
|
||||
.dim::before{
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.dim-on-hover:hover > * { z-index: 1; }
|
||||
.dim-on-hover:hover:before{
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
@keyframes glowing {
|
||||
0% { background-position: 0 0; }
|
||||
50% { background-position: 400% 0; }
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Observable } from "rxjs";
|
||||
import { IpcRequest, IpcResponse } from "shared/models/ipc";
|
||||
import { identity } from "rxjs";
|
||||
|
||||
export class IpcService {
|
||||
export class IpcService{
|
||||
|
||||
private static instance: IpcService;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ 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 { DownloadMapsModal } from "renderer/components/modal/modal-types/download-maps-modal.component";
|
||||
import { DownloadModelsModal } from "renderer/components/modal/modal-types/models/download-models-modal.component";
|
||||
|
||||
export class ModelsDownloaderService {
|
||||
|
||||
@@ -20,14 +20,14 @@ export class ModelsDownloaderService {
|
||||
|
||||
private readonly lastDownloadedModel$ = new BehaviorSubject<ModelDownload>(null);
|
||||
private readonly queue$ = new BehaviorSubject<ModelDownload[]>([]);
|
||||
private readonly currentDownload$ = this.queue$.pipe(map(queue => queue.at(0)), distinctUntilChanged(), shareReplay(1));
|
||||
private readonly _currentDownload$ = this.queue$.pipe(map(queue => queue.at(0)), distinctUntilChanged(), shareReplay(1));
|
||||
|
||||
private constructor(){
|
||||
|
||||
this.ipc = IpcService.getInstance();
|
||||
this.modal = ModalService.getInsance();
|
||||
|
||||
this.currentDownload$.pipe(filter(v => !!v)).subscribe(model => this.downloadModel(model));
|
||||
this._currentDownload$.pipe(filter(v => !!v)).subscribe(model => this.downloadModel(model));
|
||||
}
|
||||
|
||||
private downloadModel(model: ModelDownload){
|
||||
@@ -56,18 +56,22 @@ export class ModelsDownloaderService {
|
||||
}
|
||||
|
||||
public isDownloading$(model: ModelDownload): Observable<boolean>{
|
||||
return this.currentDownload$.pipe(map(current => {
|
||||
return this._currentDownload$.pipe(map(current => {
|
||||
if(!current){ return false; }
|
||||
return current.hash === model.hash && current.version === model.version
|
||||
}), distinctUntilChanged());
|
||||
}
|
||||
|
||||
public currentDownload$(): Observable<ModelDownload>{
|
||||
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 openDownloadModelsModal(version: BSVersion, type?: MSModelType): Promise<ModalResponse<void>>{
|
||||
return this.modal.openModal(DownloadMapsModal, {version: null, ownedMaps: []}); // TODO MODELS
|
||||
return this.modal.openModal(DownloadModelsModal, {version, type}); // TODO MODELS
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { MSModel } from "shared/models/models/model-saber.model";
|
||||
import { MSGetQuery, MSGetQueryFilter, MSGetQueryFilterType, MSModel } from "shared/models/models/model-saber.model";
|
||||
import { IpcService } from "../ipc.service";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
export class ModelSaberService {
|
||||
|
||||
@@ -22,4 +23,26 @@ export class ModelSaberService {
|
||||
return res.data;
|
||||
}
|
||||
|
||||
public searchModels(query: MSGetQuery): Observable<MSModel[]>{
|
||||
return this.ipc.sendV2("search-models", {args: query});
|
||||
}
|
||||
|
||||
public parseFilter(stringFilters: string): MSGetQueryFilter[]{
|
||||
return stringFilters.split(" ").map(value => {
|
||||
|
||||
let trimed = value.trim();
|
||||
const isNegative = trimed.at(0) === "-";
|
||||
|
||||
if(isNegative){
|
||||
trimed = trimed.substring(0);
|
||||
}
|
||||
|
||||
if(!trimed.includes(":")){
|
||||
return {type: MSGetQueryFilterType.SearchName, value: trimed, isNegative}
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -22,22 +22,18 @@ import { NotificationService } from "renderer/services/notification.service";
|
||||
import { timer } from "rxjs";
|
||||
import { ConfigurationService } from "renderer/services/configuration.service";
|
||||
import { OsDiagnosticService } from "renderer/services/os-diagnostic.service";
|
||||
import { IpcService } from "../services/ipc.service";
|
||||
import { MSModelType } from "shared/models/models/model-saber.model";
|
||||
import { BsmLocalModelsProgress } from "main/services/additional-content/local-models-manager.service";
|
||||
import { Progression } from "main/helpers/fs.helpers";
|
||||
import { BsmLocalModel } from "shared/models/models/bsm-local-model.interface";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
|
||||
export default function App() {
|
||||
|
||||
const themeService = ThemeService.getInstance();
|
||||
const pageState = PageStateService.getInstance();
|
||||
const maps = MapsManagerService.getInstance();
|
||||
const playlists = PlaylistsManagerService.getInstance();
|
||||
const models = ModelsManagerService.getInstance();
|
||||
const notification = NotificationService.getInstance();
|
||||
const config = ConfigurationService.getInstance();
|
||||
const os = OsDiagnosticService.getInstance();
|
||||
useService(OsDiagnosticService);
|
||||
const themeService = useService(ThemeService);
|
||||
const pageState = useService(PageStateService);
|
||||
const maps = useService(MapsManagerService);
|
||||
const playlists = useService(PlaylistsManagerService);
|
||||
const models = useService(ModelsManagerService);
|
||||
const notification = useService(NotificationService);
|
||||
const config = useService(ConfigurationService);
|
||||
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MSModelType } from "./model-saber.model";
|
||||
import { MSGetSort, MSModelType } from "./model-saber.model";
|
||||
|
||||
export const MODEL_SABER_URL = "https://modelsaber.com";
|
||||
|
||||
@@ -23,4 +23,5 @@ export const MODEL_FILE_EXTENSIONS = {
|
||||
[MSModelType.Saber]: ".saber"
|
||||
}
|
||||
|
||||
export const MODEL_TYPES = Array.from(Object.values(MSModelType));
|
||||
export const MODEL_TYPES = Array.from(Object.values(MSModelType));
|
||||
export const MS_GET_QUERY_SORTS = Array.from(Object.values(MSGetSort));
|
||||
@@ -68,4 +68,5 @@ export enum MSGetQueryFilterType {
|
||||
DiscordID = "discordid",
|
||||
ID = "id",
|
||||
SearchName = "searchName"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface Service<T> {
|
||||
getInstance: () => T;
|
||||
}
|
||||
Reference in New Issue
Block a user