[feature-136] Add model types icons + export models + init delete models + init searchbar

This commit is contained in:
MathieuG-P
2023-05-08 21:12:35 +02:00
parent d1dc7bbca0
commit a366c07128
18 changed files with 238 additions and 46 deletions
+8
View File
@@ -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
+3 -1
View File
@@ -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));