[feature-136] init show local models

This commit is contained in:
MathieuG-P
2023-05-01 23:00:13 +02:00
parent bdbdcfb1b4
commit ab798437d5
36 changed files with 456 additions and 64 deletions
+6
View File
@@ -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;
+11 -1
View File
@@ -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);
});
+3
View File
@@ -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[];
}
+1 -1
View File
@@ -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>
+2 -2
View File
@@ -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;
+1 -1
View File
@@ -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];
}
+2 -2
View File
@@ -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 {
+5
View File
@@ -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
}
@@ -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"
}
@@ -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;
}
}