mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
[feature-136] init show local models
This commit is contained in:
@@ -127,6 +127,12 @@ export async function copyDirectoryWithJunctions(src: string, dest: string, opti
|
||||
}
|
||||
}
|
||||
|
||||
export function extname(filePath: string, withDot = true){
|
||||
const ext = path.extname(filePath);
|
||||
if(withDot){ return ext; }
|
||||
return ext.split('.').at(-1);
|
||||
}
|
||||
|
||||
export interface Progression<T = unknown>{
|
||||
total: number;
|
||||
current: number;
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { ipcMain } from "electron";
|
||||
import { UtilsService } from "../services/utils.service";
|
||||
import { IpcRequest } from "shared/models/ipc";
|
||||
import { MSModel } from "shared/models/model-saber/model-saber.model";
|
||||
import { MSModel, MSModelType } from "shared/models/models/model-saber.model";
|
||||
import { LocalModelsManagerService } from "../services/additional-content/local-models-manager.service";
|
||||
import { IpcService } from "../services/ipc.service";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
ipcMain.on("one-click-install-model", async (event, request: IpcRequest<MSModel>) => {
|
||||
const utils = UtilsService.getInstance();
|
||||
@@ -58,4 +62,10 @@ ipcMain.on("is-models-deep-links-enabled", async (event, request: IpcRequest<voi
|
||||
utils.ipcSend(request.responceChannel, {success: false});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
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);
|
||||
reply(res);
|
||||
});
|
||||
@@ -21,6 +21,7 @@ import { LocalModelsManagerService } from './services/additional-content/local-m
|
||||
import { APP_NAME } from './constants';
|
||||
import { BSLauncherService } from './services/bs-launcher.service';
|
||||
import { IpcRequest } from 'shared/models/ipc';
|
||||
import { MSModelType } from '../shared/models/models/model-saber.model';
|
||||
|
||||
const isDebug = process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true';
|
||||
|
||||
@@ -105,5 +106,7 @@ else{
|
||||
log.error(args?.args);
|
||||
});
|
||||
|
||||
LocalModelsManagerService.getInstance().getModels(MSModelType.Saber, null).toPromise().catch(console.error).then(console.log);
|
||||
|
||||
}).catch(log.error);
|
||||
}
|
||||
@@ -21,7 +21,7 @@ import { Archive } from "../../models/archive.class";
|
||||
import { deleteFolder, ensureFolderExist, getFoldersInFolder, pathExist } from "../../helpers/fs.helpers";
|
||||
import { readFile } from "fs/promises";
|
||||
import { FolderLinkerService } from "../folder-linker.service";
|
||||
import { allSettled } from "../../helpers/promise.helpers";
|
||||
import { allSettled } from "../../../shared/helpers/promise.helpers";
|
||||
|
||||
export class LocalMapsManagerService {
|
||||
|
||||
|
||||
@@ -4,15 +4,22 @@ import { ipcMain } from "electron";
|
||||
import { IpcRequest } from "shared/models/ipc";
|
||||
import { UtilsService } from "../utils.service";
|
||||
import { WindowManagerService } from "../window-manager.service";
|
||||
import { MSModel, MSModelType } from "../../../shared/models/model-saber/model-saber.model";
|
||||
import { MSModel, MSModelType } from "../../../shared/models/models/model-saber.model";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { BSLocalVersionService } from "../bs-local-version.service";
|
||||
import path from "path";
|
||||
import { RequestService } from "../request.service";
|
||||
import { copyFileSync } from "fs-extra";
|
||||
import sanitize from "sanitize-filename";
|
||||
import { ensureFolderExist } from "../../helpers/fs.helpers";
|
||||
import { MODEL_TYPE_FOLDERS } from "../../../shared/models/model-saber/models-constants";
|
||||
import { Progression, ensureFolderExist, extname } from "../../helpers/fs.helpers";
|
||||
import { MODEL_TYPE_FOLDERS } from "../../../shared/models/models/constants";
|
||||
import { InstallationLocationService } from "../installation-location.service";
|
||||
import { Observable } from "rxjs";
|
||||
import { readdir } from "fs/promises";
|
||||
import md5File from "md5-file";
|
||||
import { allSettled } from "../../../shared/helpers/promise.helpers";
|
||||
import { ModelSaberService } from "../thrid-party/model-saber/model-saber.service";
|
||||
import { BsmLocalModel } from "shared/models/models/bsm-local-model.interface";
|
||||
|
||||
export class LocalModelsManagerService {
|
||||
|
||||
@@ -31,7 +38,9 @@ export class LocalModelsManagerService {
|
||||
private readonly utils: UtilsService;
|
||||
private readonly windows: WindowManagerService;
|
||||
private readonly localVersion: BSLocalVersionService;
|
||||
private readonly installPaths: InstallationLocationService;
|
||||
private readonly request: RequestService;
|
||||
private readonly modelSaber: ModelSaberService;
|
||||
|
||||
private constructor(){
|
||||
this.deepLink = DeepLinkService.getInstance();
|
||||
@@ -39,6 +48,8 @@ export class LocalModelsManagerService {
|
||||
this.windows = WindowManagerService.getInstance();
|
||||
this.localVersion = BSLocalVersionService.getInstance();
|
||||
this.request = RequestService.getInstance();
|
||||
this.installPaths = InstallationLocationService.getInstance();
|
||||
this.modelSaber = ModelSaberService.getInstance();
|
||||
|
||||
this.deepLink.addLinkOpenedListener(this.DEEP_LINKS.ModelSaber, (link) => {
|
||||
log.info("DEEP-LINK RECEIVED FROM", this.DEEP_LINKS.ModelSaber, link);
|
||||
@@ -63,10 +74,8 @@ export class LocalModelsManagerService {
|
||||
|
||||
private async getModelFolderPath(type: MSModelType, version?: BSVersion): Promise<string>{
|
||||
|
||||
if(!version){ throw "will be implemented whith models management" }
|
||||
|
||||
const versionPath = await this.localVersion.getVersionPath(version);
|
||||
const modelFolderPath = path.join(versionPath, MODEL_TYPE_FOLDERS[type]);
|
||||
const rootPath = !!version ? await this.localVersion.getVersionPath(version) : this.installPaths.sharedContentPath;
|
||||
const modelFolderPath = path.join(rootPath, MODEL_TYPE_FOLDERS[type]);
|
||||
|
||||
await ensureFolderExist(modelFolderPath);
|
||||
|
||||
@@ -105,6 +114,54 @@ export class LocalModelsManagerService {
|
||||
|
||||
}
|
||||
|
||||
private async getModelsPaths(type: MSModelType, version?: BSVersion): Promise<string[]>{
|
||||
const modelsPath = await this.getModelFolderPath(type, version);
|
||||
const files = await readdir(modelsPath, {withFileTypes: true});
|
||||
|
||||
return files.filter(file => file.isFile() && extname(file.name, false) === type).map(file => path.join(modelsPath, file.name));
|
||||
}
|
||||
|
||||
public getModels(type: MSModelType, version?: BSVersion): Observable<Progression<BsmLocalModel[]>>{
|
||||
|
||||
const progression: Progression<BsmLocalModel[]> = {
|
||||
total: 0,
|
||||
current: 0,
|
||||
extra: null
|
||||
};
|
||||
|
||||
return new Observable<Progression<BsmLocalModel[]>>(subscriber => {
|
||||
(async () => {
|
||||
const modelsPaths = await this.getModelsPaths(type, version);
|
||||
progression.total = modelsPaths.length;
|
||||
|
||||
const models = await allSettled(modelsPaths.map(async modelPath => {
|
||||
|
||||
const hash = await md5File(modelPath)
|
||||
|
||||
const localModel: BsmLocalModel = {
|
||||
path: modelPath,
|
||||
fileName: path.basename(modelPath),
|
||||
model: await this.modelSaber.getModelByHash(hash),
|
||||
type, hash
|
||||
}
|
||||
|
||||
progression.current++;
|
||||
|
||||
subscriber.next(progression);
|
||||
|
||||
return localModel;
|
||||
}));
|
||||
|
||||
progression.extra = models;
|
||||
subscriber.next(progression);
|
||||
|
||||
})().catch(e => subscriber.error(e)).finally(() => subscriber.complete());
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public enableDeepLinks(): boolean{
|
||||
return Array.from(Object.values(this.DEEP_LINKS)).every(link => this.deepLink.registerDeepLink(link));
|
||||
}
|
||||
@@ -117,4 +174,10 @@ export class LocalModelsManagerService {
|
||||
return Array.from(Object.values(this.DEEP_LINKS)).every(link => this.deepLink.isDeepLinkRegistred(link));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export interface BsmLocalModelsProgress {
|
||||
total: number;
|
||||
loaded: number;
|
||||
models: MSModel[];
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { writeFileSync } from 'fs';
|
||||
import { BSVersion } from 'shared/bs-version.interface';
|
||||
import { RequestService } from "./request.service"
|
||||
import { readJSON } from 'fs-extra';
|
||||
import { allSettled } from '../helpers/promise.helpers';
|
||||
import { allSettled } from '../../shared/helpers/promise.helpers';
|
||||
|
||||
export class BSVersionLibService{
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { splitIntoChunk } from "../../../helpers/array.helpers";
|
||||
import { splitIntoChunk } from "../../../../shared/helpers/array.helpers";
|
||||
import { BsvMapDetail } from "shared/models/maps";
|
||||
import { BsvPlaylist, SearchParams } from "shared/models/maps/beat-saver.model";
|
||||
import { BeatSaverApiService } from "./beat-saver-api.service";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import fetch from "node-fetch";
|
||||
import { ApiResult } from "renderer/models/api/api.model";
|
||||
import { MSGetQuery, MSGetQueryFilter, MSGetResponse } from "shared/models/model-saber/model-saber.model";
|
||||
import { MSGetQuery, MSGetQueryFilter, MSGetResponse } from "../../../../shared/models/models/model-saber.model";
|
||||
|
||||
export class ModelSaberApiService {
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { MSGetQuery, MSGetQueryFilter, MSModel } from "shared/models/model-saber/model-saber.model";
|
||||
import { MSGetQuery, MSGetQueryFilterType, MSModel, MSModelPlatform } from "../../../../shared/models/models/model-saber.model";
|
||||
import { ModelSaberApiService } from "./model-saber-api.service";
|
||||
import log from "electron-log";
|
||||
|
||||
export class ModelSaberService {
|
||||
|
||||
@@ -10,6 +11,8 @@ export class ModelSaberService {
|
||||
return ModelSaberService.instance;
|
||||
}
|
||||
|
||||
private readonly modelsHashCache: Map<string, MSModel> = new Map(); // key: hash, value: model
|
||||
|
||||
private readonly modelSaberApi: ModelSaberApiService;
|
||||
|
||||
private constructor(){
|
||||
@@ -21,8 +24,8 @@ export class ModelSaberService {
|
||||
const query: MSGetQuery = {
|
||||
start: 0,
|
||||
end: 1,
|
||||
platform: "pc",
|
||||
filter: [{type: "id", value: id}]
|
||||
platform: MSModelPlatform.PC,
|
||||
filter: [{type: MSGetQueryFilterType.ID, value: id}]
|
||||
}
|
||||
|
||||
try{
|
||||
@@ -37,6 +40,42 @@ export class ModelSaberService {
|
||||
return res.data[`${id}`];
|
||||
}
|
||||
catch(e){
|
||||
log.error(e);
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public async getModelByHash(hash: string): Promise<MSModel>{
|
||||
|
||||
if(this.modelsHashCache.has(hash)){
|
||||
return this.modelsHashCache.get(hash);
|
||||
}
|
||||
|
||||
const query: MSGetQuery = {
|
||||
start: 0,
|
||||
end: 1,
|
||||
platform: MSModelPlatform.PC,
|
||||
filter: [{type: MSGetQueryFilterType.Hash, value: hash}]
|
||||
}
|
||||
|
||||
try{
|
||||
const res = await this.modelSaberApi.searchModel(query);
|
||||
|
||||
if(res.status !== 200){ return null;}
|
||||
|
||||
if(Object.keys(res.data).length === 0){
|
||||
return null;
|
||||
}
|
||||
|
||||
const model = Array.from(Object.values(res.data)).at(0);
|
||||
|
||||
this.modelsHashCache.set(hash, model);
|
||||
|
||||
return model
|
||||
}
|
||||
catch(e){
|
||||
log.error(e);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { BSVersion } from "shared/bs-version.interface";
|
||||
import { LocalMapsManagerService } from "./additional-content/local-maps-manager.service";
|
||||
import { BSLocalVersionService } from "./bs-local-version.service";
|
||||
import { FolderLinkerService, LinkOptions } from "./folder-linker.service";
|
||||
import { allSettled } from "../helpers/promise.helpers";
|
||||
import { allSettled } from "../../shared/helpers/promise.helpers";
|
||||
|
||||
export class VersionFolderLinkerService {
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component"
|
||||
import { BsmImage } from "renderer/components/shared/bsm-image.component"
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook"
|
||||
import { ModalComponent, ModalExitCode } from "renderer/services/modale.service"
|
||||
import { MSModelType } from "shared/models/model-saber/model-saber.model"
|
||||
import { MSModelType } from "shared/models/models/model-saber.model"
|
||||
import BeatRunning from '../../../../../../assets/images/apngs/beat-running.png'
|
||||
|
||||
export const LinkModelsModal: ModalComponent<boolean, MSModelType> = ({resolver, data}) => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { BsmImage } from "renderer/components/shared/bsm-image.component";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { ModalComponent, ModalExitCode } from "renderer/services/modale.service";
|
||||
import BeatConflict from '../../../../../../assets/images/apngs/beat-conflict.png'
|
||||
import { MSModelType } from "shared/models/model-saber/model-saber.model";
|
||||
import { MSModelType } from "shared/models/models/model-saber.model";
|
||||
|
||||
export const UnlinkModelsModal: ModalComponent<boolean, MSModelType> = ({resolver, data}) => {
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import equal from "fast-deep-equal";
|
||||
import { memo, useState } from "react";
|
||||
import { MSModelType } from "shared/models/models/model-saber.model";
|
||||
import { BsmImage } from "../shared/bsm-image.component";
|
||||
import { motion } from "framer-motion";
|
||||
import { GlowEffect } from "../shared/glow-effect.component";
|
||||
import { useConstant } from "renderer/hooks/use-constant.hook";
|
||||
import { LinkOpenerService } from "renderer/services/link-opener.service";
|
||||
import { MODELS_TYPE_MS_PAGE_ROOT, MODEL_SABER_URL } from "shared/models/models/constants";
|
||||
|
||||
type Props = {
|
||||
className?: string,
|
||||
selected?: boolean,
|
||||
// Model props
|
||||
modelHash: string,
|
||||
modelType: MSModelType,
|
||||
modelId?: number,
|
||||
modelName: string,
|
||||
modelImage?: string,
|
||||
modelAuthor?: string,
|
||||
modelTags?: string[],
|
||||
}
|
||||
|
||||
export const ModelItem = memo(({
|
||||
className,
|
||||
selected,
|
||||
modelHash,
|
||||
modelType,
|
||||
modelId,
|
||||
modelName,
|
||||
modelImage,
|
||||
modelAuthor,
|
||||
modelTags,
|
||||
}: Props) => {
|
||||
|
||||
const linkOpener = useConstant(() => LinkOpenerService.getInstance());
|
||||
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
const openModelPage = () => {
|
||||
if(!modelId){ return; }
|
||||
const url = new URL(MODELS_TYPE_MS_PAGE_ROOT[modelType], MODEL_SABER_URL);
|
||||
url.searchParams.set("id", modelId.toString());
|
||||
linkOpener.open(url.toString());
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.li className={`relative w-52 h-52 cursor-pointer ${className ?? ""}`} onHoverStart={() => setHovered(() => true)} onHoverEnd={() => setHovered(() => false)}>
|
||||
<GlowEffect visible={selected || hovered}/>
|
||||
<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" image={modelImage} loading="lazy"/>
|
||||
<motion.div className="absolute cursor-default top-[80%] left-0 w-full h-full p-2 bg-main-color-1 z-[1] bg-opacity-60 backdrop-blur-md transition-all hover:top-0">
|
||||
<h1 className={`w-full max-w-full overflow-hidden font-bold whitespace-nowrap text-ellipsis ${modelId ? "cursor-pointer hover:underline" : ""}`} onClick={openModelPage}>{modelName}</h1>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.li>
|
||||
)
|
||||
}, equal);
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useInView } from "framer-motion";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { MutableRefObject, useRef, 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 { Progression } from "main/helpers/fs.helpers";
|
||||
import { BsmLocalModel } from "shared/models/models/bsm-local-model.interface";
|
||||
import { ModelItem } from "./model-item.component";
|
||||
|
||||
type Props = {
|
||||
className?: string,
|
||||
version?: BSVersion,
|
||||
type: MSModelType
|
||||
}
|
||||
|
||||
export function ModelsGrid({className, version, type}: Props) {
|
||||
|
||||
const modelsManager = useConstant(() => ModelsManagerService.getInstance());
|
||||
|
||||
const ref = useRef();
|
||||
const isVisible = useInView(ref, {once: true, amount: .1});
|
||||
|
||||
const [models, setModelsLoadObservable] = useSwitchableObservable<Progression<BsmLocalModel[]>>();
|
||||
|
||||
console.log(models);
|
||||
|
||||
useOnUpdate(() => {
|
||||
|
||||
if(!isVisible){ return; }
|
||||
|
||||
setModelsLoadObservable(() => modelsManager.$getModels(type, version));
|
||||
|
||||
}, [version, isVisible, type]);
|
||||
|
||||
return (
|
||||
<div ref={ref} className={`w-full h-full flex-shrink-0 ${className ?? ""}`}>
|
||||
{!models || !models?.extra ? (
|
||||
<>loading</>
|
||||
) : !models?.extra.length ? (
|
||||
<>no models</>
|
||||
) : (
|
||||
<ul className="flex w-full h-full overflow-scroll p-4 gap-4">
|
||||
{models.extra.map(localModel => (
|
||||
<ModelItem
|
||||
key={localModel.model?.hash ?? localModel.hash}
|
||||
modelHash={localModel.model?.hash ?? localModel.hash}
|
||||
modelId={localModel.model?.id}
|
||||
modelType={localModel.type}
|
||||
modelName={localModel.model?.name ?? localModel.fileName}
|
||||
modelImage={localModel.model?.thumbnail}
|
||||
modelAuthor={localModel.model?.author}
|
||||
modelTags={localModel.model?.tags}
|
||||
selected={false}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { useState } from "react";
|
||||
import { useState, useRef } from "react";
|
||||
import { ModelsTabsNavbar } from "./models-tabs-navbar.component";
|
||||
import { ModelsGrid } from "./models-grid.component";
|
||||
import { MSModelType } from "shared/models/models/model-saber.model";
|
||||
|
||||
export function ModelsPanel({version}: {version?: BSVersion}) {
|
||||
|
||||
@@ -11,8 +13,14 @@ export function ModelsPanel({version}: {version?: BSVersion}) {
|
||||
<div>
|
||||
|
||||
</div>
|
||||
<div className="w-full h-full flex flex-col bg-light-main-color-3 dark:bg-main-color-2 rounded-md shadow-black shadow-md overflow-hidden">
|
||||
<ModelsTabsNavbar version={version} tabIndex={modelTypeTab} onTabChange={setModelTypeTab}/>
|
||||
<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}/>
|
||||
<div className="flex-grow h-full flex flex-col transition-all duration-300" style={{translate: `0 ${0 - modelTypeTab * 100}%`}}>
|
||||
<ModelsGrid version={version} type={MSModelType.Avatar}/>
|
||||
<ModelsGrid version={version} type={MSModelType.Saber}/>
|
||||
<ModelsGrid version={version} type={MSModelType.Platfrom}/>
|
||||
<ModelsGrid version={version} type={MSModelType.Bloq}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
import { BSVersion } from "shared/bs-version.interface"
|
||||
import { TabNavBar } from "../shared/tab-nav-bar.component"
|
||||
import { MSModelType } from "shared/models/model-saber/model-saber.model"
|
||||
import { MSModelType } from "../../../shared/models/models/model-saber.model"
|
||||
import { LinkButton } from "../maps-mangement-components/link-button.component"
|
||||
import { Variants } from "framer-motion"
|
||||
import { DetailedHTMLProps, useEffect, useState } from "react"
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook"
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook"
|
||||
import { DetailedHTMLProps, useState } from "react"
|
||||
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 { BsContentNavBar, BsContentNavBarTab } from "../shared/bs-content-nav-bar.component"
|
||||
import { useConstant } from "renderer/hooks/use-constant.hook"
|
||||
|
||||
type Props = {
|
||||
className?: string,
|
||||
version?: BSVersion
|
||||
tabIndex: number,
|
||||
onTabChange: (index: number) => void
|
||||
}
|
||||
|
||||
export function ModelsTabsNavbar({version, tabIndex, onTabChange}: Props) {
|
||||
export function ModelsTabsNavbar({className, version, tabIndex, onTabChange}: Props) {
|
||||
|
||||
const renderTab = (props: DetailedHTMLProps<React.HTMLAttributes<any>, any>, text: string, index?: number) => {
|
||||
return <ModelTab version={version} index={index} modelType={Object.values(MSModelType).at(index)} onClick={props.onClick}/>
|
||||
}
|
||||
const tabs = useConstant<BsContentNavBarTab<MSModelType>[]>(() => {
|
||||
return Array.from(Object.values(MSModelType)).map(type => ({
|
||||
text: type, // <= TODO: Translate
|
||||
extra: type
|
||||
}))
|
||||
})
|
||||
|
||||
return (
|
||||
<TabNavBar className="!rounded-none shadow-sm" tabsText={
|
||||
["Avatars", "Sabers", "Platforms", "Bloqs"]
|
||||
} tabIndex={tabIndex} onTabChange={onTabChange} renderTab={renderTab}/>
|
||||
<BsContentNavBar className={`!rounded-none shadow-sm ${className ?? ""}`} tabIndex={tabIndex} onTabChange={onTabChange} tabs={tabs} renderTab={(props, tab) => (
|
||||
<ModelTab version={version} modelType={tab.extra} {...props}/>
|
||||
)}/>
|
||||
)
|
||||
}
|
||||
|
||||
type TabProps = {
|
||||
version?: BSVersion,
|
||||
modelType: MSModelType,
|
||||
index: number,
|
||||
onLink?: (type: MSModelType) => void,
|
||||
onUnlink?: (type: MSModelType) => void,
|
||||
} & DetailedHTMLProps<React.HTMLAttributes<HTMLLIElement>, HTMLLIElement>;
|
||||
@@ -69,9 +69,9 @@ function ModelTab({version, modelType, onClick, onLink, onUnlink}: TabProps){
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="relative text-center text-lg font-bold hover:backdrop-brightness-75 flex justify-center items-center content-center" onClick={onClick}>
|
||||
<span className="text-main-color-1 dark:text-gray-200">{modelType}</span>
|
||||
<div className="h-full flex absolute right-0 top-0 gap-1.5 items-center pr-2">
|
||||
<li className="relative w-full text-center text-lg font-bold hover:backdrop-brightness-75 flex justify-center items-center content-center px-6" onClick={onClick}>
|
||||
<span className="text-main-color-1 dark:text-gray-200 font-thin italic text-xs">{modelType}</span>
|
||||
<div className="flex items-center absolute top-0 left-0">
|
||||
{!!version && (
|
||||
<LinkButton
|
||||
variants={{ hover: {rotate: 22.5}, tap: {rotate: 45} }}
|
||||
@@ -79,7 +79,7 @@ function ModelTab({version, modelType, onClick, onLink, onUnlink}: TabProps){
|
||||
whileHover="hover"
|
||||
whileTap="tap"
|
||||
initial={{rotate: 0}}
|
||||
className="block p-0.5 h-[calc(100%-5px)] aspect-square blur-0 hover:brightness-75"
|
||||
className="block w-6 h-6 aspect-square blur-0 hover:brightness-75"
|
||||
linked={modelsAreLinked}
|
||||
title={"aaa"}
|
||||
onClick={onClickLink}
|
||||
|
||||
@@ -23,7 +23,7 @@ export function NavBar() {
|
||||
useObservable(I18nService.getInstance().currentLanguage$)
|
||||
|
||||
return (
|
||||
<nav id='nav-bar' className='z-10 flex flex-col h-full max-h-full items-center p-1 bg-light-main-color-1 dark:bg-main-color-1'>
|
||||
<nav id='nav-bar' className='z-10 flex flex-col h-full max-h-full items-center p-1'>
|
||||
<BsManagerIcon className='relative aspect-square w-16 h-16 mb-3'/>
|
||||
<ol id='versions' className='w-fit max-w-[120px] relative left-[2px] grow overflow-y-hidden scrollbar-track-transparent scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900 hover:overflow-y-scroll'>
|
||||
<MapsNavBarItem/>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { DetailedHTMLProps, Fragment } from "react";
|
||||
|
||||
type Props<T> = {
|
||||
className?: string;
|
||||
tabIndex: number;
|
||||
tabs: BsContentNavBarTab<T>[];
|
||||
renderTab: (props: DetailedHTMLProps<React.HTMLAttributes<any>, any>, tab?: BsContentNavBarTab<T>, activeTab?: BsContentNavBarTab<T>) => JSX.Element;
|
||||
onTabChange?: (index: number, tab?: BsContentNavBarTab<T>) => void;
|
||||
}
|
||||
|
||||
export type BsContentNavBarTab<T = unknown> = {
|
||||
text: string;
|
||||
extra?: T
|
||||
}
|
||||
|
||||
export function BsContentNavBar<T>({className, tabIndex, tabs, renderTab, onTabChange}: Props<T>) {
|
||||
|
||||
const handleTabClick = (index: number) => {
|
||||
onTabChange?.(index, tabs[index]);
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className={`h-full grid grid-flow-row ${className ?? ""}`}>
|
||||
{tabs.map((tab, i) => (
|
||||
<Fragment key={i}>
|
||||
{ renderTab({onClick: () => handleTabClick(i)}, tab, tabs[tabIndex]) }
|
||||
</Fragment>
|
||||
))}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
@@ -4,8 +4,8 @@ export function GermanIcon(props: {className?: string, style?: CSSProperties}) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="71px" height="48px" viewBox="0 0 71 48" version="1.1" className={props.className} style={props.style}>
|
||||
<defs/>
|
||||
<g id="Flags" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(-239.000000, -805.000000)">
|
||||
<g transform="translate(70.000000, 70.000000)" fill-rule="nonzero" id="Germany">
|
||||
<g id="Flags" stroke="none" strokeWidth="1" fill="none" fillRule="evenodd" transform="translate(-239.000000, -805.000000)">
|
||||
<g transform="translate(70.000000, 70.000000)" fillRule="nonzero" id="Germany">
|
||||
<g transform="translate(169.000000, 735.000000)">
|
||||
<g id="Bolivia-9">
|
||||
<path d="M0.52,7 L0.52,16 L70.52,16 L70.52,7 C70.52,3.33282926 67.5471707,0.36 63.88,0.36 L7.17,0.36 C5.40723053,0.357345224 3.71575038,1.05574249 2.46834517,2.3012719 C1.22093997,3.54680131 0.519998001,5.23722853 0.52,7 Z" id="Shape" fill="#434A54"/>
|
||||
|
||||
@@ -61,7 +61,7 @@ export default function TitleBar({template = "index.html"} : {template: AppWindo
|
||||
if(template === "index.html"){
|
||||
|
||||
return (
|
||||
<header id="titlebar" className="min-h-[22px] bg-light-main-color-1 shrink-0 dark:bg-main-color-1 w-screen h-[22px] flex content-center items-center justify-start z-10">
|
||||
<header id="titlebar" className="min-h-[22px] w-screen h-[22px] flex content-center items-center justify-start z-10">
|
||||
<div id="drag-region" className='grow basis-0 h-full'>
|
||||
<div id="window-title" className='pl-1'>
|
||||
<span className='text-gray-800 dark:text-gray-100 font-bold text-xs italic'>BSManager</span>
|
||||
|
||||
@@ -5,8 +5,8 @@ export function useObservable<T>(observable: Observable<T>, defaultValue?: T): T
|
||||
const [obsValue, setObsValue] = useState(defaultValue ?? null as T);
|
||||
|
||||
useEffect(() => {
|
||||
const sub = observable.subscribe(val => setObsValue(() => val));
|
||||
return () => sub.unsubscribe();
|
||||
const sub = observable.subscribe(val => setObsValue(() => val));
|
||||
return () => sub.unsubscribe();
|
||||
}, [])
|
||||
|
||||
return obsValue;
|
||||
|
||||
@@ -2,6 +2,6 @@ import { useEffect, EffectCallback, DependencyList } from "react";
|
||||
|
||||
export function useOnUpdate(func: EffectCallback, deps: DependencyList = []) {
|
||||
useEffect(() => {
|
||||
func();
|
||||
return func();
|
||||
}, deps);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Observable } from "rxjs";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export function useSwitchableObservable<T>(observable?: Observable<T>, clearOnSwitch = true): [T, React.Dispatch<React.SetStateAction<Observable<T>>>]{
|
||||
|
||||
const [currentObs, setCurrentObs] = useState(observable);
|
||||
const [obsValue, setObsValue] = useState<T>();
|
||||
|
||||
useEffect(() => {
|
||||
if(!currentObs){ return };
|
||||
if(clearOnSwitch){ setObsValue(() => null); };
|
||||
const sub = currentObs.subscribe(val => setObsValue(() => val));
|
||||
return () => {
|
||||
sub.unsubscribe()
|
||||
}
|
||||
}, [currentObs])
|
||||
|
||||
return [obsValue, setCurrentObs];
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defaultIfEmpty } from "rxjs/operators";
|
||||
import { defaultIfEmpty, share } from "rxjs/operators";
|
||||
import { Observable } from "rxjs";
|
||||
import { IpcRequest, IpcResponse } from "shared/models/ipc";
|
||||
import { identity } from "rxjs";
|
||||
@@ -62,7 +62,7 @@ export class IpcService {
|
||||
window.electron.ipcRenderer.on(request.responceChannel, (res: T) => observer.next(res));
|
||||
window.electron.ipcRenderer.on(errorChannel, (err: Error) => observer.error(err));
|
||||
window.electron.ipcRenderer.on(completeChannel, () => observer.complete());
|
||||
}).pipe(defaultValue ? defaultIfEmpty(defaultValue) : identity);
|
||||
}).pipe(defaultValue ? defaultIfEmpty(defaultValue) : identity, share());
|
||||
|
||||
window.electron.ipcRenderer.once(completeChannel, () => {
|
||||
window.electron.ipcRenderer.removeAllListeners(request.responceChannel);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { timer } from "rxjs";
|
||||
import { MSModel } from "shared/models/model-saber/model-saber.model";
|
||||
import { MSModel } from "shared/models/models/model-saber.model";
|
||||
import { IpcService } from "./ipc.service";
|
||||
import { ProgressBarService } from "./progress-bar.service";
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { MSModelType } from "shared/models/model-saber/model-saber.model";
|
||||
import { MSModelType } from "shared/models/models/model-saber.model";
|
||||
import { IpcService } from "../ipc.service";
|
||||
import { VersionFolderLinkerService, VersionLinkerActionType } from "../version-folder-linker.service";
|
||||
import { MODEL_TYPE_FOLDERS } from "shared/models/model-saber/models-constants";
|
||||
import { MODEL_TYPE_FOLDERS } from "shared/models/models/constants";
|
||||
import { Observable, distinctUntilChanged, lastValueFrom, mergeMap, share } from "rxjs";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { ModalExitCode, ModalService } from "../modale.service";
|
||||
import { LinkModelsModal } from "renderer/components/modal/modal-types/models/link-models-modal.component";
|
||||
import { UnlinkModelsModal } from "renderer/components/modal/modal-types/models/unlink-models-modal.component";
|
||||
import { Progression } from "main/helpers/fs.helpers";
|
||||
import { BsmLocalModel } from "shared/models/models/bsm-local-model.interface";
|
||||
|
||||
export class ModelsManagerService {
|
||||
|
||||
@@ -63,6 +65,10 @@ export class ModelsManagerService {
|
||||
});
|
||||
}
|
||||
|
||||
public $getModels(type: MSModelType, version?: BSVersion): Observable<Progression<BsmLocalModel[]>>{
|
||||
return this.ipc.sendV2<Progression<BsmLocalModel[]>>("get-version-models", { args: { version, type } });
|
||||
}
|
||||
|
||||
public isDeepLinksEnabled(): Promise<boolean>{
|
||||
return this.ipc.send<boolean>("is-models-deep-links-enabled").then(res => (
|
||||
res.success ? res.data : false
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MSModel } from "shared/models/model-saber/model-saber.model";
|
||||
import { MSModel } from "shared/models/models/model-saber.model";
|
||||
import { IpcService } from "../ipc.service";
|
||||
|
||||
export class ModelSaberService {
|
||||
|
||||
@@ -22,6 +22,11 @@ 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";
|
||||
|
||||
export default function App() {
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { ProgressBarService } from "renderer/services/progress-bar.service";
|
||||
import { ThemeService } from "renderer/services/theme.service";
|
||||
import { ModelSaberService } from "renderer/services/thrird-partys/model-saber.service";
|
||||
import { WindowManagerService } from "renderer/services/window-manager.service";
|
||||
import { MSModel } from "shared/models/model-saber/model-saber.model";
|
||||
import { MSModel } from "shared/models/models/model-saber.model";
|
||||
import defaultImage from '../../../../assets/images/default-version-img.jpg'
|
||||
|
||||
export default function OneClickDownloadModel() {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { MSModel, MSModelType } from "./model-saber.model";
|
||||
|
||||
export interface BsmLocalModel{
|
||||
readonly path: string;
|
||||
readonly fileName: string;
|
||||
readonly hash: string;
|
||||
readonly type: MSModelType;
|
||||
readonly model?: MSModel
|
||||
}
|
||||
+9
@@ -1,8 +1,17 @@
|
||||
import { MSModelType } from "./model-saber.model";
|
||||
|
||||
export const MODEL_SABER_URL = "https://modelsaber.com";
|
||||
|
||||
export const MODEL_TYPE_FOLDERS = {
|
||||
[MSModelType.Avatar]: "CustomAvatars",
|
||||
[MSModelType.Bloq]: "CustomNotes",
|
||||
[MSModelType.Platfrom]: "CustomPlatforms",
|
||||
[MSModelType.Saber]: "CustomSabers"
|
||||
}
|
||||
|
||||
export const MODELS_TYPE_MS_PAGE_ROOT = {
|
||||
[MSModelType.Avatar]: "Avatars",
|
||||
[MSModelType.Bloq]: "Bloqs",
|
||||
[MSModelType.Platfrom]: "Platforms",
|
||||
[MSModelType.Saber]: "Sabers"
|
||||
}
|
||||
+23
-4
@@ -12,11 +12,13 @@ export interface MSModel {
|
||||
discord?: string,
|
||||
variationid?: number,
|
||||
platform: MSModelPlatform,
|
||||
download: string,
|
||||
download: ModelDownloadURL,
|
||||
install_link: string,
|
||||
date: string
|
||||
}
|
||||
|
||||
export type ModelDownloadURL = string;
|
||||
|
||||
export enum MSModelType {
|
||||
Avatar = "avatar",
|
||||
Saber = "saber",
|
||||
@@ -47,6 +49,23 @@ export interface MSGetQueryFilter {
|
||||
}
|
||||
|
||||
export type MSGetResponse<T extends string = string> = Record<T, MSModel>;
|
||||
export type MSGetSort = "date"|"name"|"author"
|
||||
export type MSGetSortDirection = "asc"|"desc"
|
||||
export type MSGetQueryFilterType = "author"|"name"|"tag"|"hash"|"discordid"|"id"|"searchName"
|
||||
|
||||
export enum MSGetSort {
|
||||
Date = "date",
|
||||
Name = "name",
|
||||
Author = "author"
|
||||
}
|
||||
export enum MSGetSortDirection {
|
||||
Ascending = "asc",
|
||||
Descending = "desc"
|
||||
}
|
||||
|
||||
export enum MSGetQueryFilterType {
|
||||
Author = "author",
|
||||
Name = "name",
|
||||
Tag = "tag",
|
||||
Hash = "hash",
|
||||
DiscordID = "discordid",
|
||||
ID = "id",
|
||||
SearchName = "searchName"
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
export class Optionnal<T = unknown>{
|
||||
|
||||
private static readonly EMPTY = new Optionnal<null>(null);
|
||||
|
||||
public static empty(): Optionnal<null>{
|
||||
return Optionnal.EMPTY;
|
||||
}
|
||||
|
||||
public static of<T>(value: T): Optionnal<T>{
|
||||
if(value == null){
|
||||
throw new Error("Value cannot be null");
|
||||
}
|
||||
return new Optionnal<T>(value);
|
||||
}
|
||||
|
||||
public static ofNullable<T>(value: T): Optionnal<T>{
|
||||
return value == null ? Optionnal.empty() : new Optionnal<T>(value);
|
||||
}
|
||||
|
||||
private readonly value: T;
|
||||
|
||||
constructor(value: T){
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public get(): T{
|
||||
if(this.isEmpty()){
|
||||
throw new Error("No value present");
|
||||
}
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public isPresent(): boolean{ return this.value != null; }
|
||||
public isEmpty(): boolean{ return this.value == null; }
|
||||
|
||||
public map<U>(mapper: (value: T) => U): Optionnal<U>{
|
||||
if(this.isEmpty()){ return Optionnal.empty(); }
|
||||
return Optionnal.ofNullable(mapper(this.value));
|
||||
}
|
||||
|
||||
public orElse(other: T): T{
|
||||
return this.isEmpty() ? other : this.value;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user