mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
[chore] update maps and playlists panel UI for coherence
This commit is contained in:
@@ -7,7 +7,7 @@ import { InstallationLocationService } from "../installation-location.service";
|
||||
import { UtilsService } from "../utils.service";
|
||||
import crypto from "crypto";
|
||||
import { lstatSync } from "fs";
|
||||
import { copy, createReadStream, ensureDir, realpath, unlink } from "fs-extra";
|
||||
import { copy, createReadStream, ensureDir, pathExists, realpath, unlink } from "fs-extra";
|
||||
import StreamZip from "node-stream-zip";
|
||||
import { RequestService } from "../request.service";
|
||||
import sanitize from "sanitize-filename";
|
||||
@@ -255,10 +255,12 @@ export class LocalMapsManagerService {
|
||||
const mapFolderName = sanitize(`${map.id}-${map.name}`);
|
||||
const mapsFolder = await this.getMapsFolderPath(version);
|
||||
|
||||
const installedMap = await this.loadMapInfoFromPath(path.join(mapsFolder, mapFolderName)).catch(e => {
|
||||
log.error("loadMapInfoFromPath", e);
|
||||
return null;
|
||||
});
|
||||
const mapPath = path.join(mapsFolder, mapFolderName);
|
||||
|
||||
const installedMap = await pathExists(mapPath).then(exists => {
|
||||
if(!exists){ return null; }
|
||||
return this.loadMapInfoFromPath(mapPath);
|
||||
}).catch(() => null);
|
||||
|
||||
if(map.versions.every(version => version.hash === installedMap?.hash)) {
|
||||
return installedMap;
|
||||
@@ -270,8 +272,6 @@ export class LocalMapsManagerService {
|
||||
throw `Cannot download ${zipUrl}`;
|
||||
}
|
||||
|
||||
const mapPath = path.join(mapsFolder, mapFolderName);
|
||||
|
||||
await ensureFolderExist(mapPath);
|
||||
|
||||
await zip.extract(null, mapPath);
|
||||
|
||||
@@ -1,48 +1,60 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { forwardRef } from "react";
|
||||
import { useState } from "react";
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import { BsmIcon } from "../svgs/bsm-icon.component";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { FolderLinkState } from "renderer/services/version-folder-linker.service";
|
||||
import { Observable } from "rxjs";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
|
||||
type Props = {
|
||||
export type LinkBtnProps = {
|
||||
className?: string;
|
||||
title?: string;
|
||||
linked?: boolean;
|
||||
disabled?: boolean;
|
||||
state$: Observable<FolderLinkState>;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
export const LinkButton = motion(
|
||||
forwardRef((props: Props, ref) => {
|
||||
const t = useTranslation();
|
||||
const color = useThemeColor("first-color");
|
||||
export const LinkButton = ({className, title, state$, onClick}: LinkBtnProps) => {
|
||||
const t = useTranslation();
|
||||
|
||||
const state = useObservable(state$);
|
||||
const color = useThemeColor("first-color");
|
||||
const disabled = state === FolderLinkState.Processing || state === FolderLinkState.Pending;
|
||||
|
||||
const linkedColor = (() => {
|
||||
if (props.disabled) {
|
||||
return "orange";
|
||||
}
|
||||
if (props.linked) {
|
||||
const btnColor = () => {
|
||||
switch (state) {
|
||||
case FolderLinkState.Linked:
|
||||
return color;
|
||||
}
|
||||
return "red";
|
||||
})();
|
||||
case FolderLinkState.Pending:
|
||||
case FolderLinkState.Processing:
|
||||
return "orange";
|
||||
case FolderLinkState.Unlinked:
|
||||
return "red";
|
||||
default:
|
||||
return "red";
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={props.className}
|
||||
title={props.title ? t(props.title) : undefined}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
!props.disabled && props.onClick?.();
|
||||
}}
|
||||
style={{ pointerEvents: props.disabled ? "none" : "auto" }}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<span className="absolute top-0 left-0 h-full w-full rounded-full brightness-50 opacity-75 dark:opacity-20 dark:filter-none" style={{ backgroundColor: linkedColor }} />
|
||||
<BsmIcon className="p-1 absolute top-0 left-0 h-full w-full !bg-transparent -rotate-45 brightness-150" icon={props.linked ? "link" : "unlink"} style={{ color: linkedColor }} />
|
||||
</div>
|
||||
);
|
||||
})
|
||||
);
|
||||
const handleClick = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
!disabled && onClick?.();
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
variants={{ hover: { rotate: 22.5 }, tap: { rotate: 45 } }}
|
||||
whileHover="hover"
|
||||
whileTap="tap"
|
||||
initial={{ rotate: 0 }}
|
||||
className={className}
|
||||
title={title ? t(title) : null}
|
||||
onClick={handleClick}
|
||||
style={{ pointerEvents: disabled ? "none" : "auto" }}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<span className="absolute top-0 left-0 h-full w-full rounded-full brightness-50 opacity-75 dark:opacity-20 dark:filter-none" style={{ backgroundColor: btnColor() }} />
|
||||
<BsmIcon className="p-1 absolute top-0 left-0 h-full w-full !bg-transparent -rotate-45 brightness-150" icon={state === FolderLinkState.Linked ? "link" : "unlink"} style={{ color: btnColor() }} />
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -429,7 +429,7 @@ export const LocalMapsListPanel = forwardRef(({ version, className, filter, sear
|
||||
|
||||
return (
|
||||
<div ref={ref} className={className}>
|
||||
<VariableSizeList className="p-0 scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900" width="100%" height={listHeight} itemSize={() => 108} itemCount={preppedMaps.length} itemData={preppedMaps} layout="vertical" style={{ scrollbarGutter: "stable both-edges" }} itemKey={(i, data) => data[i].map(map => map.hash).join()}>
|
||||
<VariableSizeList className="scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900" width="100%" height={listHeight} itemSize={() => 108} itemCount={preppedMaps.length} itemData={preppedMaps} layout="vertical" style={{ scrollbarGutter: "stable both-edges" }} itemKey={(i, data) => data[i].map(map => map.hash).join()}>
|
||||
{props => <MapsRow maps={props.data[props.index]} style={props.style} selectedMaps$={selectedMaps$} onMapSelect={onMapSelected} onMapDelete={handleDelete} />}
|
||||
</VariableSizeList>
|
||||
</div>
|
||||
|
||||
@@ -191,8 +191,8 @@ export const MapItem = memo(({ hash, title, autor, songAutor, coverUrl, songUrl,
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative h-full w-full z-[1] rounded-md overflow-hidden -translate-x-1" ref={ref}>
|
||||
<BsmImage className="absolute top-0 left-0 w-full h-full -z-[1] object-cover" image={coverUrl} placeholder={defaultImage} errorImage={defaultImage} loading="lazy" />
|
||||
<div className="pt-1 pl-2 pr-7 top-0 left-0 w-full h-full bg-gray-600 bg-opacity-80 flex flex-col justify-between group-hover:bg-main-color-1 group-hover:bg-opacity-80">
|
||||
<BsmImage className="absolute top-0 left-0 w-full h-full -z-[1] object-cover saturate-200" image={coverUrl} placeholder={defaultImage} errorImage={defaultImage} loading="lazy" />
|
||||
<div className="pt-1 pl-2 pr-7 top-0 left-0 w-full h-full bg-neutral-600 bg-opacity-80 flex flex-col justify-between group-hover:bg-main-color-1 group-hover:bg-opacity-80">
|
||||
<h1 className="font-bold whitespace-nowrap text-ellipsis overflow-hidden w-full leading-5 tracking-wide text-lg" title={title}>
|
||||
<BsmLink className="hover:underline" href={mapUrl}>
|
||||
{title}
|
||||
@@ -240,7 +240,7 @@ export const MapItem = memo(({ hash, title, autor, songAutor, coverUrl, songUrl,
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute bg-light-main-color-1 dark:bg-main-color-3 top-0 h-full z-[1] w-[30px] -right-5 group-hover:right-0 transition-all">
|
||||
<div className="absolute bg-light-main-color-3 dark:bg-main-color-3 top-0 h-full z-[1] w-[30px] -right-5 group-hover:right-0 transition-all">
|
||||
<span className="absolute w-[10px] h-[10px] top-0 right-full bg-inherit" style={{ clipPath: 'path("M11 -1 L11 10 L10 10 A10 10 0 0 0 0 0 L0 -1 Z")' }} />
|
||||
<span className="absolute w-[10px] h-[10px] bottom-0 right-full bg-inherit" style={{ clipPath: 'path("M11 11 L11 0 L10 0 A10 10 0 0 1 0 10 L 0 11 Z")' }} />
|
||||
|
||||
|
||||
+39
-71
@@ -1,24 +1,18 @@
|
||||
import { DetailedHTMLProps, useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { TabNavBar } from "../shared/tab-nav-bar.component";
|
||||
import { LocalMapsListPanel } from "./local-maps-list-panel.component";
|
||||
import { BsmDropdownButton, DropDownItem } from "../shared/bsm-dropdown-button.component";
|
||||
import { FilterPanel } from "./filter-panel.component";
|
||||
import { MapFilter } from "shared/models/maps/beat-saver.model";
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import { MapsManagerService } from "renderer/services/maps-manager.service";
|
||||
import { motion, Variants } from "framer-motion";
|
||||
import { MapsDownloaderService } from "renderer/services/maps-downloader.service";
|
||||
import { BsmImage } from "../shared/bsm-image.component";
|
||||
import wipGif from "../../../../assets/images/gifs/wip.gif";
|
||||
import { OsDiagnosticService } from "renderer/services/os-diagnostic.service";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { BsmIcon } from "../svgs/bsm-icon.component";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { LinkButton } from "./link-button.component";
|
||||
import { debounceTime } from "rxjs/operators";
|
||||
import { VersionFolderLinkerService, VersionLinkerActionListener } from "renderer/services/version-folder-linker.service";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { BsContentTabPanel } from "../shared/bs-content-tab-panel/bs-content-tab-panel.component";
|
||||
import { BsmButton } from "../shared/bsm-button.component";
|
||||
|
||||
type Props = {
|
||||
version?: BSVersion;
|
||||
@@ -29,7 +23,6 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
|
||||
const mapsService = useService(MapsManagerService);
|
||||
const mapsDownloader = useService(MapsDownloaderService);
|
||||
const osDiagnostic = useService(OsDiagnosticService);
|
||||
const linker = useService(VersionFolderLinkerService);
|
||||
|
||||
const [tabIndex, setTabIndex] = useState(0);
|
||||
@@ -37,9 +30,6 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
const [mapSearch, setMapSearch] = useState("");
|
||||
const [playlistSearch, setPlaylistSearch] = useState("");
|
||||
const [mapsLinked, setMapsLinked] = useState(false);
|
||||
const [linkingPending, setLinkingPending] = useState(false);
|
||||
const isOnline = useObservable(osDiagnostic.isOnline$);
|
||||
const color = useThemeColor("first-color");
|
||||
const t = useTranslation();
|
||||
const mapsRef = useRef<any>();
|
||||
|
||||
@@ -50,8 +40,6 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
|
||||
loadMapIsLinked();
|
||||
|
||||
const sub = mapsService.$mapsLinkingPending(version).pipe(debounceTime(50)).subscribe(setLinkingPending);
|
||||
|
||||
const onMapsLinked: VersionLinkerActionListener = action => {
|
||||
if (!action.relativeFolder.includes(MapsManagerService.RELATIVE_MAPS_FOLDER)) {
|
||||
return;
|
||||
@@ -63,7 +51,6 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
linker.onVersionFolderUnlinked(onMapsLinked);
|
||||
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
linker.removeVersionFolderLinkedListener(onMapsLinked);
|
||||
linker.removeVersionFolderUnlinkedListener(onMapsLinked);
|
||||
};
|
||||
@@ -80,6 +67,10 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
return setPlaylistSearch(() => value);
|
||||
};
|
||||
|
||||
const handleMapsAddClick = () => {
|
||||
mapsDownloader.openDownloadMapModal(version, mapsRef.current.getMaps?.());
|
||||
};
|
||||
|
||||
const handleMapsLinkClick = () => {
|
||||
if (!mapsLinked) {
|
||||
return mapsService.linkVersion(version);
|
||||
@@ -87,56 +78,6 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
return mapsService.unlinkVersion(version);
|
||||
};
|
||||
|
||||
const handleMapsAddClick = () => {
|
||||
mapsDownloader.openDownloadMapModal(version, mapsRef.current.getMaps?.());
|
||||
};
|
||||
|
||||
const renderTab = (props: DetailedHTMLProps<React.HTMLAttributes<HTMLLIElement>, HTMLLIElement>, text: string, index: number): JSX.Element => {
|
||||
const onClickLink = (index: number) => {
|
||||
if (index === 0) {
|
||||
handleMapsLinkClick();
|
||||
}
|
||||
};
|
||||
|
||||
const onClickAdd = (index: number) => {
|
||||
if (index === 0) {
|
||||
handleMapsAddClick();
|
||||
}
|
||||
};
|
||||
|
||||
const variants: Variants = { hover: { rotate: 22.5 }, tap: { rotate: 45 } };
|
||||
|
||||
return (
|
||||
<li className="relative text-center text-lg font-bold hover:backdrop-brightness-75 flex justify-center items-center content-center" onClick={props.onClick}>
|
||||
<span className="text-main-color-1 dark:text-gray-200">{text}</span>
|
||||
{index === 0 && (
|
||||
<div className="h-full flex absolute right-0 top-0 gap-1.5 items-center pr-2">
|
||||
{isOnline && (
|
||||
<motion.div
|
||||
whileHover="hover"
|
||||
whileTap="tap"
|
||||
className="relative h-[calc(100%-5px)] flex flex-row justify-center items-center shrink-0 rounded-full overflow-hidden pr-2"
|
||||
style={{ color }}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
onClickAdd(index);
|
||||
}}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<span className="absolute top-0 left-0 h-full w-full brightness-50 opacity-75 dark:opacity-20 dark:filter-none" style={{ backgroundColor: "currentcolor" }} />
|
||||
<motion.div className="h-full p-0.5" variants={variants}>
|
||||
<BsmIcon className="block h-full aspect-square brightness-150" icon="add" />
|
||||
</motion.div>
|
||||
<span className="text-sm brightness-150">{t("misc.add")}</span>
|
||||
</motion.div>
|
||||
)}
|
||||
{!!version && <LinkButton variants={variants} disabled={linkingPending} whileHover="hover" whileTap="tap" initial={{ rotate: 0 }} className="block p-0.5 h-[calc(100%-5px)] aspect-square blur-0 hover:brightness-75" linked={mapsLinked} title={mapsLinked ? "pages.version-viewer.maps.tabs.maps.actions.link-maps.tooltips.unlink" : "pages.version-viewer.maps.tabs.maps.actions.link-maps.tooltips.link"} onClick={() => onClickLink(index)} />}
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
const dropDownItems = ((): DropDownItem[] => {
|
||||
if (tabIndex === 1) {
|
||||
return [];
|
||||
@@ -150,6 +91,15 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center gap-4">
|
||||
<nav className="w-full shrink-0 flex h-9 justify-center px-40 gap-2 text-main-color-1 dark:text-white">
|
||||
<BsmButton
|
||||
className="flex items-center justify-center w-fit rounded-full px-2 py-1 font-bold"
|
||||
icon="add"
|
||||
text="misc.add"
|
||||
typeColor="primary"
|
||||
withBar={false}
|
||||
disabled={tabIndex === 1}
|
||||
onClick={handleMapsAddClick}
|
||||
/>
|
||||
<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={t("pages.version-viewer.maps.search-bar.search-placeholder")} value={tabIndex === 0 ? mapSearch : playlistSearch} onChange={e => handleSearch(e.target.value)} tabIndex={-1} />
|
||||
</div>
|
||||
@@ -158,16 +108,34 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
</BsmDropdownButton>
|
||||
<BsmDropdownButton 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} items={dropDownItems} menuTranslationY="6px" align="center" />
|
||||
</nav>
|
||||
<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">
|
||||
<TabNavBar className="!rounded-none shadow-sm" tabIndex={tabIndex} tabsText={["misc.maps", "misc.playlists"]} onTabChange={setTabIndex} renderTab={renderTab} />
|
||||
<div className="w-full grow min-h-0 flex flex-row items-center transition-transform duration-300" style={{ transform: `translate(${-(tabIndex * 100)}%, 0)` }}>
|
||||
<BsContentTabPanel
|
||||
tabIndex={tabIndex}
|
||||
onTabChange={index => setTabIndex(index)}
|
||||
tabs={[
|
||||
{
|
||||
text: "misc.maps",
|
||||
icon: "trash",
|
||||
onClick: () => setTabIndex(0),
|
||||
linkProps: version ? {
|
||||
state$: mapsService.$mapsFolderLinkState(version),
|
||||
onClick: handleMapsLinkClick,
|
||||
} : undefined,
|
||||
},
|
||||
{
|
||||
text: "misc.playlists",
|
||||
icon: "trash",
|
||||
onClick: () => setTabIndex(1),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<>
|
||||
<LocalMapsListPanel isActive={isActive && tabIndex === 0} ref={mapsRef} className="w-full h-full shrink-0 flex flex-col" version={version} filter={mapFilter} search={mapSearch} linked={mapsLinked} />
|
||||
<div className="w-full h-full shrink-0 flex flex-col justify-center items-center content-center gap-2 overflow-hidden text-gray-800 dark:text-gray-200">
|
||||
<BsmImage className="rounded-md" image={wipGif} />
|
||||
<span>Coming soon</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</BsContentTabPanel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ export const MapsRow = memo(({ maps, style, selectedMaps$, onMapSelect, onMapDel
|
||||
};
|
||||
|
||||
return (
|
||||
<ul className="h-fit w-full flex flex-nowrap basis-0 gap-x-[8px] py-1 px-3" style={style}>
|
||||
<ul className="h-fit w-full flex flex-nowrap basis-0 gap-x-2 p-2" style={style}>
|
||||
{maps?.map(renderMapItem)}
|
||||
</ul>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import Tippy from "@tippyjs/react";
|
||||
import { Variants } from "framer-motion";
|
||||
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 { useConstant } from "renderer/hooks/use-constant.hook";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
@@ -12,7 +12,7 @@ import { BSVersionManagerService } from "renderer/services/bs-version-manager.se
|
||||
import { ConfigurationService } from "renderer/services/configuration.service";
|
||||
import { IpcService } from "renderer/services/ipc.service";
|
||||
import { ModalComponent } from "renderer/services/modale.service";
|
||||
import { VersionFolderLinkerService, VersionLinkerActionType } from "renderer/services/version-folder-linker.service";
|
||||
import { FolderLinkState, VersionFolderLinkerService, VersionLinkerActionType } from "renderer/services/version-folder-linker.service";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
|
||||
export const ShareFoldersModal: ModalComponent<void, BSVersion> = ({ data }) => {
|
||||
@@ -107,33 +107,13 @@ const FolderItem = ({ version, relativeFolder, onDelete }: FolderProps) => {
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
const name = relativeFolder.split(window.electron.path.sep).at(-1);
|
||||
const color = useThemeColor("first-color");
|
||||
|
||||
const variants: Variants = {
|
||||
hover: { rotate: 22.5 },
|
||||
tap: { rotate: 45 },
|
||||
};
|
||||
|
||||
const pending = useObservable(linker.isPending(version, relativeFolder));
|
||||
const processing = useObservable(linker.isProcessing(version, relativeFolder));
|
||||
const linkDisabled = pending || processing;
|
||||
|
||||
const [linked, setLinked] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (pending) {
|
||||
return;
|
||||
}
|
||||
loadFolderIsLinked();
|
||||
}, [version, relativeFolder, pending]);
|
||||
|
||||
const loadFolderIsLinked = () => {
|
||||
linker.isVersionFolderLinked(version, relativeFolder).toPromise().then(setLinked);
|
||||
};
|
||||
const state$ = useConstant(() => linker.$folderLinkedState(version, relativeFolder));
|
||||
const state = useObservable(state$);
|
||||
const name = relativeFolder.split(window.electron.path.sep).at(-1);
|
||||
|
||||
const onClickLink = () => {
|
||||
if (linked) {
|
||||
if (state === FolderLinkState.Linked) {
|
||||
return linker.unlinkVersionFolder({
|
||||
version,
|
||||
relativeFolder,
|
||||
@@ -150,7 +130,6 @@ const FolderItem = ({ version, relativeFolder, onDelete }: FolderProps) => {
|
||||
|
||||
const cancelLink = () => {
|
||||
linker.cancelAction(version, relativeFolder);
|
||||
loadFolderIsLinked();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -159,14 +138,18 @@ const FolderItem = ({ version, relativeFolder, onDelete }: FolderProps) => {
|
||||
{name}
|
||||
</span>
|
||||
<div className="flex flex-row gap-1.5">
|
||||
<Tippy placement="left" content={t(`modals.shared-folders.buttons.${linked ? "unlink-folder" : "link-folder"}`)} arrow={false}>
|
||||
<LinkButton variants={variants} linked={linked} disabled={linkDisabled} whileHover="hover" whileTap="tap" className="p-0.5 h-7 shrink-0 aspect-square blur-0 cursor-pointer hover:brightness-75" onClick={onClickLink} />
|
||||
<Tippy placement="left" content={t(`modals.shared-folders.buttons.${state === FolderLinkState.Linked ? "unlink-folder" : "link-folder"}`)} arrow={false}>
|
||||
<LinkButton
|
||||
className="p-0.5 h-7 shrink-0 aspect-square blur-0 cursor-pointer hover:brightness-75"
|
||||
state$={state$}
|
||||
onClick={onClickLink}
|
||||
/>
|
||||
</Tippy>
|
||||
{(() => {
|
||||
if (processing) {
|
||||
if (state === FolderLinkState.Processing) {
|
||||
return <BsmBasicSpinner className="aspect-square h-7 rounded-md p-1 dark:bg-main-color-2" thikness="3.5px" style={{ color }} />;
|
||||
}
|
||||
if (pending) {
|
||||
if (state === FolderLinkState.Pending) {
|
||||
return (
|
||||
<BsmButton
|
||||
className="aspect-square h-7 rounded-md p-1"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
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";
|
||||
@@ -14,6 +13,11 @@ import { NotificationService } from "renderer/services/notification.service";
|
||||
import { ConfigurationService } from "renderer/services/configuration.service";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { lt } from "semver";
|
||||
import { lastValueFrom, take } from "rxjs";
|
||||
import { FolderLinkState } from "renderer/services/version-folder-linker.service";
|
||||
import { BsContentTabItemProps } from "../shared/bs-content-tab-panel/bs-content-tab-item.component";
|
||||
import { BsContentTabPanel } from "../shared/bs-content-tab-panel/bs-content-tab-panel.component";
|
||||
import { LinkBtnProps } from "../maps-mangement-components/link-button.component";
|
||||
|
||||
export function ModelsPanel({ version, isActive, goToMods }: { version?: BSVersion; isActive: boolean; goToMods?: () => void }) {
|
||||
const modelsManager = useService(ModelsManagerService);
|
||||
@@ -93,6 +97,31 @@ export function ModelsPanel({ version, isActive, goToMods }: { version?: BSVersi
|
||||
{ text: "models.panel.actions.drop-down.delete", onClick: deleteModels, icon: "trash" },
|
||||
];
|
||||
|
||||
const getModelTabProps = (model: MSModelType): BsContentTabItemProps => {
|
||||
|
||||
const onClick = async () => {
|
||||
const state = await lastValueFrom(modelsManager.$modelsLinkingState(version, model).pipe(take(1)));
|
||||
if (state === FolderLinkState.Pending || state === FolderLinkState.Processing) { return; }
|
||||
if (state === FolderLinkState.Linked) {
|
||||
modelsManager.unlinkModels(model, version);
|
||||
} else {
|
||||
modelsManager.linkModels(model, version);
|
||||
}
|
||||
}
|
||||
|
||||
const linkProps: LinkBtnProps = version ? {
|
||||
state$: modelsManager.$modelsLinkingState(version, model),
|
||||
onClick,
|
||||
} : undefined;
|
||||
|
||||
return {
|
||||
text: `models.types.plural.${model}`,
|
||||
icon: model,
|
||||
onClick: () => setModelTypeTab(model),
|
||||
linkProps
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={ref} className="w-full h-full flex flex-col items-center justify-center gap-4">
|
||||
<div className="w-full shrink-0 flex h-9 justify-center px-40 gap-2 text-main-color-1 dark:text-white">
|
||||
@@ -112,24 +141,23 @@ export function ModelsPanel({ version, isActive, goToMods }: { version?: BSVersi
|
||||
</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={currentTabIndex}
|
||||
onTabChange={(index, tab) => {
|
||||
setModelTypeTab(() => tab.extra);
|
||||
setCurrentTabIndex(() => index);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex-grow h-full flex flex-col transition-all duration-300" style={{ translate: `0 ${0 - currentTabIndex * 100}%` }}>
|
||||
<BsContentTabPanel
|
||||
tabIndex={currentTabIndex}
|
||||
onTabChange={(index) => setCurrentTabIndex(index)}
|
||||
tabs={[
|
||||
getModelTabProps(MSModelType.Avatar),
|
||||
getModelTabProps(MSModelType.Saber),
|
||||
getModelTabProps(MSModelType.Platfrom),
|
||||
getModelTabProps(MSModelType.Bloq),
|
||||
]}
|
||||
>
|
||||
<>
|
||||
<ModelsGrid ref={modelsGridRefs[0]} version={version} type={MSModelType.Avatar} active={isActive && modelTypeTab === MSModelType.Avatar} search={search} downloadModels={openDownloadModal} />
|
||||
<ModelsGrid ref={modelsGridRefs[1]} version={version} type={MSModelType.Saber} active={isActive && modelTypeTab === MSModelType.Saber} search={search} downloadModels={openDownloadModal} />
|
||||
<ModelsGrid ref={modelsGridRefs[2]} version={version} type={MSModelType.Platfrom} active={isActive && modelTypeTab === MSModelType.Platfrom} search={search} downloadModels={openDownloadModal} />
|
||||
<ModelsGrid ref={modelsGridRefs[3]} version={version} type={MSModelType.Bloq} active={isActive && modelTypeTab === MSModelType.Bloq} search={search} downloadModels={openDownloadModal} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</BsContentTabPanel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { MSModelType } from "../../../shared/models/models/model-saber.model";
|
||||
import { LinkButton } from "../maps-mangement-components/link-button.component";
|
||||
import { useState } from "react";
|
||||
import { useOnUpdate } from "renderer/hooks/use-on-update.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";
|
||||
import { BsmIcon } from "../svgs/bsm-icon.component";
|
||||
import { MODEL_TYPES } from "../../../shared/models/models/constants";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
version?: BSVersion;
|
||||
tabIndex: number;
|
||||
onTabChange: (index: number, tab: BsContentNavBarTab<MSModelType>) => void;
|
||||
};
|
||||
|
||||
export function ModelsTabsNavbar({ className, version, tabIndex, onTabChange }: Props) {
|
||||
const tabs = useConstant<BsContentNavBarTab<MSModelType>[]>(() => {
|
||||
return MODEL_TYPES.map(type => ({
|
||||
text: type,
|
||||
extra: type,
|
||||
}));
|
||||
});
|
||||
|
||||
return <BsContentNavBar className={`!rounded-none shadow-sm ${className ?? ""}`} tabIndex={tabIndex} onTabChange={onTabChange} tabs={tabs} renderTab={(props, tab, activeTab) => <ModelTab version={version} modelType={tab.extra} {...props} active={tab === activeTab} />} />;
|
||||
}
|
||||
|
||||
type TabProps = {
|
||||
version?: BSVersion;
|
||||
modelType: MSModelType;
|
||||
active?: boolean;
|
||||
} & React.ComponentProps<"li">;
|
||||
|
||||
function ModelTab({ version, modelType, active, onClick }: TabProps) {
|
||||
const modelsManager = useService(ModelsManagerService);
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
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);
|
||||
const unlinkModels = () => modelsManager.unlinkModels(modelType, version);
|
||||
|
||||
const onClickLink = () => {
|
||||
if (!version) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (modelsAreLinked) {
|
||||
return unlinkModels();
|
||||
}
|
||||
return linkModels();
|
||||
};
|
||||
|
||||
return (
|
||||
<li className={`relative w-full cursor-pointer flex-1 text-center text-lg font-bold flex justify-center items-center content-center px-7 dim-on-hover ${active ? "dim" : ""}`} onClick={onClick}>
|
||||
<div className="flex flex-col gap-0.5 justify-start items-center text-main-color-1 dark:text-gray-200">
|
||||
<BsmIcon icon={modelType} className="w-7 h-7" />
|
||||
<span className=" font-thin italic text-xs">{t(`models.types.plural.${modelType}`)}</span>
|
||||
</div>
|
||||
<div className="flex items-center absolute top-1.5 left-1.5">{!!version && <LinkButton variants={{ hover: { rotate: 22.5 }, tap: { rotate: 45 } }} disabled={linkBtnDisabled} whileHover="hover" whileTap="tap" initial={{ rotate: 0 }} className="block w-6 h-6 aspect-square blur-0 cursor-pointer hover:brightness-75" linked={modelsAreLinked} title={modelsAreLinked ? "models.panel.actions.unlink-models" : "models.panel.actions.link-models"} onClick={onClickLink} />}</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export function SettingRadioArray<T>({ id, items, selectedItemId, selectedItemVa
|
||||
return (
|
||||
<div id={id} className="w-full flex gap-1.5" style={{flexDirection: direction}}>
|
||||
{items.map(i => (
|
||||
<div onClick={() => onItemSelected(i)} key={i.id} className={`py-3 w-full flex cursor-pointer justify-between items-center rounded-md px-2 transition-colors duration-200 ${isSelected(i) ? "bg-light-main-color-3 dark:bg-main-color-3" : "bg-light-main-color-1 dark:bg-main-color-1"} ${i.className}`}>
|
||||
<div onClick={() => onItemSelected(i)} key={i.id} className={`py-3 w-full flex cursor-pointer justify-between items-center rounded-md px-2 transition-colors duration-300 ${isSelected(i) ? "bg-light-main-color-3 dark:bg-main-color-3" : "bg-light-main-color-1 dark:bg-main-color-1"} ${i.className}`}>
|
||||
<div className="flex items-center">
|
||||
<div className="h-5 rounded-full aspect-square border-2 border-gray-800 dark:border-white p-[3px] mr-2">
|
||||
<motion.span initial={{ scale: 0 }} animate={{ scale: isSelected(i) ? 1 : 0 }} className="h-full w-full block bg-gray-800 dark:bg-white rounded-full" />
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { DetailedHTMLProps, Fragment } from "react";
|
||||
import { BsmIconType } from "../svgs/bsm-icon.component";
|
||||
|
||||
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;
|
||||
icon?: BsmIconType;
|
||||
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={JSON.stringify(tab)}>{renderTab({ onClick: () => handleTabClick(i) }, tab, tabs[tabIndex])}</Fragment>
|
||||
))}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { LinkBtnProps, LinkButton } from "renderer/components/maps-mangement-components/link-button.component";
|
||||
import { BsmIcon, BsmIconType } from "renderer/components/svgs/bsm-icon.component";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
|
||||
export type BsContentTabItemProps<T = unknown> = {
|
||||
text: string;
|
||||
icon: BsmIconType;
|
||||
active?: boolean;
|
||||
value?: T;
|
||||
linkProps?: LinkBtnProps;
|
||||
onClick: (value?: T) => void;
|
||||
};
|
||||
|
||||
type BsContentTabItemComponent<T = unknown> = ({text, icon, active, value, linkProps, onClick}: BsContentTabItemProps<T>) => JSX.Element;
|
||||
|
||||
export const BsContentTabItem: BsContentTabItemComponent = ({ text, icon, active, value, linkProps, onClick }) => {
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
const handleClick = (e: React.MouseEvent<HTMLLIElement, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onClick(value);
|
||||
}
|
||||
|
||||
return (
|
||||
<li className={`relative w-full cursor-pointer flex-1 text-center text-lg font-bold flex justify-center items-center content-center px-7 bg-light-main-color-2 dark:bg-main-color-2 hover:bg-light-main-color-1 dark:hover:bg-main-color-1 ${active && "!bg-light-main-color-1 dark:!bg-main-color-1"}`} onClick={handleClick}>
|
||||
<div className="flex flex-col gap-0.5 justify-start items-center text-main-color-1 dark:text-gray-200">
|
||||
<BsmIcon icon={icon} className="w-7 h-7" />
|
||||
<span className=" font-thin italic text-xs">{t(text)}</span>
|
||||
</div>
|
||||
{linkProps && (
|
||||
<div className="flex items-center absolute top-1.5 left-1.5">
|
||||
<LinkButton
|
||||
state$={linkProps.state$}
|
||||
className={linkProps.className ?? "block w-6 h-6 aspect-square blur-0 cursor-pointer hover:brightness-75"}
|
||||
title={linkProps.title}
|
||||
onClick={linkProps.onClick}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import { LaserSlider } from "../laser-slider.component";
|
||||
import { BsContentTabItem, BsContentTabItemProps } from "./bs-content-tab-item.component";
|
||||
|
||||
type Props<T = unknown> = {
|
||||
className?: string;
|
||||
tabs: BsContentTabItemProps<T>[];
|
||||
tabIndex: number;
|
||||
onTabChange: (index: number, tab: BsContentTabItemProps<T>) => void;
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
export function BsContentTabPanel({className, tabIndex, tabs, onTabChange, children}: Props) {
|
||||
|
||||
const sliderColor = useThemeColor("second-color");
|
||||
|
||||
return (
|
||||
<div className={className ?? "w-full h-full flex flex-row bg-light-main-color-1 dark:bg-main-color-1 rounded-md shadow-black shadow-md overflow-hidden"}>
|
||||
<nav className="h-full grid grid-flow-row !rounded-none shadow-sm flex-shrink-0">
|
||||
{tabs.map((tab, i) => (
|
||||
<BsContentTabItem
|
||||
key={`${tab.text}${tab.icon}`}
|
||||
{...tab}
|
||||
active={tab.active ?? tabIndex === i}
|
||||
onClick={value => {onTabChange(i, tab); tab.onClick(value);}}
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
<LaserSlider className="h-full w-1 relative shrink-0" mode="vertical" color={sliderColor} nbSteps={tabs.length} step={tabIndex}/>
|
||||
<div className="flex-grow h-full flex flex-col transition-all duration-300" style={{ translate: `0 ${0 - tabIndex * 100}%` }}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
type Props = {
|
||||
mode: "horizontal" | "vertical";
|
||||
color: string;
|
||||
className?: string;
|
||||
nbSteps: number;
|
||||
step: number;
|
||||
}
|
||||
|
||||
export function LaserSlider({mode, color, className, nbSteps, step}: Props) {
|
||||
|
||||
const sliderStyle = ((): CSSProperties => {
|
||||
if(mode === "vertical"){
|
||||
return {
|
||||
transform: `translate(0, ${step * 100}%)`,
|
||||
height: `calc(100% / ${nbSteps})`,
|
||||
width: "100%"
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
transform: `translate(${step * 100}%, 0)`,
|
||||
width: `calc(100% / ${nbSteps})`,
|
||||
height: "100%"
|
||||
}
|
||||
})();
|
||||
|
||||
return (
|
||||
<div className={className} style={{ color }}>
|
||||
<span className="absolute h-full w-full bg-current brightness-50" />
|
||||
<span className="absolute block bg-current transition-transform duration-300 shadow-center shadow-current" style={sliderStyle} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DetailedHTMLProps, Fragment } from "react";
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { LaserSlider } from "./laser-slider.component";
|
||||
|
||||
type Props = {
|
||||
tabIndex: number;
|
||||
@@ -22,10 +23,7 @@ export function TabNavBar(props: Props) {
|
||||
|
||||
return (
|
||||
<nav className={`relative h-8 shrink-0 cursor-pointer rounded-md overflow-hidden shadow-md shadow-black bg-light-main-color-2 dark:bg-main-color-2 ${props.className}`}>
|
||||
<div className="absolute w-full h-1 bottom-0" style={{ color: secondColor }}>
|
||||
<span className="absolute h-full w-full bg-current brightness-50" />
|
||||
<span className="absolute h-full block bg-current transition-transform duration-300 shadow-center shadow-current" style={{ transform: `translate(${currentIndex * 100}%, 0)`, width: `calc(100% / ${props.tabsText.length})` }} />
|
||||
</div>
|
||||
<LaserSlider className="absolute w-full h-1 bottom-0" mode="horizontal" color={secondColor} nbSteps={props.tabsText.length} step={currentIndex} />
|
||||
<ul className="grid" style={{ gridTemplateColumns: `repeat(${props.tabsText.length}, minmax(0, 1fr))` }}>
|
||||
{props.tabsText.map((text, index) =>
|
||||
props.renderTab ? (
|
||||
|
||||
@@ -13,7 +13,7 @@ import { ConfigurationService } from "./configuration.service";
|
||||
import { ArchiveProgress } from "shared/models/archive.interface";
|
||||
import { map, last, catchError } from "rxjs/operators";
|
||||
import { ProgressionInterface } from "shared/models/progress-bar";
|
||||
import { VersionFolderLinkerService, VersionLinkerActionType } from "./version-folder-linker.service";
|
||||
import { FolderLinkState, VersionFolderLinkerService, VersionLinkerActionType } from "./version-folder-linker.service";
|
||||
|
||||
export class MapsManagerService {
|
||||
private static instance: MapsManagerService;
|
||||
@@ -176,6 +176,10 @@ export class MapsManagerService {
|
||||
}
|
||||
|
||||
public $mapsLinkingPending(version: BSVersion): Observable<boolean> {
|
||||
return this.linker.$isVersionFolderPending(version, MapsManagerService.RELATIVE_MAPS_FOLDER);
|
||||
return this.linker.$isPending(version, MapsManagerService.RELATIVE_MAPS_FOLDER);
|
||||
}
|
||||
|
||||
public $mapsFolderLinkState(version: BSVersion): Observable<FolderLinkState> {
|
||||
return this.linker.$folderLinkedState(version, MapsManagerService.RELATIVE_MAPS_FOLDER);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { MSModelType } from "shared/models/models/model-saber.model";
|
||||
import { IpcService } from "../ipc.service";
|
||||
import { VersionFolderLinkerService, VersionLinkerActionListener, VersionLinkerActionType } from "../version-folder-linker.service";
|
||||
import { FolderLinkState, VersionFolderLinkerService, VersionLinkerActionListener, VersionLinkerActionType } from "../version-folder-linker.service";
|
||||
import { MODEL_TYPE_FOLDERS } from "shared/models/models/constants";
|
||||
import { Observable, lastValueFrom, map } from "rxjs";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
@@ -49,7 +49,11 @@ export class ModelsManagerService {
|
||||
}
|
||||
|
||||
public $modelsLinkingPending(version: BSVersion, type: MSModelType): Observable<boolean> {
|
||||
return this.versionFolderLinked.$isVersionFolderPending(version, MODEL_TYPE_FOLDERS[type]);
|
||||
return this.versionFolderLinked.$isPending(version, MODEL_TYPE_FOLDERS[type]);
|
||||
}
|
||||
|
||||
public $modelsLinkingState(version: BSVersion, type: MSModelType): Observable<FolderLinkState> {
|
||||
return this.versionFolderLinked.$folderLinkedState(version, MODEL_TYPE_FOLDERS[type]);
|
||||
}
|
||||
|
||||
public onModelsFolderLinked(callback: VersionLinkerActionListener): void {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { LinkOptions, UnlinkOptions } from "main/services/folder-linker.service";
|
||||
import { map, distinctUntilChanged, filter, mergeMap, shareReplay } from "rxjs/operators";
|
||||
import { BehaviorSubject, Observable } from "rxjs";
|
||||
import { BehaviorSubject, Observable, of } 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 {
|
||||
private static instance: VersionFolderLinkerService;
|
||||
@@ -62,6 +61,13 @@ export class VersionFolderLinkerService {
|
||||
return this.ipcService.sendV2<boolean, VersionLinkerAction>("link-version-folder-action", { args: action });
|
||||
}
|
||||
|
||||
private get currentAction$(): Observable<VersionLinkerAction> {
|
||||
return this._queue$.pipe(
|
||||
map(actions => actions.at(0)),
|
||||
distinctUntilChanged()
|
||||
);
|
||||
}
|
||||
|
||||
public linkVersionFolder(action: VersionLinkFolderAction): Promise<boolean> {
|
||||
const promise = new Promise<boolean>(resolve => {
|
||||
const callBack: VersionLinkerActionListener = (performedAction, linked) => {
|
||||
@@ -122,12 +128,33 @@ export class VersionFolderLinkerService {
|
||||
return this.ipcService.sendV2("is-version-folder-linked", { args: { version, relativeFolder } });
|
||||
}
|
||||
|
||||
public isPending(version: BSVersion, relativeFolder: string): Observable<boolean> {
|
||||
return this.queue$.pipe(map(queue => queue.length > 0 && queue.some(action => action.version === version && action.relativeFolder === relativeFolder), distinctUntilChanged()));
|
||||
public $folderLinkedState(version: BSVersion, relativeFolder: string): Observable<FolderLinkState> {
|
||||
return this._queue$.pipe(
|
||||
mergeMap(queue => {
|
||||
const currentAction = queue.at(0);
|
||||
if(currentAction && currentAction.version === version && currentAction.relativeFolder === relativeFolder) {
|
||||
return of(FolderLinkState.Processing)
|
||||
}
|
||||
|
||||
if(queue.some(action => action.version === version && action.relativeFolder === relativeFolder)) {
|
||||
return of(FolderLinkState.Pending);
|
||||
}
|
||||
|
||||
return this.isVersionFolderLinked(version, relativeFolder).pipe(
|
||||
map(linked => linked ? FolderLinkState.Linked : FolderLinkState.Unlinked)
|
||||
);
|
||||
}),
|
||||
distinctUntilChanged(),
|
||||
shareReplay(1)
|
||||
);
|
||||
}
|
||||
|
||||
public isProcessing(version: BSVersion, relativeFolder: string): Observable<boolean> {
|
||||
return this.currentAction$.pipe(map(action => action && action.version === version && action.relativeFolder === relativeFolder));
|
||||
public $isPending(version: BSVersion, relativeFolder: string): Observable<boolean> {
|
||||
return this.$folderLinkedState(version, relativeFolder).pipe(map(state => state === FolderLinkState.Pending));
|
||||
}
|
||||
|
||||
public $isProcessing(version: BSVersion, relativeFolder: string): Observable<boolean> {
|
||||
return this.$folderLinkedState(version, relativeFolder).pipe(map(state => state === FolderLinkState.Processing));
|
||||
}
|
||||
|
||||
public getLinkedFolders(version: BSVersion, options?: { relative?: boolean }): Observable<string[]> {
|
||||
@@ -137,27 +164,6 @@ export class VersionFolderLinkerService {
|
||||
public relinkAllVersionsFolders(): Observable<void> {
|
||||
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(),
|
||||
shareReplay(1)
|
||||
);
|
||||
}
|
||||
|
||||
public get currentAction$(): Observable<VersionLinkerAction> {
|
||||
return this._queue$.pipe(
|
||||
map(actions => actions.at(0)),
|
||||
distinctUntilChanged()
|
||||
);
|
||||
}
|
||||
|
||||
public get queue$(): Observable<VersionLinkerAction[]> {
|
||||
return this._queue$.asObservable();
|
||||
}
|
||||
}
|
||||
|
||||
export const enum VersionLinkerActionType {
|
||||
@@ -182,3 +188,10 @@ export interface VersionUnlinkFolderAction extends VersionLinkerAction {
|
||||
}
|
||||
|
||||
export type VersionLinkerActionListener = (action: VersionLinkerAction, linked: boolean) => void;
|
||||
|
||||
export enum FolderLinkState {
|
||||
Linked,
|
||||
Unlinked,
|
||||
Pending,
|
||||
Processing
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user