mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
[feature-107] Implement playlists linking + remove possibility to play a playlist for now + some refacto
This commit is contained in:
@@ -65,6 +65,9 @@ module.exports = {
|
||||
tsconfigRootDir: __dirname,
|
||||
createDefaultProgram: true,
|
||||
},
|
||||
globals: {
|
||||
JSX: true,
|
||||
}
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
// See https://github.com/benmosher/eslint-plugin-import/issues/1396#issuecomment-575727774 for line below
|
||||
|
||||
@@ -116,7 +116,7 @@ export class LocalMapsManagerService {
|
||||
return shasum.digest("hex");
|
||||
}
|
||||
|
||||
private async loadMapInfoFromPath(mapPath: string): Promise<BsmLocalMap> {
|
||||
public async loadMapInfoFromPath(mapPath: string): Promise<BsmLocalMap> {
|
||||
|
||||
const getUrlsAndReturn = (rawInfo: RawMapInfoData, hash: string, mapPath: string) => {
|
||||
const coverUrl = pathToFileURL(path.join(mapPath, rawInfo._coverImageFilename)).href;
|
||||
|
||||
@@ -29,8 +29,8 @@ export const AvailableVersionItem = memo(function AvailableVersionItem({version,
|
||||
{version.recommended && (
|
||||
<span className="uppercase absolute -rotate-45 top-9 -left-[6.2rem] font-bold text-white bg-red-600 w-full text-center text-xs z-[1] shadow-sm shadow-black py-0.5" title={t("pages.available-versions.recommended-tooltip")}>{t("pages.available-versions.recommended")}</span>
|
||||
)}
|
||||
<BsmImage image={version.ReleaseImg ? version.ReleaseImg : defaultImage} errorImage={defaultImage} placeholder={defaultImage} className="absolute top-0 right-0 w-full h-full opacity-40 blur-xl object-cover" loading="lazy" />
|
||||
<BsmImage image={version.ReleaseImg ? version.ReleaseImg : defaultImage} errorImage={defaultImage} placeholder={defaultImage} className="bg-black w-full h-3/4 object-cover" loading="lazy" />
|
||||
<BsmImage image={version.ReleaseImg ? version.ReleaseImg : defaultImage} errorImage={defaultImage} placeholder={defaultImage} className="absolute top-0 right-0 w-full h-full opacity-40 blur-xl object-cover" />
|
||||
<BsmImage image={version.ReleaseImg ? version.ReleaseImg : defaultImage} errorImage={defaultImage} placeholder={defaultImage} className="bg-black w-full h-3/4 object-cover" />
|
||||
<div className="z-[1] p-2 w-full flex items-center justify-between grow">
|
||||
<div>
|
||||
<h2 className="block text-xl font-bold text-white tracking-wider">{version.BSVersion}</h2>
|
||||
|
||||
@@ -68,6 +68,16 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
return mapsService.unlinkVersion(version);
|
||||
};
|
||||
|
||||
const handlePlaylistLinkClick = () => {
|
||||
if(playlistLinkedState === FolderLinkState.Pending || playlistLinkedState === FolderLinkState.Processing){ return Promise.resolve(false); }
|
||||
|
||||
if (playlistLinkedState === FolderLinkState.Unlinked) {
|
||||
return playlistsService.linkVersion(version);
|
||||
}
|
||||
|
||||
return playlistsService.unlinkVersion(version);
|
||||
}
|
||||
|
||||
const dropDownItems = ((): DropDownItem[] => {
|
||||
if (tabIndex === 1) {
|
||||
return [];
|
||||
@@ -117,7 +127,7 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
onClick: () => setTabIndex(1),
|
||||
linkProps: version ? {
|
||||
state: playlistLinkedState,
|
||||
onClick: () => Promise.resolve(false),
|
||||
onClick: handlePlaylistLinkClick,
|
||||
} : null,
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -178,7 +178,7 @@ export const MapItem = memo(({ hash, title, autor, songAutor, coverUrl, songUrl,
|
||||
</AnimatePresence>
|
||||
<div className="h-full w-full relative pl-[100px] rounded-md overflow-hidden flex flex-row justify-end">
|
||||
<div className={`absolute top-0 left-0 h-full aspect-square cursor-pointer ${showOwned && "border-l-[5px]"}`} style={{ borderColor: showOwned && color }}>
|
||||
<BsmImage className="w-full h-full object-cover" image={coverUrl} placeholder={defaultImage} errorImage={defaultImage} loading="lazy" />
|
||||
<BsmImage className="w-full h-full object-cover" image={coverUrl} placeholder={defaultImage} errorImage={defaultImage} />
|
||||
<span
|
||||
className="absolute flex justify-center items-center w-full h-full pr-1 bg-transparent top-0 left-0 group-hover:bg-black group-hover:bg-opacity-40"
|
||||
style={{ color }}
|
||||
@@ -192,7 +192,7 @@ 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 saturate-200" image={coverUrl} placeholder={defaultImage} errorImage={defaultImage} loading="lazy" />
|
||||
<BsmImage className="absolute top-0 left-0 w-full h-full -z-[1] object-cover saturate-200" image={coverUrl} placeholder={defaultImage} errorImage={defaultImage} />
|
||||
<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}>
|
||||
|
||||
+3
-7
@@ -11,6 +11,7 @@ import { BSVersion } from "shared/bs-version.interface";
|
||||
import { noop } from "shared/helpers/function.helpers";
|
||||
import { LocalBPList } from "shared/models/playlists/local-playlist.models";
|
||||
import { PlaylistItem } from "./playlist-item.component";
|
||||
import { useStateMap } from "renderer/hooks/use-state-map.hook";
|
||||
|
||||
type Props = {
|
||||
version: BSVersion;
|
||||
@@ -28,12 +29,12 @@ export const LocalPlaylistsListPanel = forwardRef<unknown, Props>(({ version, cl
|
||||
const [playlistsLoading, setPlaylistsLoading] = useState(false);
|
||||
const [playlists, setPlaylists] = useState<LocalBPList[]>([]);
|
||||
const loadPercent$ = useConstant(() => new BehaviorSubject<number>(0));
|
||||
const linked = useStateMap(linkedState, (newState, precMapped) => (newState === FolderLinkState.Pending || newState === FolderLinkState.Processing) ? precMapped : newState === FolderLinkState.Linked, false);
|
||||
|
||||
const loadPlaylists = (): Promise<LocalBPList[]> => {
|
||||
setPlaylistsLoading(true);
|
||||
const obs = playlistService.getVersionPlaylists(version).pipe(
|
||||
tap({ next: load => loadPercent$.next((load.current / load.total) * 100)}),
|
||||
tap({ next: console.log}),
|
||||
map(load => load.data),
|
||||
finalize(() => setPlaylistsLoading(false))
|
||||
);
|
||||
@@ -73,10 +74,6 @@ export const LocalPlaylistsListPanel = forwardRef<unknown, Props>(({ version, cl
|
||||
return max === -Infinity ? null : max;
|
||||
}
|
||||
|
||||
const getSongsOfPlaylist = (playlist: LocalBPList) => {
|
||||
return playlist.songs.map(s => ({ url: s.songUrl ?? `https://cdn.beatsaver.com/${s.song.hash}.mp3`, bpm: s.songDetails?.bpm ?? 0}));
|
||||
}
|
||||
|
||||
useOnUpdate(() => {
|
||||
|
||||
if(!isActiveOnce){ return noop(); }
|
||||
@@ -89,7 +86,7 @@ export const LocalPlaylistsListPanel = forwardRef<unknown, Props>(({ version, cl
|
||||
loadPercent$.next(0);
|
||||
});
|
||||
|
||||
}, [isActiveOnce, version]);
|
||||
}, [isActiveOnce, version, linked]);
|
||||
|
||||
const render = () => {
|
||||
if(playlistsLoading){
|
||||
@@ -114,7 +111,6 @@ export const LocalPlaylistsListPanel = forwardRef<unknown, Props>(({ version, cl
|
||||
duration={getDurationOfPlaylist(p)}
|
||||
maxNps={getMaxNpsOfPlaylist(p)}
|
||||
minNps={getMinNpsOfPlaylist(p)}
|
||||
songs={getSongsOfPlaylist(p)}
|
||||
/>
|
||||
)}
|
||||
</ul>
|
||||
|
||||
+29
-32
@@ -6,9 +6,9 @@ import { PersonIcon } from 'renderer/components/svgs/icons/person-icon.component
|
||||
import { useThemeColor } from 'renderer/hooks/use-theme-color.hook';
|
||||
import dateFormat from 'dateformat';
|
||||
import { NpsIcon } from 'renderer/components/svgs/icons/nps-icon.component';
|
||||
import { useService } from 'renderer/hooks/use-service.hook';
|
||||
import { AudioPlayerService } from 'renderer/services/audio-player.service';
|
||||
import { filter, lastValueFrom, skip, take } from 'rxjs';
|
||||
import { GlowEffect } from 'renderer/components/shared/glow-effect.component';
|
||||
import { useState } from 'react';
|
||||
import { FolderOpenIcon } from 'renderer/components/svgs/icons/folder-open-icon.component';
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
@@ -22,14 +22,14 @@ type Props = {
|
||||
duration?: number;
|
||||
minNps?: number;
|
||||
maxNps?: number;
|
||||
songs?: { url: string, bpm: number }[];
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
export function PlaylistItem({ id, className, title, author, coverUrl, coverBase64, duration, nbMaps, nbMappers, minNps, maxNps, songs }: Props) {
|
||||
export function PlaylistItem({ id, className, title, author, coverUrl, coverBase64, duration, nbMaps, nbMappers, minNps, maxNps, selected }: Props) {
|
||||
|
||||
const player = useService(AudioPlayerService);
|
||||
const color = useThemeColor("first-color");
|
||||
|
||||
const firstColor = useThemeColor("first-color");
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
const nbMapsText = nbMaps ? Intl.NumberFormat(undefined, { notation: "compact" }).format(nbMaps).trim() : null;
|
||||
const nbMappersText = nbMappers ? Intl.NumberFormat(undefined, { notation: "compact" }).format(nbMappers).trim() : null;
|
||||
@@ -45,35 +45,32 @@ export function PlaylistItem({ id, className, title, author, coverUrl, coverBase
|
||||
return duration > 3600 ? dateFormat(date, "h:MM:ss") : dateFormat(date, "MM:ss");
|
||||
})();
|
||||
|
||||
const startPlay = async () => {
|
||||
if (!songs || songs.length === 0) {
|
||||
return;
|
||||
}
|
||||
const playlist = songs.map(s => ({ src: s.url, bpm: s.bpm }));
|
||||
player.playlist(playlist, 0);
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.li id={id} layoutId={id} className={`relative flex flex-row justify-start items-center flex-grow basis-0 min-w-80 h-28 cursor-pointer rounded-md overflow-hidden ${className}`} onClick={() => startPlay()}>
|
||||
<div className="absolute top-0 left-0 size-full flex justify-center items-center -z-[1]">
|
||||
<BsmImage className="size-full object-cover scale-[2] saturate-150 blur-xl" image={coverUrl} base64={coverBase64} loading="lazy" />
|
||||
<div className="absolute top-0 left-0 size-full bg-black opacity-15"/>
|
||||
</div>
|
||||
<div className="h-full aspect-square p-2.5">
|
||||
<BsmImage className="size-full flex-shrink-0 object-cover rounded-md shadow-center shadow-black bg-main-color-1" image={coverUrl} base64={coverBase64} loading="lazy" />
|
||||
</div>
|
||||
<div className="h-full py-2.5 text-white">
|
||||
<h1 className="font-bold text-lg capitalize tracking-wide line-clamp-1">{title}</h1>
|
||||
<p className="text-xs font-bold">Créé par <span className="brightness-200" style={{color: firstColor}}>{author}</span></p>
|
||||
|
||||
<div className="flex flex-row flex-wrap w-full gap-2 mt-1">
|
||||
{ nbMapsText && <div className="flex items-center text-sm h-5 gap-0.5"> <MapIcon className='h-full aspect-square'/> <span className="mb-0.5">{nbMapsText}</span> </div> }
|
||||
{ nbMappersText && <div className="flex items-center text-sm h-5 gap-0.5"> <PersonIcon className='h-full aspect-square'/> <span className="mb-0.5">{nbMappersText}</span> </div> }
|
||||
{ durationText && <div className="flex items-center text-sm h-5 gap-0.5"> <ClockIcon className='h-full aspect-square'/> <span className="mb-0.5">{durationText}</span> </div> }
|
||||
{ (minNps && maxNps) && <div className="flex items-center text-sm h-5 gap-0.5"> <NpsIcon className='h-full aspect-square scale-95'/> <span className="mb-0.5">{`${minNpsText} - ${maxNpsText}`}</span> </div> }
|
||||
<motion.li id={id} layoutId={id} className='relative flex-grow basis-0 min-w-80 h-28 cursor-pointer' onHoverStart={() => setHovered(() => true)} onHoverEnd={() => setHovered(() => false)} >
|
||||
<GlowEffect visible={selected || hovered}/>
|
||||
<div className={`size-full relative flex flex-row justify-start items-center overflow-hidden rounded-md ${className}`}>
|
||||
<div className="absolute top-0 left-0 size-full flex justify-center items-center -z-[1]">
|
||||
<BsmImage className="size-full object-cover scale-[2] saturate-150 blur-xl" image={coverUrl} base64={coverBase64} />
|
||||
<div className="absolute top-0 left-0 size-full bg-black opacity-15"/>
|
||||
</div>
|
||||
<div className="relative h-full aspect-square p-2.5" style={{ color }}>
|
||||
<BsmImage className="size-full flex-shrink-0 object-cover rounded-md shadow-center shadow-black bg-main-color-1" image={coverUrl} base64={coverBase64} style={{filter: hovered && "brightness(75%)"}} />
|
||||
<FolderOpenIcon className="absolute size-1/2 top-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2 transition-opacity duration-150 text-white hover:text-current" style={{ opacity: hovered ? 1 : 0}}/>
|
||||
</div>
|
||||
<div className="h-full py-2.5 text-white">
|
||||
<h1 className="font-bold text-lg tracking-wide line-clamp-1 hover:underline w-fit">{title}</h1>
|
||||
<p className="text-xs font-bold">Créé par <span className="brightness-200" style={{color}}>{author}</span></p>
|
||||
|
||||
<div className="flex flex-row flex-wrap w-full gap-2 mt-1">
|
||||
{ nbMapsText && <div className="flex items-center text-sm h-5 gap-0.5"> <MapIcon className='h-full aspect-square'/> <span className="mb-0.5">{nbMapsText}</span> </div> }
|
||||
{ nbMappersText && <div className="flex items-center text-sm h-5 gap-0.5"> <PersonIcon className='h-full aspect-square'/> <span className="mb-0.5">{nbMappersText}</span> </div> }
|
||||
{ durationText && <div className="flex items-center text-sm h-5 gap-0.5"> <ClockIcon className='h-full aspect-square'/> <span className="mb-0.5">{durationText}</span> </div> }
|
||||
{ (minNps && maxNps) && <div className="flex items-center text-sm h-5 gap-0.5"> <NpsIcon className='h-full aspect-square scale-95'/> <span className="mb-0.5">{`${minNpsText} - ${maxNpsText}`}</span> </div> }
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</motion.li>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
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 BeatRunning from "../../../../../assets/images/apngs/beat-running.png";
|
||||
|
||||
export const LinkPlaylistModal: ModalComponent<boolean> = ({ resolver }) => {
|
||||
const t = useTranslation();
|
||||
const [keepMaps, setKeepMaps] = useState(true);
|
||||
|
||||
// TODO : Translate
|
||||
|
||||
return (
|
||||
<form className="text-gray-800 dark:text-gray-200">
|
||||
<h1 className="text-3xl uppercase tracking-wide w-full text-center">Lier les playlists</h1>
|
||||
<BsmImage className="mx-auto h-24" image={BeatRunning} />
|
||||
<p className="max-w-sm w-full">La liaison des playlists permet de partager les playlists entre toute les version. Une fois liée, cette version profitera des playlists partagées</p>
|
||||
<p className="max-w-sm w-full text-sm italic mt-2">L'ajout et la suppression de playlists sera également partagé</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="Conserver les playlists déplacera les playlists de la version actuelle dans le dossier des playlists partagées. Dans le cas contraire elles seront perdues">
|
||||
Conserver les playlists
|
||||
</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="Lier les playlists" />
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -9,7 +9,6 @@ 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 (
|
||||
|
||||
@@ -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 BeatConflict from "../../../../../assets/images/apngs/beat-conflict.png";
|
||||
|
||||
export const UnlinkPlaylistModal: ModalComponent<boolean> = ({ resolver }) => {
|
||||
const t = useTranslation();
|
||||
const [keepMaps, setKeepMaps] = useState(true);
|
||||
|
||||
// TODO : Translate
|
||||
|
||||
return (
|
||||
<form className="text-gray-800 dark:text-gray-200">
|
||||
<h1 className="text-3xl uppercase tracking-wide w-full text-center">Délier les playlists</h1>
|
||||
<BsmImage className="mx-auto h-24" image={BeatConflict} />
|
||||
<p className="max-w-sm w-full">Attention, délier les playlists ne permettra plus l'utilisation des playlists paratagées pour cette version.</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="Conserver les playlists créera une copie des playlists partagées pour la version actuelle. Dans le cas contraire, aucune playlist ne sera conservée pour cette version.">
|
||||
Conserver les playlists
|
||||
</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="Délier les playlists" />
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -116,8 +116,8 @@ function ModelItemElement<T = unknown>(props: Props<T>) {
|
||||
<GlowEffect visible={props.selected || hovered} />
|
||||
<div className="absolute top-0 left-0 w-full h-full rounded-lg overflow-hidden blur-none bg-black shadow-sm shadow-black">
|
||||
<div ref={ref} className="contents">
|
||||
<BsmImage className="absolute top-0 left-0 w-full h-full object-cover brightness-50 scale-[200%] blur-md" image={thumbnailUrl} placeholder={defaultImage} loading="lazy" />
|
||||
<BsmImage className="absolute top-0 left-1/2 -translate-x-1/2 max-w-[20rem] w-full h-full object-cover" image={thumbnailUrl} placeholder={defaultImage} loading="lazy" />
|
||||
<BsmImage className="absolute top-0 left-0 w-full h-full object-cover brightness-50 scale-[200%] blur-md" image={thumbnailUrl} placeholder={defaultImage} />
|
||||
<BsmImage className="absolute top-0 left-1/2 -translate-x-1/2 max-w-[20rem] w-full h-full object-cover" image={thumbnailUrl} placeholder={defaultImage} />
|
||||
<div className="absolute top-0 right-0 h-full w-0 flex flex-col items-end gap-1 pt-1.5 pr-1.5">
|
||||
{!props.isDownloading ? (
|
||||
actionButtons().map((button, index) => (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CSSProperties } from "react";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { Observable } from "rxjs";
|
||||
import { Observable, map } from "rxjs";
|
||||
|
||||
type Props = {
|
||||
value$: Observable<number | string>;
|
||||
@@ -9,7 +9,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export default function TextProgressBar({ value$, className, style }: Props) {
|
||||
const value = useObservable(() => value$);
|
||||
const value = useObservable(() => value$.pipe(map(v => Math.round(Number(v)))));
|
||||
|
||||
const prefix = typeof value === "number" ? "%" : "";
|
||||
|
||||
|
||||
@@ -40,5 +40,5 @@ export const BsmImage = forwardRef<HTMLImageElement, Props>(({ className, image,
|
||||
setIsLoaded(() => true);
|
||||
};
|
||||
|
||||
return <img ref={ref} title={title} className={className} src={imageSrc} loading={loading} onLoad={handleLoaded} onError={handleError} style={styles} onClick={e => onClick?.(e)} alt=" " decoding="async" />;
|
||||
return <img ref={ref} title={title} className={className} src={imageSrc} loading={loading ?? "lazy"} onLoad={handleLoaded} onError={handleError} style={styles} onClick={e => onClick?.(e)} alt=" " decoding="async" />;
|
||||
});
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createSvgIcon } from "../svg-icon.type";
|
||||
|
||||
export const FolderOpenIcon = createSvgIcon((props, ref) => {
|
||||
|
||||
return (
|
||||
<svg ref={ref} {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor">
|
||||
<path d="M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h207q16 0 30.5 6t25.5 17l57 57h360q17 0 28.5 11.5T880-680q0 17-11.5 28.5T840-640H314q-62 0-108 39t-46 99v262l79-263q8-26 29.5-41.5T316-560h516q41 0 64.5 32.5T909-457l-72 240q-8 26-29.5 41.5T760-160H160Z"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
});
|
||||
@@ -1,10 +1,11 @@
|
||||
// These types and functions are meant replace the current clunky SVG system (the one with `bsm-icon.component.tsx`)
|
||||
// These types and functions are meant replace the current clunky SVG system (the one with `bsm-icon.component.tsx`)
|
||||
|
||||
import { ForwardedRef, forwardRef } from "react"
|
||||
import { ForwardRefExoticComponent, ForwardedRef, RefAttributes, SVGProps, forwardRef } from "react"
|
||||
|
||||
export type SvgIcon = React.ForwardRefExoticComponent<Omit<React.SVGProps<SVGSVGElement>, "ref"> & React.RefAttributes<SVGSVGElement>>;
|
||||
export type SvgRenderFunction = (props: React.SVGProps<SVGSVGElement>, ref: ForwardedRef<SVGSVGElement>) => JSX.Element;
|
||||
|
||||
export type SvgIcon = ForwardRefExoticComponent<Omit<SVGProps<SVGSVGElement>, "ref"> & RefAttributes<SVGSVGElement>>;
|
||||
export type SvgRenderFunction = (props: SVGProps<SVGSVGElement>, ref: ForwardedRef<SVGSVGElement>) => JSX.Element;
|
||||
|
||||
export function createSvgIcon(render: SvgRenderFunction): SvgIcon {
|
||||
return forwardRef(render);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function useStateMap<T = unknown, U = unknown>(value: T, map: (value: T, mappedValue: U) => U, defaultValue?: U): U {
|
||||
|
||||
const [mappedValue, setMappedValue] = useState(defaultValue ?? map(value, defaultValue));
|
||||
|
||||
useEffect(() => {
|
||||
setMappedValue(map(value, mappedValue));
|
||||
}, [value, map]);
|
||||
|
||||
return mappedValue;
|
||||
|
||||
}
|
||||
@@ -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 { FolderLinkState, VersionFolderLinkerService, VersionLinkerActionType } from "./version-folder-linker.service";
|
||||
import { FolderLinkState, VersionFolderLinkerService } from "./version-folder-linker.service";
|
||||
|
||||
export class MapsManagerService {
|
||||
private static instance: MapsManagerService;
|
||||
@@ -64,7 +64,6 @@ export class MapsManagerService {
|
||||
|
||||
return this.linker.linkVersionFolder({
|
||||
version,
|
||||
type: VersionLinkerActionType.Link,
|
||||
relativeFolder: MapsManagerService.RELATIVE_MAPS_FOLDER,
|
||||
options: { keepContents: !!modalRes.data },
|
||||
});
|
||||
@@ -79,7 +78,6 @@ export class MapsManagerService {
|
||||
|
||||
return this.linker.unlinkVersionFolder({
|
||||
version,
|
||||
type: VersionLinkerActionType.Unlink,
|
||||
relativeFolder: MapsManagerService.RELATIVE_MAPS_FOLDER,
|
||||
options: { keepContents: !!modalRes.data },
|
||||
});
|
||||
|
||||
@@ -4,6 +4,10 @@ import { Observable, lastValueFrom } from "rxjs";
|
||||
import { FolderLinkState, VersionFolderLinkerService } from "./version-folder-linker.service";
|
||||
import { Progression } from "main/helpers/fs.helpers";
|
||||
import { LocalBPList } from "shared/models/playlists/local-playlist.models";
|
||||
import { ModalExitCode, ModalService } from "./modale.service";
|
||||
import { LinkMapsModal } from "renderer/components/modal/modal-types/link-maps-modal.component";
|
||||
import { UnlinkPlaylistModal } from "renderer/components/modal/modal-types/unlink-playlist-modal.component";
|
||||
import { LinkPlaylistModal } from "renderer/components/modal/modal-types/link-playlist-modal.component";
|
||||
|
||||
export class PlaylistsManagerService {
|
||||
private static instance: PlaylistsManagerService;
|
||||
@@ -19,16 +23,46 @@ export class PlaylistsManagerService {
|
||||
|
||||
private readonly ipc: IpcService;
|
||||
private readonly linker: VersionFolderLinkerService;
|
||||
private readonly modal: ModalService;
|
||||
|
||||
private constructor() {
|
||||
this.ipc = IpcService.getInstance();
|
||||
this.linker = VersionFolderLinkerService.getInstance();
|
||||
this.modal = ModalService.getInstance();
|
||||
}
|
||||
|
||||
public getVersionPlaylists(version: BSVersion): Observable<Progression<LocalBPList[]>> {
|
||||
return this.ipc.sendV2("get-version-playlists", { args: version });
|
||||
}
|
||||
|
||||
public async linkVersion(version: BSVersion): Promise<boolean> {
|
||||
const modalRes = await this.modal.openModal(LinkPlaylistModal);
|
||||
|
||||
if (modalRes.exitCode !== ModalExitCode.COMPLETED) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
return this.linker.linkVersionFolder({
|
||||
version,
|
||||
relativeFolder: PlaylistsManagerService.RELATIVE_PLAYLISTS_FOLDER,
|
||||
options: { keepContents: !!modalRes.data },
|
||||
});
|
||||
}
|
||||
|
||||
public async unlinkVersion(version: BSVersion): Promise<boolean> {
|
||||
const modalRes = await this.modal.openModal(UnlinkPlaylistModal);
|
||||
|
||||
if (modalRes.exitCode !== ModalExitCode.COMPLETED) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
return this.linker.unlinkVersionFolder({
|
||||
version,
|
||||
relativeFolder: PlaylistsManagerService.RELATIVE_PLAYLISTS_FOLDER,
|
||||
options: { keepContents: !!modalRes.data },
|
||||
});
|
||||
}
|
||||
|
||||
public isDeepLinksEnabled(): Promise<boolean> {
|
||||
return lastValueFrom(this.ipc.sendV2<boolean>("is-playlists-deep-links-enabled"));
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ export class VersionFolderLinkerService {
|
||||
this.onVersionFolderLinked(callBack);
|
||||
});
|
||||
|
||||
this._queue$.next([...this._queue$.value, action]);
|
||||
this._queue$.next([...this._queue$.value, {...action, type: VersionLinkerActionType.Link}]);
|
||||
|
||||
return promise;
|
||||
}
|
||||
@@ -100,7 +100,7 @@ export class VersionFolderLinkerService {
|
||||
this.onVersionFolderUnlinked(callBack);
|
||||
});
|
||||
|
||||
this._queue$.next([...this._queue$.value, action]);
|
||||
this._queue$.next([...this._queue$.value, {...action, type: VersionLinkerActionType.Unlink}]);
|
||||
|
||||
return promise;
|
||||
}
|
||||
@@ -136,7 +136,7 @@ export class VersionFolderLinkerService {
|
||||
if(currentAction && equal(currentAction.version, version) && currentAction.relativeFolder === relativeFolder) {
|
||||
return of(FolderLinkState.Processing)
|
||||
}
|
||||
|
||||
|
||||
if(queue.some(action => equal(action.version, version) && action.relativeFolder === relativeFolder)) {
|
||||
return of(FolderLinkState.Pending);
|
||||
}
|
||||
@@ -146,7 +146,7 @@ export class VersionFolderLinkerService {
|
||||
);
|
||||
}),
|
||||
distinctUntilChanged(),
|
||||
shareReplay(1)
|
||||
shareReplay(1)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -179,14 +179,8 @@ export interface VersionLinkerAction {
|
||||
options?: LinkOptions;
|
||||
}
|
||||
|
||||
export interface VersionLinkFolderAction extends VersionLinkerAction {
|
||||
type: VersionLinkerActionType.Link;
|
||||
}
|
||||
|
||||
export interface VersionUnlinkFolderAction extends VersionLinkerAction {
|
||||
type: VersionLinkerActionType.Unlink;
|
||||
options?: UnlinkOptions;
|
||||
}
|
||||
export type VersionLinkFolderAction = Omit<VersionLinkerAction, "type">;
|
||||
export type VersionUnlinkFolderAction = Omit<VersionLinkerAction, "type">;
|
||||
|
||||
export type VersionLinkerActionListener = (action: VersionLinkerAction, linked: boolean) => void;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user