[feature-136] Models panel + models linkage + models linkage modals + some cleaning

This commit is contained in:
MathieuG-P
2023-04-29 15:24:45 +02:00
parent f602547ca4
commit bdbdcfb1b4
21 changed files with 649 additions and 55 deletions
@@ -4,7 +4,7 @@ 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/model-saber/model-saber.model";
import { BSVersion } from "shared/bs-version.interface";
import { BSLocalVersionService } from "../bs-local-version.service";
import path from "path";
@@ -12,6 +12,7 @@ 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";
export class LocalModelsManagerService {
@@ -26,13 +27,6 @@ export class LocalModelsManagerService {
ModelSaber: "modelsaber",
};
private readonly MODEL_TYPE_FOLDER: Record<Exclude<MSModelType, "misc">, string> = {
avatar: "CustomAvatars",
bloq: "CustomNotes",
platform: "CustomPlatforms",
saber: "CustomSabers"
}
private readonly deepLink: DeepLinkService;
private readonly utils: UtilsService;
private readonly windows: WindowManagerService;
@@ -70,10 +64,9 @@ export class LocalModelsManagerService {
private async getModelFolderPath(type: MSModelType, version?: BSVersion): Promise<string>{
if(!version){ throw "will be implemented whith models management" }
if(type === "misc"){ throw "model type not supported"; }
const versionPath = await this.localVersion.getVersionPath(version);
const modelFolderPath = path.join(versionPath, this.MODEL_TYPE_FOLDER[type]);
const modelFolderPath = path.join(versionPath, MODEL_TYPE_FOLDERS[type]);
await ensureFolderExist(modelFolderPath);
@@ -0,0 +1,32 @@
import { useState } from "react"
import { BsmButton } from "renderer/components/shared/bsm-button.component"
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 BeatRunning from '../../../../../../assets/images/apngs/beat-running.png'
export const LinkModelsModal: ModalComponent<boolean, MSModelType> = ({resolver, data}) => {
const t = useTranslation();
const [keepMaps, setKeepMaps] = useState(true)
return (
<form className="text-gray-800 dark:text-gray-200">
<h1 className="text-3xl uppercase tracking-wide w-full text-center">{t(`modals.models.link-models.${data}.title`)}</h1>
<BsmImage className="mx-auto h-24" image={BeatRunning} />
<p className="max-w-sm w-full">{t(`modals.models.link-models.${data}.desc`)}</p>
<p className="max-w-sm w-full text-sm italic mt-2">{t(`modals.models.link-models.${data}.info`)}</p>
<div className="relative h-5 flex my-3 items-center">
<BsmCheckbox className="h-full aspect-square relative bg-inherit mr-1.5 z-[1]" checked={keepMaps} onChange={setKeepMaps}/>
<span className="text-sm mb-0.5 cursor-help" title={t(`modals.models.link-models.${data}.keep-models.title`)}>{t(`modals.models.link-models.${data}.keep-models.label`)}</span>
</div>
<div className="grid grid-flow-col grid-cols-2 gap-4 mt-4">
<BsmButton typeColor="cancel" className="rounded-md text-center transition-all" onClick={() => resolver({exitCode: ModalExitCode.CANCELED})} withBar={false} text="misc.cancel"/>
<BsmButton typeColor="primary" className="rounded-md text-center transition-all" onClick={() => resolver({exitCode: ModalExitCode.COMPLETED, data: keepMaps})} withBar={false} text={`modals.models.link-models.${data}.valid-btn`}/>
</div>
</form>
)
}
@@ -0,0 +1,30 @@
import { useState } from "react";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
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 BeatConflict from '../../../../../../assets/images/apngs/beat-conflict.png'
import { MSModelType } from "shared/models/model-saber/model-saber.model";
export const UnlinkModelsModal: ModalComponent<boolean, MSModelType> = ({resolver, data}) => {
const t = useTranslation();
const [keepMaps, setKeepMaps] = useState(true);
return (
<form className="text-gray-800 dark:text-gray-200">
<h1 className="text-3xl uppercase tracking-wide w-full text-center">{t(`modals.models.unlink-models.${data}.title`)}</h1>
<BsmImage className="mx-auto h-24" image={BeatConflict} />
<p className="max-w-sm w-full">{t(`modals.models.unlink-models.${data}.desc`)}</p>
<div className="relative h-5 flex my-3 items-center">
<BsmCheckbox className="h-full aspect-square relative bg-inherit mr-1.5 z-[1]" checked={keepMaps} onChange={setKeepMaps}/>
<span className="text-sm mb-0.5 cursor-help" title={t(`modals.models.unlink-models.${data}.keep-models.title`)}>{t(`modals.models.unlink-models.${data}.keep-models.label`)}</span>
</div>
<div className="grid grid-flow-col grid-cols-2 gap-4 mt-4">
<BsmButton typeColor="cancel" className="rounded-md text-center transition-all" onClick={() => resolver({exitCode: ModalExitCode.CANCELED})} withBar={false} text="misc.cancel"/>
<BsmButton typeColor="error" className="rounded-md text-center transition-all" onClick={() => resolver({exitCode: ModalExitCode.COMPLETED, data: keepMaps})} withBar={false} text={`modals.models.unlink-models.${data}.valid-btn`}/>
</div>
</form>
)
}
@@ -1,11 +1,9 @@
import Tippy from "@tippyjs/react";
import { Variants } from "framer-motion";
import { version } from "os";
import { useEffect, useState } from "react";
import { LinkButton } from "renderer/components/maps-mangement-components/link-button.component";
import { BsmBasicSpinner } from "renderer/components/shared/bsm-basic-spinner/bsm-basic-spinner.component";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { BsmIcon } from "renderer/components/svgs/bsm-icon.component";
import { useObservable } from "renderer/hooks/use-observable.hook";
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
import { useTranslation } from "renderer/hooks/use-translation.hook";
@@ -0,0 +1,26 @@
import { BSVersion } from "shared/bs-version.interface";
import { useState } from "react";
import { ModelsTabsNavbar } from "./models-tabs-navbar.component";
export function ModelsPanel({version}: {version?: BSVersion}) {
const [modelTypeTab, setModelTypeTab] = useState<ModelTabType>(ModelTabType.Avatars);
return (
<div className="w-full h-full flex flex-col items-center justify-center">
<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>
</div>
)
}
enum ModelTabType {
Avatars = 0,
Sabers = 1,
Platforms = 2,
Bloqs = 3,
}
@@ -0,0 +1,92 @@
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 { 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 { 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"
type Props = {
version?: BSVersion
tabIndex: number,
onTabChange: (index: number) => void
}
export function ModelsTabsNavbar({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}/>
}
return (
<TabNavBar className="!rounded-none shadow-sm" tabsText={
["Avatars", "Sabers", "Platforms", "Bloqs"]
} tabIndex={tabIndex} onTabChange={onTabChange} renderTab={renderTab}/>
)
}
type TabProps = {
version?: BSVersion,
modelType: MSModelType,
index: number,
onLink?: (type: MSModelType) => void,
onUnlink?: (type: MSModelType) => void,
} & DetailedHTMLProps<React.HTMLAttributes<HTMLLIElement>, HTMLLIElement>;
function ModelTab({version, modelType, onClick, onLink, onUnlink}: TabProps){
const modelsManager = useConstant(() => ModelsManagerService.getInstance());
const [modelsAreLinked, setModelsAreLinked] = useState(false);
const [linkBtnDisabled, setLinkBtnDisabled] = useState(false);
useOnUpdate(() => {
if(!version){ return; }
const sub = modelsManager.$modelsLinkingPending(version, modelType).subscribe(async (pending) => {
if(pending){ return setLinkBtnDisabled(() => pending); }
const modelsLinked = await modelsManager.isModelsLinked(version, modelType);
setModelsAreLinked(() => modelsLinked);
setLinkBtnDisabled(() => pending);
});
return () => {
sub.unsubscribe();
}
}, [version]);
const linkModels = () => modelsManager.linkModels(modelType, version).then(() => onLink?.(modelType));
const unlinkModels = () => modelsManager.unlinkModels(modelType, version).then(() => onUnlink?.(modelType));
const onClickLink = () => {
if(!version){ return Promise.resolve(); }
if(modelsAreLinked){ return unlinkModels(); }
return linkModels();
}
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">
{!!version && (
<LinkButton
variants={{ hover: {rotate: 22.5}, tap: {rotate: 45} }}
disabled={linkBtnDisabled}
whileHover="hover"
whileTap="tap"
initial={{rotate: 0}}
className="block p-0.5 h-[calc(100%-5px)] aspect-square blur-0 hover:brightness-75"
linked={modelsAreLinked}
title={"aaa"}
onClick={onClickLink}
/>
)}
</div>
</li>
)
}
+13
View File
@@ -0,0 +1,13 @@
import { useRef } from "react"
type ResultBox<T> = { v: T }
export function useConstant<T>(fn: () => T): T {
const ref = useRef<ResultBox<T>>()
if (!ref.current) {
ref.current = { v: fn() }
}
return ref.current.v
}
+7
View File
@@ -0,0 +1,7 @@
import { useEffect, EffectCallback, DependencyList } from "react";
export function useOnUpdate(func: EffectCallback, deps: DependencyList = []) {
useEffect(() => {
func();
}, deps);
}
@@ -28,7 +28,7 @@ import beatleaderIcon from "../../../assets/images/third-party-icons/beat-leader
import Tippy from '@tippyjs/react';
import { MapsManagerService } from "renderer/services/maps-manager.service";
import { PlaylistsManagerService } from "renderer/services/playlists-manager.service";
import { ModelsManagerService } from "renderer/services/models-manager.service";
import { ModelsManagerService } from "renderer/services/models-management/models-manager.service";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { VersionFolderLinkerService } from "renderer/services/version-folder-linker.service";
@@ -16,6 +16,7 @@ import { ModsSlide } from 'renderer/components/version-viewer/slides/mods/mods-s
import { UninstallModal } from 'renderer/components/modal/modal-types/uninstall-modal.component';
import { MapsPlaylistsPanel } from 'renderer/components/maps-mangement-components/maps-playlists-panel.component';
import { ShareFoldersModal } from 'renderer/components/modal/modal-types/share-folders-modal.component';
import { ModelsPanel } from 'renderer/components/models-management/models-panel.component';
export function VersionViewer() {
@@ -78,12 +79,15 @@ export function VersionViewer() {
<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", "misc.mods"]} onTabChange={(i : number) => setCurrentTabIndex(i)}/>
<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}/>
<div className="w-full shrink-0 px-3 pb-3 flex flex-col items-center">
<MapsPlaylistsPanel version={state}/>
</div>
<div className="w-full shrink-0 px-3 pb-3 flex flex-col items-center">
<ModelsPanel version={state}/>
</div>
<ModsSlide version={state} onDisclamerDecline={handleModsDisclaimerDecline}/>
</div>
</div>
@@ -159,9 +159,7 @@ export class MapsManagerService {
}
public $mapsLinkingPending(version: BSVersion): Observable<boolean>{
return this.linker.queue$.pipe(mergeMap(async queue => {
return queue.some(q => q.relativeFolder.includes(MapsManagerService.RELATIVE_MAPS_FOLDER) && equal(q.version, version));
}), distinctUntilChanged());
return this.linker.$isVersionFolderPending(version, MapsManagerService.RELATIVE_MAPS_FOLDER);
}
}
@@ -0,0 +1,82 @@
import { MSModelType } from "shared/models/model-saber/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 { 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";
export class ModelsManagerService {
private static instance: ModelsManagerService;
public static getInstance(): ModelsManagerService{
if(!ModelsManagerService.instance){ ModelsManagerService.instance = new ModelsManagerService(); }
return ModelsManagerService.instance;
}
private readonly ipc: IpcService;
private readonly versionFolderLinked: VersionFolderLinkerService;
private readonly modalService: ModalService;
private constructor(){
this.ipc = IpcService.getInstance();
this.versionFolderLinked = VersionFolderLinkerService.getInstance();
this.modalService = ModalService.getInsance();
}
public isModelsLinked(version: BSVersion, type: MSModelType): Promise<boolean>{
return lastValueFrom(this.versionFolderLinked.isVersionFolderLinked(version, MODEL_TYPE_FOLDERS[type]));
}
public $modelsLinkingPending(version: BSVersion, type: MSModelType): Observable<boolean>{
return this.versionFolderLinked.$isVersionFolderPending(version, MODEL_TYPE_FOLDERS[type]);
}
public async linkModels(type: MSModelType, version?: BSVersion): Promise<boolean>{
const res = await this.modalService.openModal(LinkModelsModal, type);
if(res.exitCode !== ModalExitCode.COMPLETED){ return null; }
return this.versionFolderLinked.linkVersionFolder({
version,
relativeFolder: MODEL_TYPE_FOLDERS[type],
type: VersionLinkerActionType.Link,
options: { keepContents: res.data !== false }
});
}
public async unlinkModels(type: MSModelType, version?: BSVersion): Promise<boolean>{
const res = await this.modalService.openModal(UnlinkModelsModal, type);
if(res.exitCode !== ModalExitCode.COMPLETED){ return null; }
return this.versionFolderLinked.unlinkVersionFolder({
version,
relativeFolder: MODEL_TYPE_FOLDERS[type],
type: VersionLinkerActionType.Unlink,
options: { keepContents: res.data !== false }
});
}
public isDeepLinksEnabled(): Promise<boolean>{
return this.ipc.send<boolean>("is-models-deep-links-enabled").then(res => (
res.success ? res.data : false
));
}
public async enableDeepLink(): Promise<boolean>{
const res = await this.ipc.send<boolean>("register-models-deep-link");
return res.success ? res.data : false;
}
public async disableDeepLink(): Promise<boolean>{
const res = await this.ipc.send<boolean>("unregister-models-deep-link");
return res.success ? res.data : false;
}
}
@@ -1,34 +0,0 @@
import { IpcService } from "./ipc.service";
export class ModelsManagerService {
private static instance: ModelsManagerService;
public static getInstance(): ModelsManagerService{
if(!ModelsManagerService.instance){ ModelsManagerService.instance = new ModelsManagerService(); }
return ModelsManagerService.instance;
}
private readonly ipc: IpcService;
private constructor(){
this.ipc = IpcService.getInstance();
}
public isDeepLinksEnabled(): Promise<boolean>{
return this.ipc.send<boolean>("is-models-deep-links-enabled").then(res => (
res.success ? res.data : false
));
}
public async enableDeepLink(): Promise<boolean>{
const res = await this.ipc.send<boolean>("register-models-deep-link");
return res.success ? res.data : false;
}
public async disableDeepLink(): Promise<boolean>{
const res = await this.ipc.send<boolean>("unregister-models-deep-link");
return res.success ? res.data : false;
}
}
@@ -1,9 +1,10 @@
import { LinkOptions, UnlinkOptions } from "main/services/folder-linker.service";
import { map, distinctUntilChanged, filter } from "rxjs/operators";
import { map, distinctUntilChanged, filter, mergeMap, share } from "rxjs/operators";
import { BehaviorSubject, Observable } from "rxjs";
import { BSVersion } from "shared/bs-version.interface";
import { IpcService } from "./ipc.service";
import { ProgressBarService } from "./progress-bar.service";
import equal from "fast-deep-equal";
export class VersionFolderLinkerService{
@@ -137,6 +138,12 @@ export class VersionFolderLinkerService{
return this.ipcService.sendV2("relink-all-versions-folders");
}
public $isVersionFolderPending(version: BSVersion, relativeFolder: string): Observable<boolean>{
return this.queue$.pipe(mergeMap(async queue => {
return queue.some(q => q.relativeFolder.includes(relativeFolder) && equal(q.version, version));
}), distinctUntilChanged(), share());
}
public get currentAction$(): Observable<VersionLinkerAction>{
return this._queue$.pipe(map(actions => actions.at(0)), distinctUntilChanged());
}
+1 -1
View File
@@ -17,7 +17,7 @@ import 'tippy.js/dist/tippy.css';
import 'tippy.js/animations/shift-away-subtle.css';
import { MapsManagerService } from "renderer/services/maps-manager.service";
import { PlaylistsManagerService } from "renderer/services/playlists-manager.service";
import { ModelsManagerService } from "renderer/services/models-manager.service";
import { ModelsManagerService } from "renderer/services/models-management/models-manager.service";
import { NotificationService } from "renderer/services/notification.service";
import { timer } from "rxjs";
import { ConfigurationService } from "renderer/services/configuration.service";
@@ -17,8 +17,18 @@ export interface MSModel {
date: string
}
export type MSModelType = "saber"|"platform"|"bloq"|"misc"|"avatar";
export type MSModelPlatform = "pc"|"quest"|"all";
export enum MSModelType {
Avatar = "avatar",
Saber = "saber",
Platfrom = "platform",
Bloq = "bloq"
};
export enum MSModelPlatform {
PC = "pc",
QUEST = "quest",
ALL = "all"
};
export interface MSGetQuery {
type?: MSModelType;
@@ -0,0 +1,8 @@
import { MSModelType } from "./model-saber.model";
export const MODEL_TYPE_FOLDERS = {
[MSModelType.Avatar]: "CustomAvatars",
[MSModelType.Bloq]: "CustomNotes",
[MSModelType.Platfrom]: "CustomPlatforms",
[MSModelType.Saber]: "CustomSabers"
}