[feature-107] moving files around and create Playlist panel component

This commit is contained in:
MathieuG-P
2024-02-07 16:36:25 +01:00
parent 1b4b59fbf0
commit 9a3ebf2971
17 changed files with 157 additions and 130 deletions
+10 -18
View File
@@ -1,8 +1,6 @@
import { ipcMain } from "electron";
import { IpcRequest } from "shared/models/ipc";
import { LocalPlaylistsManagerService } from "../services/additional-content/local-playlists-manager.service";
import { UtilsService } from "../services/utils.service";
import { IpcService } from "../services/ipc.service";
import { of, throwError } from "rxjs";
const ipc = IpcService.getInstance();
@@ -11,38 +9,32 @@ ipc.on<string>("one-click-install-playlist", (req, reply) => {
reply(mapsManager.oneClickInstallPlaylist(req.args));
});
ipcMain.on("register-playlists-deep-link", async (event, request: IpcRequest<void>) => {
ipc.on("register-playlists-deep-link", (_, reply) => {
const maps = LocalPlaylistsManagerService.getInstance();
const utils = UtilsService.getInstance();
try {
const res = maps.enableDeepLinks();
utils.ipcSend(request.responceChannel, { success: true, data: res });
reply(of(maps.enableDeepLinks()));
} catch (e) {
utils.ipcSend(request.responceChannel, { success: false });
reply(throwError(() => e));
}
});
ipcMain.on("unregister-playlists-deep-link", async (event, request: IpcRequest<void>) => {
ipc.on("unregister-playlists-deep-link", (_, reply) => {
const maps = LocalPlaylistsManagerService.getInstance();
const utils = UtilsService.getInstance();
try {
const res = maps.disableDeepLinks();
utils.ipcSend(request.responceChannel, { success: true, data: res });
reply(of(maps.disableDeepLinks()));
} catch (e) {
utils.ipcSend(request.responceChannel, { success: false });
reply(throwError(() => e));
}
});
ipcMain.on("is-playlists-deep-links-enabled", async (event, request: IpcRequest<void>) => {
ipc.on("is-playlists-deep-links-enabled", (_, reply) => {
const maps = LocalPlaylistsManagerService.getInstance();
const utils = UtilsService.getInstance();
try {
const res = maps.isDeepLinksEnabled();
utils.ipcSend(request.responceChannel, { success: true, data: res });
reply(of(maps.isDeepLinksEnabled()));
} catch (e) {
utils.ipcSend(request.responceChannel, { success: false });
reply(throwError(() => e));
}
});
@@ -1,15 +1,13 @@
import { useEffect, useRef, useState } from "react";
import { useRef, useState } from "react";
import { BSVersion } from "shared/bs-version.interface";
import { LocalMapsListPanel } from "./local-maps-list-panel.component";
import { LocalMapsListPanel } from "./maps/local-maps-list-panel.component";
import { BsmDropdownButton, DropDownItem } from "../shared/bsm-dropdown-button.component";
import { FilterPanel } from "./filter-panel.component";
import { FilterPanel } from "./maps/filter-panel.component";
import { MapFilter } from "shared/models/maps/beat-saver.model";
import { MapsManagerService } from "renderer/services/maps-manager.service";
import { MapsDownloaderService } from "renderer/services/maps-downloader.service";
import { BsmImage } from "../shared/bsm-image.component";
import wipGif from "../../../../assets/images/gifs/wip.gif";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { FolderLinkState, VersionFolderLinkerService, VersionLinkerActionListener } from "renderer/services/version-folder-linker.service";
import { FolderLinkState } 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";
@@ -17,6 +15,8 @@ import { MapIcon } from "../svgs/icons/map-icon.component";
import { PlaylistIcon } from "../svgs/icons/playlist-icon.component";
import { useObservable } from "renderer/hooks/use-observable.hook";
import { of } from "rxjs";
import { LocalPlaylistsListPanel } from "./playlists/local-playlists-list-panel.component";
import { PlaylistsManagerService } from "renderer/services/playlists-manager.service";
type Props = {
version?: BSVersion;
@@ -24,46 +24,28 @@ type Props = {
};
export function MapsPlaylistsPanel({ version, isActive }: Props) {
const mapsService = useService(MapsManagerService);
const mapsDownloader = useService(MapsDownloaderService);
const linker = useService(VersionFolderLinkerService);
const playlistsService = useService(PlaylistsManagerService);
const t = useTranslation();
const [tabIndex, setTabIndex] = useState(0);
const mapsRef = useRef<any>();
const [mapFilter, setMapFilter] = useState<MapFilter>({});
const [mapSearch, setMapSearch] = useState("");
const mapsLinkedState = useObservable(() => {
if(!version) return of(FolderLinkState.Unlinked);
return mapsService.$mapsFolderLinkState(version);
}, FolderLinkState.Unlinked, [version]);
const [playlistSearch, setPlaylistSearch] = useState("");
const [mapsLinked, setMapsLinked] = useState(false);
const mapsLinkedState = useObservable(() => version ? mapsService.$mapsFolderLinkState(version) : of(null), FolderLinkState.Unlinked, [version]);
const t = useTranslation();
const mapsRef = useRef<any>();
const playlistLinkedState = useObservable(() => {
if(!version) return of(FolderLinkState.Unlinked);
return playlistsService.$playlistsFolderLinkState(version);
}, FolderLinkState.Unlinked, [version]);
useEffect(() => {
if (!version) {
return;
}
loadMapIsLinked();
const onMapsLinked: VersionLinkerActionListener = action => {
if (!action.relativeFolder.includes(MapsManagerService.RELATIVE_MAPS_FOLDER)) {
return;
}
loadMapIsLinked();
};
linker.onVersionFolderLinked(onMapsLinked);
linker.onVersionFolderUnlinked(onMapsLinked);
return () => {
linker.removeVersionFolderLinkedListener(onMapsLinked);
linker.removeVersionFolderUnlinkedListener(onMapsLinked);
};
}, [version, isActive]);
const loadMapIsLinked = () => {
mapsService.versionHaveMapsLinked(version).then(setMapsLinked);
};
const handleSearch = (value: string) => {
if (tabIndex === 0) {
@@ -77,9 +59,12 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
};
const handleMapsLinkClick = () => {
if (!mapsLinked) {
if(mapsLinkedState === FolderLinkState.Pending || mapsLinkedState === FolderLinkState.Processing){ return Promise.resolve(false); }
if (mapsLinkedState === FolderLinkState.Unlinked) {
return mapsService.linkVersion(version);
}
return mapsService.unlinkVersion(version);
};
@@ -130,15 +115,16 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
text: "misc.playlists",
icon: PlaylistIcon,
onClick: () => setTabIndex(1),
linkProps: version ? {
state: playlistLinkedState,
onClick: () => Promise.resolve(false),
} : null,
},
]}
>
<>
<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>
<LocalMapsListPanel isActive={isActive && tabIndex === 0} ref={mapsRef} className="w-full h-full shrink-0" version={version} filter={mapFilter} search={mapSearch} linkedState={mapsLinkedState} />
<LocalPlaylistsListPanel isActive={isActive && tabIndex === 1} version={version} linkedState={playlistLinkedState}/>
</>
</BsContentTabPanel>
</div>
@@ -3,18 +3,18 @@ import { motion } from "framer-motion";
import { MutableRefObject, useEffect, useRef, useState } from "react";
import { MAP_TYPES } from "renderer/partials/maps/map-tags/map-types";
import { MAP_STYLES } from "renderer/partials/maps/map-tags/map-styles";
import { BsmCheckbox } from "../shared/bsm-checkbox.component";
import { minToS } from "../../../shared/helpers/time.helpers";
import { BsmCheckbox } from "../../shared/bsm-checkbox.component";
import { minToS } from "../../../../shared/helpers/time.helpers";
import dateFormat from "dateformat";
import { BsmRange } from "../shared/bsm-range.component";
import { BsmRange } from "../../shared/bsm-range.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { MAP_SPECIFICITIES } from "renderer/partials/maps/map-general/map-specificity";
import { MAP_REQUIREMENTS } from "renderer/partials/maps/map-requirements/map-requirements";
import { MAP_DIFFICULTIES_COLORS } from "renderer/partials/maps/map-difficulties/map-difficulties-colors";
import { BsmButton } from "../shared/bsm-button.component";
import { BsmButton } from "../../shared/bsm-button.component";
import equal from "fast-deep-equal/es6";
import clone from "rfdc";
import { GlowEffect } from "../shared/glow-effect.component";
import { GlowEffect } from "../../shared/glow-effect.component";
export type Props = {
className?: string;
@@ -11,24 +11,26 @@ import { debounceTime, last, mergeMap, tap } from "rxjs/operators";
import { BeatSaverService } from "renderer/services/thrird-partys/beat-saver.service";
import { OsDiagnosticService } from "renderer/services/os-diagnostic.service";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import BeatWaitingImg from "../../../../assets/images/apngs/beat-waiting.png";
import BeatConflict from "../../../../assets/images/apngs/beat-conflict.png";
import { BsmImage } from "../shared/bsm-image.component";
import { BsmButton } from "../shared/bsm-button.component";
import TextProgressBar from "../progress-bar/text-progress-bar.component";
import { useChangeOnce } from "renderer/hooks/use-change-once.hook";
import BeatWaitingImg from "../../../../../assets/images/apngs/beat-waiting.png";
import BeatConflict from "../../../../../assets/images/apngs/beat-conflict.png";
import { BsmImage } from "../../shared/bsm-image.component";
import { BsmButton } from "../../shared/bsm-button.component";
import TextProgressBar from "../../progress-bar/text-progress-bar.component";
import { useChangeUntilEqual } from "renderer/hooks/use-change-until-equal.hook";
import { useService } from "renderer/hooks/use-service.hook";
import { FolderLinkState } from "renderer/services/version-folder-linker.service";
import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
type Props = {
version: BSVersion;
className?: string;
filter?: MapFilter;
search?: string;
linked?: boolean;
linkedState?: FolderLinkState;
isActive?: boolean;
};
export const LocalMapsListPanel = forwardRef(({ version, className, filter, search, linked, isActive }: Props, forwardRef) => {
export const LocalMapsListPanel = forwardRef(({ version, className, filter, search, linkedState, isActive }: Props, forwardRef) => {
const mapsManager = useService(MapsManagerService);
const mapsDownloader = useService(MapsDownloaderService);
const bsaver = useService(BeatSaverService);
@@ -41,7 +43,8 @@ export const LocalMapsListPanel = forwardRef(({ version, className, filter, sear
const [selectedMaps$] = useState(new BehaviorSubject<BsmLocalMap[]>([]));
const [itemPerRow, setItemPerRow] = useState(2);
const [listHeight, setListHeight] = useState(0);
const isActiveOnce = useChangeOnce(isActive, true);
const isActiveOnce = useChangeUntilEqual(isActive, { untilEqual: true, emitOnEqual: true });
const [linked, setLinked] = useState(false);
const [loadPercent$] = useState(new BehaviorSubject(0));
@@ -68,6 +71,11 @@ export const LocalMapsListPanel = forwardRef(({ version, className, filter, sear
[selectedMaps$.value, maps, version]
);
useOnUpdate(() => {
if(linkedState === FolderLinkState.Pending || linkedState === FolderLinkState.Processing) return () => {};
setLinked(linkedState === FolderLinkState.Linked);
}, [linkedState]);
useEffect(() => {
if (isActiveOnce) {
loadMaps();
@@ -1,9 +1,9 @@
import { BsmImage } from "../shared/bsm-image.component";
import { BsmImage } from "../../shared/bsm-image.component";
import { BsvMapCharacteristic, BsvMapDifficultyType } from "shared/models/maps/beat-saver.model";
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
import { BsmLink } from "../shared/bsm-link.component";
import { BsmIcon } from "../svgs/bsm-icon.component";
import { BsmButton } from "../shared/bsm-button.component";
import { BsmLink } from "../../shared/bsm-link.component";
import { BsmIcon } from "../../svgs/bsm-icon.component";
import { BsmButton } from "../../shared/bsm-button.component";
import { AnimatePresence, motion } from "framer-motion";
import { useState, Fragment, memo, useRef } from "react";
import { LinkOpenerService } from "renderer/services/link-opener.service";
@@ -12,13 +12,13 @@ import { AudioPlayerService } from "renderer/services/audio-player.service";
import { useObservable } from "renderer/hooks/use-observable.hook";
import { map } from "rxjs/operators";
import equal from "fast-deep-equal/es6";
import { BsmBasicSpinner } from "../shared/bsm-basic-spinner/bsm-basic-spinner.component";
import defaultImage from "../../../../assets/images/default-version-img.jpg";
import { BsmBasicSpinner } from "../../shared/bsm-basic-spinner/bsm-basic-spinner.component";
import defaultImage from "../../../../../assets/images/default-version-img.jpg";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { MAP_DIFFICULTIES } from "renderer/partials/maps/map-difficulties/map-difficulties";
import { MAP_DIFFICULTIES_COLORS } from "renderer/partials/maps/map-difficulties/map-difficulties-colors";
import useDoubleClick from "use-double-click";
import { GlowEffect } from "../shared/glow-effect.component";
import { GlowEffect } from "../../shared/glow-effect.component";
import { useDelayedState } from "renderer/hooks/use-delayed-state.hook";
import { useService } from "renderer/hooks/use-service.hook";
import Tippy from "@tippyjs/react";
@@ -0,0 +1,15 @@
import { FolderLinkState } from "renderer/services/version-folder-linker.service";
import { BSVersion } from "shared/bs-version.interface";
type Props = {
version: BSVersion;
className?: string;
linkedState?: FolderLinkState;
isActive?: boolean;
};
export function LocalPlaylistsListPanel({ version, className, isActive, linkedState }: Props) {
return (
<div>local-playlists-list-panel.component</div>
)
}
@@ -1,7 +1,7 @@
import { motion } from "framer-motion";
import { useCallback, useEffect, useRef, useState } from "react";
import { FilterPanel } from "renderer/components/maps-mangement-components/filter-panel.component";
import { MapItem, ParsedMapDiff } from "renderer/components/maps-mangement-components/map-item.component";
import { FilterPanel } from "renderer/components/maps-playlists-panel/maps/filter-panel.component";
import { MapItem, ParsedMapDiff } from "renderer/components/maps-playlists-panel/maps/map-item.component";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { BsmDropdownButton } from "renderer/components/shared/bsm-dropdown-button.component";
import { BsmSelect, BsmSelectOption } from "renderer/components/shared/bsm-select.component";
@@ -1,6 +1,6 @@
import Tippy from "@tippyjs/react";
import { useEffect, useState } from "react";
import { LinkButton } from "renderer/components/maps-mangement-components/link-button.component";
import { LinkButton } from "renderer/components/shared/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 { useObservable } from "renderer/hooks/use-observable.hook";
@@ -137,7 +137,7 @@ const FolderItem = ({ version, relativeFolder, onDelete }: FolderProps) => {
</span>
<div className="flex flex-row gap-1.5">
<Tippy placement="left" content={t(`modals.shared-folders.buttons.${state === FolderLinkState.Linked ? "unlink-folder" : "link-folder"}`)} arrow={false}>
<LinkButton
<LinkButton
className="p-0.5 h-7 shrink-0 aspect-square blur-0 cursor-pointer hover:brightness-75"
state={state}
onClick={onClickLink}
@@ -1,4 +1,4 @@
import { LinkBtnProps, LinkButton } from "renderer/components/maps-mangement-components/link-button.component";
import { LinkBtnProps, LinkButton } from "renderer/components/shared/link-button.component";
import { SvgIcon } from "renderer/components/svgs/svg-icon.type";
import { useTranslation } from "renderer/hooks/use-translation.hook";
@@ -32,13 +32,13 @@ export const BsContentTabItem: BsContentTabItemComponent = ({ text, icon: Icon,
{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}
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>
);
}
}
@@ -3,6 +3,7 @@ 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 React from "react";
export type LinkBtnProps = {
className?: string;
@@ -11,9 +12,9 @@ export type LinkBtnProps = {
onClick?: () => unknown;
};
export const LinkButton = ({className, title, state, onClick}: LinkBtnProps) => {
export function LinkButton({className, title, state, onClick}: LinkBtnProps) {
const t = useTranslation();
const color = useThemeColor("first-color");
const disabled = state === FolderLinkState.Processing || state === FolderLinkState.Pending;
@@ -34,14 +35,15 @@ export const LinkButton = ({className, title, state, onClick}: LinkBtnProps) =>
const handleClick = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
e.preventDefault();
e.stopPropagation();
!disabled && onClick?.();
if(disabled){ return; }
onClick?.();
}
return (
<motion.div
variants={{ hover: { rotate: 22.5 }, tap: { rotate: 45 } }}
whileHover="hover"
whileTap="tap"
whileHover="hover"
whileTap="tap"
initial={{ rotate: 0 }}
className={className}
title={title ? t(title) : null}
@@ -1,18 +0,0 @@
import equal from "fast-deep-equal";
import { useEffect, useRef, useState } from "react";
export function useChangeOnce<T = unknown>(initialValue: T, onlyFirstIfTruthy?: boolean): T {
const [trackedValue, setTrackedValue] = useState<T>(initialValue);
const didChangeOnceRef = useRef<boolean>(onlyFirstIfTruthy ? !!initialValue : false);
useEffect(() => {
if (didChangeOnceRef.current || equal(initialValue, trackedValue)) {
return;
}
setTrackedValue(() => initialValue);
didChangeOnceRef.current = true;
}, [initialValue]);
return trackedValue;
}
@@ -0,0 +1,35 @@
import equal from "fast-deep-equal";
import { useEffect, useRef, useState } from "react";
type Options<T = unknown> = {
untilEqual: T;
emitOnEqual?: boolean;
}
export function useChangeUntilEqual<T = unknown>(variableValue: T, { untilEqual, emitOnEqual }: Options<T>): T {
const [value, setValue] = useState(variableValue);
const untilEqualRef = useRef(untilEqual);
const emitOnEqualRef = useRef(emitOnEqual);
const hasBeenEqual = useRef(false);
useEffect(() => {
if(hasBeenEqual.current) {
return;
}
const isEqual = equal(variableValue, untilEqualRef.current);
if(!isEqual){
return setValue(variableValue);
}
hasBeenEqual.current = true;
if(emitOnEqualRef.current) {
setValue(variableValue);
}
}, [variableValue]);
return value;
}
@@ -1,5 +1,5 @@
import { useState } from "react";
import { MapsPlaylistsPanel } from "renderer/components/maps-mangement-components/maps-playlists-panel.component";
import { MapsPlaylistsPanel } from "renderer/components/maps-playlists-panel/maps-playlists-panel.component";
import { ModelsPanel } from "renderer/components/models-management/models-panel.component";
import { TabNavBar } from "renderer/components/shared/tab-nav-bar.component";
import { Slideshow } from "renderer/components/slideshow/slideshow.component";
@@ -12,7 +12,7 @@ import { IpcService } from "renderer/services/ipc.service";
import { LaunchSlide } from "renderer/components/version-viewer/slides/launch/launch-slide.component";
import { ModsSlide } from "renderer/components/version-viewer/slides/mods/mods-slide.component";
import { UninstallModal } from "renderer/components/modal/modal-types/uninstall-modal.component";
import { MapsPlaylistsPanel } from "renderer/components/maps-mangement-components/maps-playlists-panel.component";
import { MapsPlaylistsPanel } from "renderer/components/maps-playlists-panel/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";
import { useService } from "renderer/hooks/use-service.hook";
@@ -101,7 +101,9 @@ export class MapsManagerService {
const progress$ = this.ipcService.sendV2<DeleteMapsProgress>("delete-maps", { args: maps }).pipe(map(progress => (progress.deleted / progress.total) * 100));
showProgressBar && this.progressBar.show(progress$, true);
if (showProgressBar) {
this.progressBar.show(progress$, true);
}
progress$.toPromise().finally(() => this.progressBar.hide(true));
@@ -175,10 +177,6 @@ export class MapsManagerService {
return this.lastUnlinkedVersion$.asObservable();
}
public $mapsLinkingPending(version: BSVersion): Observable<boolean> {
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,4 +1,7 @@
import { BSVersion } from "shared/bs-version.interface";
import { IpcService } from "./ipc.service";
import { Observable, lastValueFrom } from "rxjs";
import { FolderLinkState, VersionFolderLinkerService } from "./version-folder-linker.service";
export class PlaylistsManagerService {
private static instance: PlaylistsManagerService;
@@ -10,23 +13,29 @@ export class PlaylistsManagerService {
return PlaylistsManagerService.instance;
}
public static readonly RELATIVE_PLAYLISTS_FOLDER = "Playlists";
private readonly ipc: IpcService;
private readonly linker: VersionFolderLinkerService;
private constructor() {
this.ipc = IpcService.getInstance();
this.linker = VersionFolderLinkerService.getInstance();
}
public isDeepLinksEnabled(): Promise<boolean> {
return this.ipc.send<boolean>("is-playlists-deep-links-enabled").then(res => (res.success ? res.data : false));
return lastValueFrom(this.ipc.sendV2<boolean>("is-playlists-deep-links-enabled"));
}
public async enableDeepLink(): Promise<boolean> {
const res = await this.ipc.send<boolean>("register-playlists-deep-link");
return res.success ? res.data : false;
public enableDeepLink(): Promise<boolean> {
return lastValueFrom(this.ipc.sendV2<boolean>("register-playlists-deep-link"));
}
public async disableDeepLink(): Promise<boolean> {
const res = await this.ipc.send<boolean>("unregister-playlists-deep-link");
return res.success ? res.data : false;
public disableDeepLink(): Promise<boolean> {
return lastValueFrom(this.ipc.sendV2<boolean>("unregister-playlists-deep-link"));
}
public $playlistsFolderLinkState(version: BSVersion): Observable<FolderLinkState> {
return this.linker.$folderLinkedState(version, PlaylistsManagerService.RELATIVE_PLAYLISTS_FOLDER);
}
}