diff --git a/.eslintrc.js b/.eslintrc.js index d0de28b5..019724e6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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 diff --git a/src/main/services/additional-content/maps/local-maps-manager.service.ts b/src/main/services/additional-content/maps/local-maps-manager.service.ts index 21bf9fca..38e75f4a 100644 --- a/src/main/services/additional-content/maps/local-maps-manager.service.ts +++ b/src/main/services/additional-content/maps/local-maps-manager.service.ts @@ -116,7 +116,7 @@ export class LocalMapsManagerService { return shasum.digest("hex"); } - private async loadMapInfoFromPath(mapPath: string): Promise { + public async loadMapInfoFromPath(mapPath: string): Promise { const getUrlsAndReturn = (rawInfo: RawMapInfoData, hash: string, mapPath: string) => { const coverUrl = pathToFileURL(path.join(mapPath, rawInfo._coverImageFilename)).href; diff --git a/src/renderer/components/available-versions/available-version-item.component.tsx b/src/renderer/components/available-versions/available-version-item.component.tsx index 605c054d..2246682b 100644 --- a/src/renderer/components/available-versions/available-version-item.component.tsx +++ b/src/renderer/components/available-versions/available-version-item.component.tsx @@ -29,8 +29,8 @@ export const AvailableVersionItem = memo(function AvailableVersionItem({version, {version.recommended && ( {t("pages.available-versions.recommended")} )} - - + +

{version.BSVersion}

diff --git a/src/renderer/components/maps-playlists-panel/maps-playlists-panel.component.tsx b/src/renderer/components/maps-playlists-panel/maps-playlists-panel.component.tsx index 01f14efc..4a5aaa15 100644 --- a/src/renderer/components/maps-playlists-panel/maps-playlists-panel.component.tsx +++ b/src/renderer/components/maps-playlists-panel/maps-playlists-panel.component.tsx @@ -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, }, ]} diff --git a/src/renderer/components/maps-playlists-panel/maps/map-item.component.tsx b/src/renderer/components/maps-playlists-panel/maps/map-item.component.tsx index b94c1d09..331d1db5 100644 --- a/src/renderer/components/maps-playlists-panel/maps/map-item.component.tsx +++ b/src/renderer/components/maps-playlists-panel/maps/map-item.component.tsx @@ -178,7 +178,7 @@ export const MapItem = memo(({ hash, title, autor, songAutor, coverUrl, songUrl,
- +
- +

diff --git a/src/renderer/components/maps-playlists-panel/playlists/local-playlists-list-panel.component.tsx b/src/renderer/components/maps-playlists-panel/playlists/local-playlists-list-panel.component.tsx index 32df4cc8..efd76a7d 100644 --- a/src/renderer/components/maps-playlists-panel/playlists/local-playlists-list-panel.component.tsx +++ b/src/renderer/components/maps-playlists-panel/playlists/local-playlists-list-panel.component.tsx @@ -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(({ version, cl const [playlistsLoading, setPlaylistsLoading] = useState(false); const [playlists, setPlaylists] = useState([]); const loadPercent$ = useConstant(() => new BehaviorSubject(0)); + const linked = useStateMap(linkedState, (newState, precMapped) => (newState === FolderLinkState.Pending || newState === FolderLinkState.Processing) ? precMapped : newState === FolderLinkState.Linked, false); const loadPlaylists = (): Promise => { 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(({ 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(({ version, cl loadPercent$.next(0); }); - }, [isActiveOnce, version]); + }, [isActiveOnce, version, linked]); const render = () => { if(playlistsLoading){ @@ -114,7 +111,6 @@ export const LocalPlaylistsListPanel = forwardRef(({ version, cl duration={getDurationOfPlaylist(p)} maxNps={getMaxNpsOfPlaylist(p)} minNps={getMinNpsOfPlaylist(p)} - songs={getSongsOfPlaylist(p)} /> )} diff --git a/src/renderer/components/maps-playlists-panel/playlists/playlist-item.component.tsx b/src/renderer/components/maps-playlists-panel/playlists/playlist-item.component.tsx index 75d95f77..d549f544 100644 --- a/src/renderer/components/maps-playlists-panel/playlists/playlist-item.component.tsx +++ b/src/renderer/components/maps-playlists-panel/playlists/playlist-item.component.tsx @@ -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 ( - startPlay()}> -
- -
-
-
- -
-
-

{title}

-

Créé par {author}

- -
- { nbMapsText &&
{nbMapsText}
} - { nbMappersText &&
{nbMappersText}
} - { durationText &&
{durationText}
} - { (minNps && maxNps) &&
{`${minNpsText} - ${maxNpsText}`}
} + setHovered(() => true)} onHoverEnd={() => setHovered(() => false)} > + +
+
+ +
+
+ + +
+
+

{title}

+

Créé par {author}

+
+ { nbMapsText &&
{nbMapsText}
} + { nbMappersText &&
{nbMappersText}
} + { durationText &&
{durationText}
} + { (minNps && maxNps) &&
{`${minNpsText} - ${maxNpsText}`}
} +
+ +
+ ) } diff --git a/src/renderer/components/modal/modal-types/link-playlist-modal.component.tsx b/src/renderer/components/modal/modal-types/link-playlist-modal.component.tsx new file mode 100644 index 00000000..a0d26f1d --- /dev/null +++ b/src/renderer/components/modal/modal-types/link-playlist-modal.component.tsx @@ -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 = ({ resolver }) => { + const t = useTranslation(); + const [keepMaps, setKeepMaps] = useState(true); + + // TODO : Translate + + return ( +
+

Lier les playlists

+ +

La liaison des playlists permet de partager les playlists entre toute les version. Une fois liée, cette version profitera des playlists partagées

+

L'ajout et la suppression de playlists sera également partagé

+
+ + + Conserver les playlists + +
+
+ resolver({ exitCode: ModalExitCode.CANCELED })} withBar={false} text="misc.cancel" /> + resolver({ exitCode: ModalExitCode.COMPLETED, data: keepMaps })} withBar={false} text="Lier les playlists" /> +
+ + ); +}; diff --git a/src/renderer/components/modal/modal-types/models/link-models-modal.component.tsx b/src/renderer/components/modal/modal-types/models/link-models-modal.component.tsx index 88d51f86..ecaafbc7 100644 --- a/src/renderer/components/modal/modal-types/models/link-models-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/models/link-models-modal.component.tsx @@ -9,7 +9,6 @@ import BeatRunning from "../../../../../../assets/images/apngs/beat-running.png" export const LinkModelsModal: ModalComponent = ({ resolver, data }) => { const t = useTranslation(); - const [keepMaps, setKeepMaps] = useState(true); return ( diff --git a/src/renderer/components/modal/modal-types/unlink-playlist-modal.component.tsx b/src/renderer/components/modal/modal-types/unlink-playlist-modal.component.tsx new file mode 100644 index 00000000..34b1a46b --- /dev/null +++ b/src/renderer/components/modal/modal-types/unlink-playlist-modal.component.tsx @@ -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 = ({ resolver }) => { + const t = useTranslation(); + const [keepMaps, setKeepMaps] = useState(true); + + // TODO : Translate + + return ( +
+

Délier les playlists

+ +

Attention, délier les playlists ne permettra plus l'utilisation des playlists paratagées pour cette version.

+
+ + + Conserver les playlists + +
+
+ resolver({ exitCode: ModalExitCode.CANCELED })} withBar={false} text="misc.cancel" /> + resolver({ exitCode: ModalExitCode.COMPLETED, data: keepMaps })} withBar={false} text="Délier les playlists" /> +
+ + ); +}; diff --git a/src/renderer/components/models-management/model-item.component.tsx b/src/renderer/components/models-management/model-item.component.tsx index cbb8555a..ea0b90fb 100644 --- a/src/renderer/components/models-management/model-item.component.tsx +++ b/src/renderer/components/models-management/model-item.component.tsx @@ -116,8 +116,8 @@ function ModelItemElement(props: Props) {
- - + +
{!props.isDownloading ? ( actionButtons().map((button, index) => ( diff --git a/src/renderer/components/progress-bar/text-progress-bar.component.tsx b/src/renderer/components/progress-bar/text-progress-bar.component.tsx index 81964e87..86d1028b 100644 --- a/src/renderer/components/progress-bar/text-progress-bar.component.tsx +++ b/src/renderer/components/progress-bar/text-progress-bar.component.tsx @@ -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; @@ -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" ? "%" : ""; diff --git a/src/renderer/components/shared/bsm-image.component.tsx b/src/renderer/components/shared/bsm-image.component.tsx index bddb0914..fc316b40 100644 --- a/src/renderer/components/shared/bsm-image.component.tsx +++ b/src/renderer/components/shared/bsm-image.component.tsx @@ -40,5 +40,5 @@ export const BsmImage = forwardRef(({ className, image, setIsLoaded(() => true); }; - return onClick?.(e)} alt=" " decoding="async" />; + return onClick?.(e)} alt=" " decoding="async" />; }); diff --git a/src/renderer/components/svgs/icons/folder-open-icon.component.tsx b/src/renderer/components/svgs/icons/folder-open-icon.component.tsx new file mode 100644 index 00000000..6107000c --- /dev/null +++ b/src/renderer/components/svgs/icons/folder-open-icon.component.tsx @@ -0,0 +1,11 @@ +import { createSvgIcon } from "../svg-icon.type"; + +export const FolderOpenIcon = createSvgIcon((props, ref) => { + + return ( + + + + ); + +}); diff --git a/src/renderer/components/svgs/svg-icon.type.ts b/src/renderer/components/svgs/svg-icon.type.ts index 67e58193..380c07c2 100644 --- a/src/renderer/components/svgs/svg-icon.type.ts +++ b/src/renderer/components/svgs/svg-icon.type.ts @@ -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, "ref"> & React.RefAttributes>; -export type SvgRenderFunction = (props: React.SVGProps, ref: ForwardedRef) => JSX.Element; + +export type SvgIcon = ForwardRefExoticComponent, "ref"> & RefAttributes>; +export type SvgRenderFunction = (props: SVGProps, ref: ForwardedRef) => JSX.Element; export function createSvgIcon(render: SvgRenderFunction): SvgIcon { return forwardRef(render); -} \ No newline at end of file +} diff --git a/src/renderer/hooks/use-state-map.hook.ts b/src/renderer/hooks/use-state-map.hook.ts new file mode 100644 index 00000000..347d95e3 --- /dev/null +++ b/src/renderer/hooks/use-state-map.hook.ts @@ -0,0 +1,13 @@ +import { useEffect, useState } from "react"; + +export function useStateMap(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; + +} diff --git a/src/renderer/services/maps-manager.service.ts b/src/renderer/services/maps-manager.service.ts index e973b859..8aeed33f 100644 --- a/src/renderer/services/maps-manager.service.ts +++ b/src/renderer/services/maps-manager.service.ts @@ -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 }, }); diff --git a/src/renderer/services/playlists-manager.service.ts b/src/renderer/services/playlists-manager.service.ts index f45ebee3..9aca80cc 100644 --- a/src/renderer/services/playlists-manager.service.ts +++ b/src/renderer/services/playlists-manager.service.ts @@ -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> { return this.ipc.sendV2("get-version-playlists", { args: version }); } + public async linkVersion(version: BSVersion): Promise { + 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 { + 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 { return lastValueFrom(this.ipc.sendV2("is-playlists-deep-links-enabled")); } diff --git a/src/renderer/services/version-folder-linker.service.ts b/src/renderer/services/version-folder-linker.service.ts index 936de9d6..dcc2df4a 100644 --- a/src/renderer/services/version-folder-linker.service.ts +++ b/src/renderer/services/version-folder-linker.service.ts @@ -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; +export type VersionUnlinkFolderAction = Omit; export type VersionLinkerActionListener = (action: VersionLinkerAction, linked: boolean) => void;