mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
[feature-136] Add model types icons + export models + init delete models + init searchbar
This commit is contained in:
@@ -5,6 +5,9 @@ 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";
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { from } from "rxjs";
|
||||
import { BsmLocalModel } from "shared/models/models/bsm-local-model.interface";
|
||||
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
@@ -68,4 +71,9 @@ ipc.on<{version: BSVersion, type: MSModelType}>("get-version-models", async (req
|
||||
const models = LocalModelsManagerService.getInstance();
|
||||
const res = await models.getModels(req.args.type, req.args.version);
|
||||
reply(res);
|
||||
});
|
||||
|
||||
ipc.on<{version: BSVersion, models: BsmLocalModel[], outPath: string}>("export-models", async (req, reply) => {
|
||||
const models = LocalModelsManagerService.getInstance();
|
||||
reply(await models.exportModels(req.args.outPath, req.args.version, req.args.models));
|
||||
});
|
||||
@@ -12,14 +12,16 @@ import { RequestService } from "../request.service";
|
||||
import { copyFileSync } from "fs-extra";
|
||||
import sanitize from "sanitize-filename";
|
||||
import { Progression, ensureFolderExist } from "../../helpers/fs.helpers";
|
||||
import { MODEL_FILE_EXTENSIONS, MODEL_TYPE_FOLDERS } from "../../../shared/models/models/constants";
|
||||
import { MODEL_FILE_EXTENSIONS, MODEL_TYPES, MODEL_TYPE_FOLDERS } from "../../../shared/models/models/constants";
|
||||
import { InstallationLocationService } from "../installation-location.service";
|
||||
import { Observable } from "rxjs";
|
||||
import { Observable, lastValueFrom } 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";
|
||||
import { ArchiveProgress } from "shared/models/archive.interface";
|
||||
import { Archive } from "../../models/archive.class";
|
||||
|
||||
export class LocalModelsManagerService {
|
||||
|
||||
@@ -145,7 +147,7 @@ export class LocalModelsManagerService {
|
||||
|
||||
const localModel: BsmLocalModel = {
|
||||
path: modelPath,
|
||||
fileName: path.basename(modelPath),
|
||||
fileName: path.basename(modelPath, MODEL_FILE_EXTENSIONS[type]),
|
||||
model: await this.modelSaber.getModelByHash(hash),
|
||||
type, hash
|
||||
}
|
||||
@@ -165,6 +167,20 @@ export class LocalModelsManagerService {
|
||||
|
||||
}
|
||||
|
||||
public async exportModels(output: string, version?: BSVersion, models?: BsmLocalModel[]): Promise<Observable<ArchiveProgress>>{
|
||||
const archive = new Archive(output);
|
||||
if(models?.length){
|
||||
models.forEach(model => archive.addFile(model.path));
|
||||
}
|
||||
else{
|
||||
for(const type of MODEL_TYPES){
|
||||
archive.addDirectory(await this.getModelFolderPath(type, version));
|
||||
}
|
||||
}
|
||||
|
||||
return archive.finalize();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public enableDeepLinks(): boolean{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import equal from "fast-deep-equal";
|
||||
import { memo, useState } from "react";
|
||||
import { MSModel } from "shared/models/models/model-saber.model";
|
||||
import { MSModel, 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";
|
||||
@@ -10,17 +10,19 @@ import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import { followCursor } from "tippy.js"
|
||||
import defaultImage from '../../../../assets/images/default-version-img.jpg'
|
||||
import { BsmLocalModel } from "shared/models/models/bsm-local-model.interface";
|
||||
|
||||
type Props = {
|
||||
className?: string,
|
||||
selected?: boolean,
|
||||
hash: string,
|
||||
onClick?: React.ComponentProps<"li">["onClick"],
|
||||
} & Partial<MSModel>
|
||||
} & Partial<MSModel> & Partial<BsmLocalModel>
|
||||
|
||||
export const ModelItem = memo(({
|
||||
className,
|
||||
selected,
|
||||
path,
|
||||
hash,
|
||||
id,
|
||||
type,
|
||||
@@ -60,10 +62,10 @@ export const ModelItem = memo(({
|
||||
|
||||
return (
|
||||
// TODO TRANSLATE ALL TEXT
|
||||
<motion.li className={`relative w-52 h-52 cursor-pointer ${className ?? ""}`} onHoverStart={() => setHovered(() => true)} onHoverEnd={() => setHovered(() => false)} onClick={onClick}>
|
||||
<motion.li className={`relative flex-grow min-w-[13rem] h-52 cursor-pointer ${className ?? ""}`} onHoverStart={() => setHovered(() => true)} onHoverEnd={() => setHovered(() => false)} onClick={onClick}>
|
||||
<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 object-cover" image={thumbnail} placeholder={defaultImage} loading="lazy"/>
|
||||
<BsmImage className={`absolute top-0 left-0 w-full h-full object-cover ${type === MSModelType.Avatar ? "object-top" : ""}`} image={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()}}>
|
||||
<BsmLink className={`block w-fit max-w-full overflow-hidden font-bold whitespace-nowrap text-ellipsis ${id ? "cursor-pointer hover:underline" : ""}`} href={modelPageUrl}>{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}>{author ?? ''}</BsmLink>
|
||||
@@ -73,13 +75,20 @@ export const ModelItem = memo(({
|
||||
))}
|
||||
</ul>
|
||||
<Tippy placement="top" content={idContentCopied === "hash" ? "Copied!" : "Copy hash"} followCursor="horizontal" plugins={[followCursor]} hideOnClick={false}>
|
||||
<span className="mt-0.5 cursor-copy block w-full max-w-full overflow-hidden whitespace-nowrap text-ellipsis bg-main-color-1 rounded-md p-1 uppercase text-xs" onClick={() => copyContent(`${hash}`, 'hash')}>{hash}</span>
|
||||
<span className="mt-0.5 cursor-copy block w-full max-w-fit overflow-hidden whitespace-nowrap text-ellipsis bg-main-color-1 rounded-md p-1 uppercase text-xs" onClick={() => copyContent(`${hash}`, 'hash')}>{hash}</span>
|
||||
</Tippy>
|
||||
{id && (
|
||||
<Tippy placement="top" content={idContentCopied === "id" ? "Copied!" : "Copy id"} followCursor="horizontal" plugins={[followCursor]} hideOnClick={false}>
|
||||
<span className="cursor-copy w-fit max-w-full overflow-hidden whitespace-nowrap text-ellipsis bg-main-color-1 rounded-md p-1 uppercase text-xs" onClick={() => copyContent(`${id}`, 'id')}>{id}</span>
|
||||
</Tippy>
|
||||
)}
|
||||
<div className="flex gap-1">
|
||||
{id && (
|
||||
<Tippy placement="top" content={idContentCopied === "id" ? "Copied!" : "Copy id"} followCursor="horizontal" plugins={[followCursor]} hideOnClick={false}>
|
||||
<span className="cursor-copy w-fit shrink-0 max-w-full overflow-hidden whitespace-nowrap text-ellipsis bg-main-color-1 rounded-md p-1 uppercase text-xs" onClick={() => copyContent(`${id}`, 'id')}>{id}</span>
|
||||
</Tippy>
|
||||
)}
|
||||
{path && (
|
||||
<Tippy placement="top" content={idContentCopied === "path" ? "Copied!" : "Copy path"} followCursor="horizontal" plugins={[followCursor]} hideOnClick={false}>
|
||||
<span className="cursor-copy w-fit max-w-full overflow-hidden whitespace-nowrap text-ellipsis bg-main-color-1 rounded-md p-1 text-xs" onClick={() => copyContent(`${path}`, 'path')}>{path}</span>
|
||||
</Tippy>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.li>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useInView } from "framer-motion";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { MutableRefObject, useRef, useState } from "react";
|
||||
import { useRef, forwardRef, useImperativeHandle } 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";
|
||||
@@ -9,6 +9,7 @@ import { useSwitchableObservable } from "renderer/hooks/use-switchable-observabl
|
||||
import { Progression } from "main/helpers/fs.helpers";
|
||||
import { BsmLocalModel } from "shared/models/models/bsm-local-model.interface";
|
||||
import { ModelItem } from "./model-item.component";
|
||||
import { useBehaviorSubject } from "renderer/hooks/use-behavior-subject.hook";
|
||||
|
||||
type Props = {
|
||||
className?: string,
|
||||
@@ -16,7 +17,7 @@ type Props = {
|
||||
type: MSModelType
|
||||
}
|
||||
|
||||
export function ModelsGrid({className, version, type}: Props) {
|
||||
export const ModelsGrid = forwardRef(({className, version, type}: Props, forwardRef) => {
|
||||
|
||||
const modelsManager = useConstant(() => ModelsManagerService.getInstance());
|
||||
|
||||
@@ -24,26 +25,38 @@ export function ModelsGrid({className, version, type}: Props) {
|
||||
const isVisible = useInView(ref, {once: true, amount: .1});
|
||||
|
||||
const [models, setModelsLoadObservable] = useSwitchableObservable<Progression<BsmLocalModel[]>>();
|
||||
|
||||
const [modelsSelected, modelsSelected$] = useBehaviorSubject<Set<BsmLocalModel>>(new Set());
|
||||
const isLoading = !models || !models?.extra;
|
||||
const hasModels = !isLoading && models?.extra.length;
|
||||
|
||||
console.log(models);
|
||||
useImperativeHandle(forwardRef, () => ({
|
||||
getSelectedModels: () => {
|
||||
return Array.from(modelsSelected)
|
||||
},
|
||||
}), [modelsSelected]);
|
||||
|
||||
useOnUpdate(() => {
|
||||
|
||||
if(!isVisible){ return; }
|
||||
|
||||
setModelsLoadObservable(() => modelsManager.$getModels(type, version));
|
||||
|
||||
}, [version, isVisible, type]);
|
||||
|
||||
const handleModelClick = (model: BsmLocalModel) => {
|
||||
const newSet = new Set(modelsSelected);
|
||||
if(newSet.has(model)){
|
||||
newSet.delete(model);
|
||||
}
|
||||
else {
|
||||
newSet.add(model);
|
||||
}
|
||||
modelsSelected$.next(newSet);
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={ref} className={`w-full h-full flex-shrink-0 ${className ?? ""}`}>
|
||||
{isLoading && <>loading</>}
|
||||
{!hasModels && <>no models</>}
|
||||
{hasModels && !isLoading && (
|
||||
<ul className="flex flex-wrap justify-center w-full h-full overflow-scroll p-4 gap-4">
|
||||
<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">
|
||||
{models.extra.map(localModel => (
|
||||
<ModelItem
|
||||
key={localModel.model?.hash ?? localModel.hash}
|
||||
@@ -56,11 +69,13 @@ export function ModelsGrid({className, version, type}: Props) {
|
||||
discord={localModel.model?.discord}
|
||||
discordid={localModel.model?.discordid}
|
||||
tags={localModel.model?.tags}
|
||||
selected={false}
|
||||
selected={modelsSelected.has(localModel)}
|
||||
path={localModel.path}
|
||||
onClick={() => handleModelClick(localModel)}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,25 +1,50 @@
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { useState, useRef } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { ModelsTabsNavbar } from "./models-tabs-navbar.component";
|
||||
import { ModelsGrid } from "./models-grid.component";
|
||||
import { MSModelType } from "shared/models/models/model-saber.model";
|
||||
import { BsmDropdownButton, DropDownItem } from "../shared/bsm-dropdown-button.component";
|
||||
import { useConstant } from "renderer/hooks/use-constant.hook";
|
||||
import { ModelsManagerService } from "renderer/services/models-management/models-manager.service";
|
||||
import { BsmLocalModel } from "shared/models/models/bsm-local-model.interface";
|
||||
|
||||
export function ModelsPanel({version}: {version?: BSVersion}) {
|
||||
|
||||
const modelsManager = useConstant(() => ModelsManagerService.getInstance());
|
||||
const [avatarsRef, sabersRef, platformsRef, bloqsRef] = [useRef(null), useRef(null), useRef(null), useRef(null)];
|
||||
const [modelTypeTab, setModelTypeTab] = useState<ModelTabType>(ModelTabType.Avatars);
|
||||
|
||||
const getSelectedModels = () => [avatarsRef, sabersRef, platformsRef, bloqsRef].map(ref =>(ref.current?.getSelectedModels() as BsmLocalModel[])).flat()
|
||||
|
||||
const exportModels = () => {
|
||||
modelsManager.exportModels(getSelectedModels(), version);
|
||||
}
|
||||
|
||||
const deleteModels = () => {
|
||||
modelsManager.deleteModels(getSelectedModels(), version);
|
||||
}
|
||||
|
||||
const threeDotsItems = useConstant<DropDownItem[]>(() => [
|
||||
{text: "TODO TRANSLATE EXPORT", onClick: exportModels, icon: "export"},
|
||||
{text: "TODO TRANSLATE DELETE", onClick: deleteModels, icon: "trash"},
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center">
|
||||
<div>
|
||||
|
||||
<div className="w-full shrink-0 flex h-9 justify-center px-40 gap-2 mb-3 text-main-color-1 dark:text-white">
|
||||
<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"} 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}/>
|
||||
|
||||
<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}/>
|
||||
<ModelsGrid ref={avatarsRef} version={version} type={MSModelType.Avatar}/>
|
||||
<ModelsGrid ref={sabersRef} version={version} type={MSModelType.Saber}/>
|
||||
<ModelsGrid ref={platformsRef} version={version} type={MSModelType.Platfrom}/>
|
||||
<ModelsGrid ref={bloqsRef} version={version} type={MSModelType.Bloq}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ModelsManagerService } from "renderer/services/models-management/models
|
||||
import { BsContentNavBar, BsContentNavBarTab } from "../shared/bs-content-nav-bar.component"
|
||||
import { useConstant } from "renderer/hooks/use-constant.hook"
|
||||
import { BsmIcon } from "../svgs/bsm-icon.component"
|
||||
import { MODEL_TYPES } from "../../../shared/models/models/constants";
|
||||
|
||||
type Props = {
|
||||
className?: string,
|
||||
@@ -18,7 +19,7 @@ type Props = {
|
||||
export function ModelsTabsNavbar({className, version, tabIndex, onTabChange}: Props) {
|
||||
|
||||
const tabs = useConstant<BsContentNavBarTab<MSModelType>[]>(() => {
|
||||
return Array.from(Object.values(MSModelType)).map(type => ({
|
||||
return MODEL_TYPES.map(type => ({
|
||||
text: type, // <= TODO: Translate
|
||||
extra: type
|
||||
}))
|
||||
@@ -71,12 +72,12 @@ function ModelTab({version, modelType, active, onClick, onLink, onUnlink}: TabPr
|
||||
}
|
||||
|
||||
return (
|
||||
<li className={`relative w-full text-center text-lg font-bold hover:backdrop-brightness-75 flex justify-center items-center content-center px-6 ${active ? "backdrop-brightness-75" : ""}`} onClick={onClick}>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<BsmIcon icon="trash"/>
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
<div className="flex items-center absolute top-1 left-1">
|
||||
<div className="flex items-center absolute top-1.5 left-1.5">
|
||||
{!!version && (
|
||||
<LinkButton
|
||||
variants={{ hover: {rotate: 22.5}, tap: {rotate: 45} }}
|
||||
@@ -84,7 +85,7 @@ function ModelTab({version, modelType, active, onClick, onLink, onUnlink}: TabPr
|
||||
whileHover="hover"
|
||||
whileTap="tap"
|
||||
initial={{rotate: 0}}
|
||||
className="block w-6 h-6 aspect-square blur-0 hover:brightness-75"
|
||||
className="block w-6 h-6 aspect-square blur-0 cursor-pointer hover:brightness-75"
|
||||
linked={modelsAreLinked}
|
||||
title={"aaa"} //TODO ADD TRANSLATION
|
||||
onClick={onClickLink}
|
||||
|
||||
@@ -52,8 +52,13 @@ import { VolumeUpIcon } from "./icons/volume-up-icon.component";
|
||||
import { VolumeOffIcon } from "./icons/volume-off-icon.component";
|
||||
import { VolumeDownIcon } from "./icons/volume-down-icon.component";
|
||||
import { GermanIcon } from "./flags/german-icon.component";
|
||||
import { MSModelType } from "shared/models/models/model-saber.model";
|
||||
import { ModelTypeAvatarIcon } from "./icons/model-type-avatar-icon.component";
|
||||
import { ModelTypeSaberIcon } from "./icons/model-type-saber-icon.component";
|
||||
import { ModelTypePlatformIcon } from "./icons/model-type-platform-icon.component";
|
||||
import { ModelTypeBloqIcon } from "./icons/model-type-bloq-icon.component";
|
||||
|
||||
export type BsmIconType = BsvMapCharacteristic | (
|
||||
export type BsmIconType = BsvMapCharacteristic | MSModelType | (
|
||||
"settings"|"trash"|"favorite"|"folder"|"bsNote"|"check"|"three-dots"|"twitch"|"eye"|"play"|"checkCircleIcon"|"discord"|"info"|
|
||||
"terminal"|"desktop"|"oculus"|"add"|"cross"|"task"|"github"|"close"|"thumbUpFill"|"timerFill"|"pause"|"twitter"|"sync"|
|
||||
"copy"|"steam"|"edit"|"export"|"patreon"|"search"|"bsMapDifficulty"|"link"|"unlink"|"download"|"filter"|"mee6"|"volume-up"|"volume-off"|"volume-down"|
|
||||
@@ -62,6 +67,8 @@ export type BsmIconType = BsvMapCharacteristic | (
|
||||
|
||||
export const BsmIcon = memo(({className, icon, style}: {className?: string, icon: BsmIconType, style?: CSSProperties}) => {
|
||||
|
||||
// TODO : Very ugly very messy, need to find a better way to do this
|
||||
|
||||
const renderIcon = () => {
|
||||
if(icon === "settings"){ return <SettingIcon className={className} style={style}/> }
|
||||
if(icon === "trash"){ return <TrashIcon className={className} style={style}/> }
|
||||
@@ -115,6 +122,10 @@ export const BsmIcon = memo(({className, icon, style}: {className?: string, icon
|
||||
if(icon === "volume-up"){ return <VolumeUpIcon className={className} style={style}/> }
|
||||
if(icon === "volume-down"){ return <VolumeDownIcon className={className} style={style}/> }
|
||||
if(icon === "volume-off"){ return <VolumeOffIcon className={className} style={style}/> }
|
||||
if(icon === MSModelType.Avatar){ return <ModelTypeAvatarIcon className={className} style={style}/> }
|
||||
if(icon === MSModelType.Saber){ return <ModelTypeSaberIcon className={className} style={style}/> }
|
||||
if(icon === MSModelType.Platfrom){ return <ModelTypePlatformIcon className={className} style={style}/> }
|
||||
if(icon === MSModelType.Bloq){ return <ModelTypeBloqIcon className={className} style={style}/> }
|
||||
return <TrashIcon className={className} style={style}/>
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
export function ModelTypeAvatarIcon(props: {className?: string, style?: CSSProperties}) {
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" {...props} fill="currentColor">
|
||||
<path d="M64 416L168.6 180.7c15.3-34.4 40.3-63.5 72-83.7l146.9-94c3-1.9 6.5-2.9 10-2.9C407.7 0 416 8.3 416 18.6v1.6c0 2.6-.5 5.1-1.4 7.5L354.8 176.9c-1.9 4.7-2.8 9.7-2.8 14.7c0 5.5 1.2 11 3.4 16.1L448 416H240.9l11.8-35.4 40.4-13.5c6.5-2.2 10.9-8.3 10.9-15.2s-4.4-13-10.9-15.2l-40.4-13.5-13.5-40.4C237 276.4 230.9 272 224 272s-13 4.4-15.2 10.9l-13.5 40.4-40.4 13.5C148.4 339 144 345.1 144 352s4.4 13 10.9 15.2l40.4 13.5L207.1 416H64zM279.6 141.5c-1.1-3.3-4.1-5.5-7.6-5.5s-6.5 2.2-7.6 5.5l-6.7 20.2-20.2 6.7c-3.3 1.1-5.5 4.1-5.5 7.6s2.2 6.5 5.5 7.6l20.2 6.7 6.7 20.2c1.1 3.3 4.1 5.5 7.6 5.5s6.5-2.2 7.6-5.5l6.7-20.2 20.2-6.7c3.3-1.1 5.5-4.1 5.5-7.6s-2.2-6.5-5.5-7.6l-20.2-6.7-6.7-20.2zM32 448H480c17.7 0 32 14.3 32 32s-14.3 32-32 32H32c-17.7 0-32-14.3-32-32s14.3-32 32-32z"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
export function ModelTypeBloqIcon(props: {className?: string, style?: CSSProperties}) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="Bloq" viewBox="0 0 300 300" {...props} fill="currentColor">
|
||||
<path d="M264,0H36.05A36,36,0,0,0,0,36.05V264A36,36,0,0,0,36.05,300H264A36,36,0,0,0,300,264V36.05A36,36,0,0,0,264,0Zm2.38,52.68a5.86,5.86,0,0,1-3.53,5.37L152.08,106a5.87,5.87,0,0,1-4.7,0L39.19,58.07a5.87,5.87,0,0,1-3.48-5.35V36.46a5.85,5.85,0,0,1,5.86-5.85H260.48a5.85,5.85,0,0,1,5.85,5.85Z"></path>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
export function ModelTypePlatformIcon(props: {className?: string, style?: CSSProperties}) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" {...props} fill="currentColor">
|
||||
<path d="M0 80c0 26.5 21.5 48 48 48H64v64h64V128h96v64h64V128h96v64h64V128h16c26.5 0 48-21.5 48-48V13.4C512 6 506 0 498.6 0c-1.7 0-3.4 .3-5 1l-49 19.6C425.7 28.1 405.5 32 385.2 32H126.8c-20.4 0-40.5-3.9-59.4-11.4L18.4 1c-1.6-.6-3.3-1-5-1C6 0 0 6 0 13.4V80zM64 288V480c0 17.7 14.3 32 32 32s32-14.3 32-32V288H384V480c0 17.7 14.3 32 32 32s32-14.3 32-32V288h32c17.7 0 32-14.3 32-32s-14.3-32-32-32H32c-17.7 0-32 14.3-32 32s14.3 32 32 32H64z"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
export function ModelTypeSaberIcon(props: {className?: string, style?: CSSProperties}) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512" {...props} fill="currentColor">
|
||||
<path d="M234.7 42.7L197 56.8c-3 1.1-5 4-5 7.2s2 6.1 5 7.2l37.7 14.1L248.8 123c1.1 3 4 5 7.2 5s6.1-2 7.2-5l14.1-37.7L315 71.2c3-1.1 5-4 5-7.2s-2-6.1-5-7.2L277.3 42.7 263.2 5c-1.1-3-4-5-7.2-5s-6.1 2-7.2 5L234.7 42.7zM46.1 395.4c-18.7 18.7-18.7 49.1 0 67.9l34.6 34.6c18.7 18.7 49.1 18.7 67.9 0L529.9 116.5c18.7-18.7 18.7-49.1 0-67.9L495.3 14.1c-18.7-18.7-49.1-18.7-67.9 0L46.1 395.4zM484.6 82.6l-105 105-23.3-23.3 105-105 23.3 23.3zM7.5 117.2C3 118.9 0 123.2 0 128s3 9.1 7.5 10.8L64 160l21.2 56.5c1.7 4.5 6 7.5 10.8 7.5s9.1-3 10.8-7.5L128 160l56.5-21.2c4.5-1.7 7.5-6 7.5-10.8s-3-9.1-7.5-10.8L128 96 106.8 39.5C105.1 35 100.8 32 96 32s-9.1 3-10.8 7.5L64 96 7.5 117.2zm352 256c-4.5 1.7-7.5 6-7.5 10.8s3 9.1 7.5 10.8L416 416l21.2 56.5c1.7 4.5 6 7.5 10.8 7.5s9.1-3 10.8-7.5L480 416l56.5-21.2c4.5-1.7 7.5-6 7.5-10.8s-3-9.1-7.5-10.8L480 352l-21.2-56.5c-1.7-4.5-6-7.5-10.8-7.5s-9.1 3-10.8 7.5L416 352l-56.5 21.2z"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -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] 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 shrink-0">
|
||||
<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>
|
||||
|
||||
@@ -7,6 +7,8 @@ import { BSLauncherService, LaunchMods } from "renderer/services/bs-launcher.ser
|
||||
import { ConfigurationService } from "renderer/services/configuration.service";
|
||||
import { BSVersion } from "shared/bs-version.interface"
|
||||
import { LaunchModToogle } from "./launch-mod-toogle.component";
|
||||
import BSLogo from '../../../../../../assets/images/apngs/bs-logo.png';
|
||||
import { BsmImage } from "renderer/components/shared/bsm-image.component";
|
||||
|
||||
type Props = {version: BSVersion};
|
||||
|
||||
@@ -51,6 +53,10 @@ export function LaunchSlide({version}: Props) {
|
||||
|
||||
return (
|
||||
<div className="w-full shrink-0 items-center relative flex flex-col justify-start">
|
||||
<div className="flex flex-col gap-3 justify-center items-center mb-5">
|
||||
<BsmImage className='relative object-cover h-28' image={BSLogo}/>
|
||||
<h1 className='relative text-4xl font-bold italic -top-3'>{version.name ? `${version.BSVersion} - ${version.name}` : version.BSVersion}</h1>
|
||||
</div>
|
||||
<div className='grid grid-flow-col gap-6'>
|
||||
{!version.oculus && <LaunchModToogle infoText="pages.version-viewer.launch-mods.oculus-description" icon='oculus' onClick={() => setMode(LaunchMods.OCULUS_MOD, !oculusMode)} active={oculusMode} text="pages.version-viewer.launch-mods.oculus"/>}
|
||||
<LaunchModToogle infoText="pages.version-viewer.launch-mods.desktop-description" icon='desktop' onClick={() => setMode(LaunchMods.DESKTOP_MOD, !desktopMode)} active={desktopMode} text="pages.version-viewer.launch-mods.desktop"/>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useState } from "react";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { useObservable } from "./use-observable.hook";
|
||||
import { useConstant } from "./use-constant.hook";
|
||||
|
||||
export function useBehaviorSubject<T>(value: T): [T, BehaviorSubject<T>]{
|
||||
const [subject] = useState(new BehaviorSubject(value));
|
||||
const subject = useConstant(() => new BehaviorSubject(value));
|
||||
const subjectValue = useObservable(subject);
|
||||
|
||||
return [subjectValue, subject];
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { BSVersion } from 'shared/bs-version.interface';
|
||||
import { useState } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import BSLogo from '../../../assets/images/apngs/bs-logo.png';
|
||||
import { TabNavBar } from 'renderer/components/shared/tab-nav-bar.component';
|
||||
import { BsmDropdownButton } from 'renderer/components/shared/bsm-dropdown-button.component';
|
||||
import { BsmImage } from 'renderer/components/shared/bsm-image.component';
|
||||
@@ -77,8 +76,6 @@ export function VersionViewer() {
|
||||
<>
|
||||
<BsmImage className="absolute w-full h-full top-0 left-0 object-cover" image={state.ReleaseImg || DefautVersionImage} errorImage={DefautVersionImage}/>
|
||||
<div className="relative flex items-center flex-col w-full h-full text-gray-200 backdrop-blur-lg">
|
||||
<BsmImage className='relative object-cover h-28' image={BSLogo}/>
|
||||
<h1 className='relative text-4xl font-bold italic -top-3'>{state.name ? `${state.BSVersion} - ${state.name}` : state.BSVersion}</h1>
|
||||
<TabNavBar className='my-3' tabIndex={currentTabIndex} tabsText={["misc.launch", "misc.maps", "Models", "misc.mods"]} onTabChange={(i : number) => setCurrentTabIndex(i)}/>
|
||||
<div className='mt-2 w-full min-h-0 grow flex transition-transform duration-300' style={{transform: `translate(${-(currentTabIndex * 100)}%, 0)`}}>
|
||||
<LaunchSlide version={state}/>
|
||||
|
||||
@@ -117,7 +117,7 @@ export class MapsManagerService {
|
||||
|
||||
if(!resFile.success){ return; }
|
||||
|
||||
const exportProgress$: Observable<ProgressionInterface> = await this.ipcService.sendV2<ArchiveProgress, {version: BSVersion, maps: BsmLocalMap[], outPath: string}>("export-maps", {args: {version, maps, outPath: resFile.data}}).pipe(
|
||||
const exportProgress$: Observable<ProgressionInterface> = this.ipcService.sendV2<ArchiveProgress, {version: BSVersion, maps: BsmLocalMap[], outPath: string}>("export-maps", {args: {version, maps, outPath: resFile.data}}).pipe(
|
||||
map(p => {
|
||||
return { progression: (p.prossesedFiles / p.totalFiles) * 100, label: `${p.prossesedFiles} / ${p.totalFiles}` } as ProgressionInterface
|
||||
})
|
||||
@@ -128,6 +128,7 @@ export class MapsManagerService {
|
||||
await exportProgress$.toPromise().catch(e => {
|
||||
this.notifications.notifyError({title: "notifications.types.error", desc: e.message});
|
||||
}).then(() => {
|
||||
// TODO TRANSLATE
|
||||
this.notifications.notifySuccess({title: "Export terminé 🎉", duration: 3000});
|
||||
this.progressBar.complete();
|
||||
this.progressBar.hide(true);
|
||||
|
||||
@@ -2,18 +2,26 @@ 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/models/constants";
|
||||
import { Observable, distinctUntilChanged, lastValueFrom, mergeMap, share } from "rxjs";
|
||||
import { Observable, distinctUntilChanged, lastValueFrom, map, 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";
|
||||
import { ProgressBarService } from "../progress-bar.service";
|
||||
import { OpenSaveDialogOption } from "shared/models/os/dialog.model";
|
||||
import { ProgressionInterface } from "shared/models/progress-bar";
|
||||
import { ArchiveProgress } from "shared/models/archive.interface";
|
||||
import { NotificationService } from "../notification.service";
|
||||
import { ConfigurationService } from "../configuration.service";
|
||||
|
||||
export class ModelsManagerService {
|
||||
|
||||
private static instance: ModelsManagerService;
|
||||
|
||||
private static readonly REMEMBER_CHOICE_DELETE_MODEL_KEY = "not-confirm-delete-model"
|
||||
|
||||
public static getInstance(): ModelsManagerService{
|
||||
if(!ModelsManagerService.instance){ ModelsManagerService.instance = new ModelsManagerService(); }
|
||||
return ModelsManagerService.instance;
|
||||
@@ -22,11 +30,17 @@ export class ModelsManagerService {
|
||||
private readonly ipc: IpcService;
|
||||
private readonly versionFolderLinked: VersionFolderLinkerService;
|
||||
private readonly modalService: ModalService;
|
||||
private readonly progressBar: ProgressBarService;
|
||||
private readonly notifications: NotificationService;
|
||||
private readonly config: ConfigurationService;
|
||||
|
||||
private constructor(){
|
||||
this.ipc = IpcService.getInstance();
|
||||
this.versionFolderLinked = VersionFolderLinkerService.getInstance();
|
||||
this.modalService = ModalService.getInsance();
|
||||
this.progressBar = ProgressBarService.getInstance();
|
||||
this.notifications = NotificationService.getInstance();
|
||||
this.config = ConfigurationService.getInstance();
|
||||
}
|
||||
|
||||
public isModelsLinked(version: BSVersion, type: MSModelType): Promise<boolean>{
|
||||
@@ -69,6 +83,54 @@ export class ModelsManagerService {
|
||||
return this.ipc.sendV2<Progression<BsmLocalModel[]>>("get-version-models", { args: { version, type } });
|
||||
}
|
||||
|
||||
public async exportModels(models: BsmLocalModel[], version?: BSVersion){
|
||||
if(!this.progressBar.require()){ return; }
|
||||
|
||||
const resFile = await this.ipc.send<string, OpenSaveDialogOption>("save-file", {args: {
|
||||
filename: version ? `${version.name ?? version.BSVersion} Models` : "Models",
|
||||
filters: [{name: "zip", extensions: ["zip"]}]
|
||||
}});
|
||||
|
||||
if(!resFile.success){ return; }
|
||||
|
||||
const exportProgress$: Observable<ProgressionInterface> = this.ipc.sendV2<ArchiveProgress, {version: BSVersion, models: BsmLocalModel[], outPath: string}>("export-models", {args: {version, models, outPath: resFile.data}}).pipe(
|
||||
map(p => {
|
||||
return { progression: (p.prossesedFiles / p.totalFiles) * 100, label: `${p.prossesedFiles} / ${p.totalFiles}` } as ProgressionInterface
|
||||
})
|
||||
);
|
||||
|
||||
this.progressBar.show(exportProgress$, true);
|
||||
|
||||
lastValueFrom(exportProgress$).catch(e => {
|
||||
this.notifications.notifyError({title: "notifications.types.error"});
|
||||
}).then(() => {
|
||||
// TODO TRANSLATE
|
||||
this.notifications.notifySuccess({title: "TODO TRANSLATE SUCCESS", duration: 3000});
|
||||
this.progressBar.complete();
|
||||
this.progressBar.hide(true);
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
public async deleteModels(models: BsmLocalModel[], version?: BSVersion){
|
||||
|
||||
const types = Array.from(new Set(models.map(m => m.type)));
|
||||
|
||||
const modelsLinked = await (async () => {
|
||||
for(const type of types){
|
||||
if(!(await this.isModelsLinked(version, type))){ continue; }
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})();
|
||||
|
||||
|
||||
const askModal = models.length > 1 || !this.config.get<boolean>(ModelsManagerService.REMEMBER_CHOICE_DELETE_MODEL_KEY);
|
||||
|
||||
console.log(modelsLinked, askModal);
|
||||
|
||||
}
|
||||
|
||||
public isDeepLinksEnabled(): Promise<boolean>{
|
||||
return this.ipc.send<boolean>("is-models-deep-links-enabled").then(res => (
|
||||
res.success ? res.data : false
|
||||
|
||||
@@ -21,4 +21,6 @@ export const MODEL_FILE_EXTENSIONS = {
|
||||
[MSModelType.Bloq]: ".bloq",
|
||||
[MSModelType.Platfrom]: ".plat",
|
||||
[MSModelType.Saber]: ".saber"
|
||||
}
|
||||
}
|
||||
|
||||
export const MODEL_TYPES = Array.from(Object.values(MSModelType));
|
||||
Reference in New Issue
Block a user