From 1e6f051f59f0c7d159f802dad9b85a6e141fcfe4 Mon Sep 17 00:00:00 2001 From: MathieuG-P <40181755+Zagrios@users.noreply.github.com> Date: Sat, 2 Mar 2024 17:50:08 +0100 Subject: [PATCH] [feature-107] can now list maps of playlist + lot of other things --- src/main/ipcs/bs-playlist-ipcs.ts | 4 +- .../local-playlists-manager.service.ts | 71 ++++++----- .../maps-playlists-panel.component.tsx | 17 ++- .../maps/local-maps-list-panel.component.tsx | 24 ++-- .../maps/map-item.component.tsx | 2 +- .../maps/maps-row.component.tsx | 60 ++++++---- .../local-playlists-list-panel.component.tsx | 113 +++++++++--------- .../playlists/playlist-item.component.tsx | 30 +++-- .../login-to-steam-modal.component.tsx | 10 +- .../steam-mobile-approve-modal.component.tsx | 6 +- .../changelog-modal.component.tsx | 2 +- ...create-launch-shortcut-modal.component.tsx | 10 +- .../delete-maps-modal.component.tsx | 4 +- .../download-maps-modal.component.tsx | 37 +++--- .../edit-version-modal.component.tsx | 2 +- .../models/delete-models-modal.component.tsx | 2 +- .../download-models-modal.component.tsx | 2 +- .../models/link-models-modal.component.tsx | 2 +- .../models/unlink-models-modal.component.tsx | 2 +- ...local-playlist-details-modal.component.tsx | 57 +++++++++ .../playlist-details-template.component.tsx | 66 ++++++++++ .../share-folders-modal.component.tsx | 2 +- .../uninstall-all-mods-modal.component.tsx | 2 +- .../uninstall-mod-modal.component.tsx | 2 +- .../modal-types/uninstall-modal.component.tsx | 2 +- .../components/modal/modal.component.tsx | 69 ++++++----- .../bs-content-tab-panel.component.tsx | 8 +- ...theme-color-gradient-spliter.component.tsx | 11 ++ .../svgs/icons/search-icon.component.tsx | 8 +- .../pages/version-viewer.component.tsx | 6 +- src/renderer/services/auto-updater.service.ts | 2 +- .../services/bs-mods-manager.service.ts | 4 +- .../steam-downloader.service.ts | 16 +-- .../services/bs-version-manager.service.ts | 6 +- .../services/maps-downloader.service.ts | 2 +- src/renderer/services/maps-manager.service.ts | 2 +- src/renderer/services/modale.service.ts | 46 +++---- .../models-downloader.service.ts | 2 +- .../models-manager.service.ts | 8 +- .../services/playlists-manager.service.ts | 7 +- src/renderer/windows/App.tsx | 4 + src/shared/models/maps/beat-saver.model.ts | 6 +- .../models/playlists/local-playlist.models.ts | 19 +-- .../models/playlists/playlist.interface.ts | 6 +- 44 files changed, 472 insertions(+), 291 deletions(-) create mode 100644 src/renderer/components/modal/modal-types/playlist/local-playlist-details-modal.component.tsx create mode 100644 src/renderer/components/modal/modal-types/playlist/playlist-details-template.component.tsx create mode 100644 src/renderer/components/shared/theme-color-gradient-spliter.component.tsx diff --git a/src/main/ipcs/bs-playlist-ipcs.ts b/src/main/ipcs/bs-playlist-ipcs.ts index b3284481..3d76da3d 100644 --- a/src/main/ipcs/bs-playlist-ipcs.ts +++ b/src/main/ipcs/bs-playlist-ipcs.ts @@ -40,7 +40,7 @@ ipc.on("is-playlists-deep-links-enabled", (_, reply) => { } }); -ipc.on("get-version-playlists", (req, reply) => { +ipc.on("get-version-playlists-details", (req, reply) => { const playlists = LocalPlaylistsManagerService.getInstance(); - reply(playlists.getVersionPlaylists(req.args)); + reply(playlists.getVersionPlaylistsDetails(req.args)); }); diff --git a/src/main/services/additional-content/local-playlists-manager.service.ts b/src/main/services/additional-content/local-playlists-manager.service.ts index c51c75f1..6cc43af0 100644 --- a/src/main/services/additional-content/local-playlists-manager.service.ts +++ b/src/main/services/additional-content/local-playlists-manager.service.ts @@ -15,7 +15,7 @@ import { Progression, pathExist } from "../../helpers/fs.helpers"; import { FileAssociationService } from "../file-association.service"; import { SongDetailsCacheService } from "./maps/song-details-cache.service"; import { sToMs } from "shared/helpers/time.helpers"; -import { LocalBPList, LocalBpListSong } from "shared/models/playlists/local-playlist.models"; +import { LocalBPList, LocalBPListsDetails } from "shared/models/playlists/local-playlist.models"; import { SongCacheService } from "./maps/song-cache.service"; import { pathToFileURL } from "url"; import { InstallationLocationService } from "../installation-location.service"; @@ -108,7 +108,7 @@ export class LocalPlaylistsManagerService { this.windows.openWindow(`oneclick-download-playlist.html?playlistUrl=${downloadUrl}`); } - private readLocalBPListsOfFolder(folerPath: string, version?: BSVersion): Observable> { + private getLocalBPListsOfFolder(folerPath: string): Observable> { return new Observable>(obs => { const progress: Progression = { current: 0, total: 0, data: [] }; @@ -116,13 +116,10 @@ export class LocalPlaylistsManagerService { (async () => { - await this.songDetails.waitLoaded(sToMs(15)); - if(!pathExistsSync(folerPath)) { throw new Error(`Playlists folder not found ${folerPath}`); } - const mapsFolder = version ? await this.maps.getMapsFolderPath(version) : null; const playlists = readdirSync(folerPath).filter(file => path.extname(file) === ".bplist"); progress.total = playlists.length; @@ -130,18 +127,8 @@ export class LocalPlaylistsManagerService { const playlistPath = path.join(folerPath, playlist); const bpList = await this.readPlaylistFile(playlistPath); - const localBpListSongs = bpList.songs.map(song => { - const localInfos = mapsFolder ? this.songCache.getMapInfoFromHash(song.hash) : null; - const coverPath = localInfos ? path.join(mapsFolder, localInfos.path, localInfos.info.rawInfo._coverImageFilename) : null; - const songFilePath = localInfos ? path.join(mapsFolder, localInfos.path, localInfos.info.rawInfo._songFilename) : null; - return ({ - song, - songDetails: this.songDetails.getSongDetails(song.hash), - coverUrl: (coverPath && pathExistsSync(coverPath)) ? pathToFileURL(coverPath).href : null, - songUrl: (songFilePath && pathExistsSync(songFilePath)) ? pathToFileURL(songFilePath).href : null, - } as LocalBpListSong) - }); - const localBpList: LocalBPList = { ...bpList, path: playlistPath, songs: localBpListSongs }; + + const localBpList: LocalBPList = { ...bpList, path: playlistPath }; bpLists.push(localBpList); progress.current += 1; obs.next(progress); @@ -153,18 +140,48 @@ export class LocalPlaylistsManagerService { }); } - public getVersionPlaylists(version: BSVersion): Observable> { + public getVersionPlaylistsDetails(version: BSVersion): Observable> { - console.log("getVersionPlaylists", version); + return new Observable>(obs => { + (async () => { - return new Observable>(obs => { - this.getPlaylistsFolder(version) - .then(folder => this.readLocalBPListsOfFolder(folder, version)) - .then(progress$ => lastValueFrom(progress$.pipe( - tap(progress => obs.next({...progress, data: []})), - ))) - .then(res => obs.next(res)) - .catch(err => obs.error(err)) + const folder = await this.getPlaylistsFolder(version); + const progress$ = this.getLocalBPListsOfFolder(folder); + + const localBPListsRes = (await lastValueFrom(progress$.pipe(tap({ next: progress => obs.next({...progress, data: []}) })))); + + await this.songDetails.waitLoaded(sToMs(15)); + + const tryExtractPlaylistId = (url: string) => { + const regex = /\/id\/(\d+)\/download/; + const match = url.match(regex); + return match ? Number(match[1]) : undefined; + } + + const bpListsDetails: LocalBPListsDetails[] = []; + for(const bpList of localBPListsRes.data){ + + const bpListDetails: LocalBPListsDetails = { + ...bpList, + nbMaps: bpList.songs?.length ?? 0, + id: bpList.customData?.syncURL ? tryExtractPlaylistId(bpList.customData.syncURL) : undefined + } + + const songsDetails = bpList.songs?.map(s => this.songDetails.getSongDetails(s.hash)); + + if(songsDetails){ + bpListDetails.duration = songsDetails.reduce((acc, song) => acc + song.duration, 0); + bpListDetails.nbMappers = new Set(songsDetails.map(s => s.uploader.id)).size; + bpListDetails.minNps = Math.min(...songsDetails.map(s => Math.min(...s.difficulties.map(d => d.nps || 0)))); + bpListDetails.maxNps = Math.max(...songsDetails.map(s => Math.max(...s.difficulties.map(d => d.nps || 0)))); + } + + bpListsDetails.push(bpListDetails); + } + + obs.next({...localBPListsRes, data: bpListsDetails}); + + })().catch(err => obs.error(err)) .finally(() => obs.complete()); }); } 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 4a5aaa15..8073f321 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 @@ -1,4 +1,4 @@ -import { useRef, useState } from "react"; +import { Dispatch, SetStateAction, createContext, useMemo, useRef, useState } from "react"; import { BSVersion } from "shared/bs-version.interface"; import { LocalMapsListPanel } from "./maps/local-maps-list-panel.component"; import { BsmDropdownButton, DropDownItem } from "../shared/bsm-dropdown-button.component"; @@ -14,15 +14,19 @@ import { BsmButton } from "../shared/bsm-button.component"; 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 { BehaviorSubject, Observable, of } from "rxjs"; import { LocalPlaylistsListPanel } from "./playlists/local-playlists-list-panel.component"; import { PlaylistsManagerService } from "renderer/services/playlists-manager.service"; +import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"; +import { useConstant } from "renderer/hooks/use-constant.hook"; type Props = { version?: BSVersion; isActive?: boolean; }; +export const InstalledMapsContext = createContext<{ maps$?: BehaviorSubject; setMaps: (maps: BsmLocalMap[]) => void }>(null); + export function MapsPlaylistsPanel({ version, isActive }: Props) { const mapsService = useService(MapsManagerService); @@ -32,6 +36,9 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) { const t = useTranslation(); const [tabIndex, setTabIndex] = useState(0); + const maps$ = useConstant(() => new BehaviorSubject(undefined)); + const mapsContextValue = useConstant(() => ({ maps$: maps$, setMaps: maps$.next.bind(maps$) })); + const mapsRef = useRef(); const [mapFilter, setMapFilter] = useState({}); const [mapSearch, setMapSearch] = useState(""); @@ -55,7 +62,7 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) { }; const handleMapsAddClick = () => { - mapsDownloader.openDownloadMapModal(version, mapsRef.current.getMaps?.()); + mapsDownloader.openDownloadMapModal(version, maps$.value); }; const handleMapsLinkClick = () => { @@ -132,10 +139,10 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) { }, ]} > - <> + - + ); diff --git a/src/renderer/components/maps-playlists-panel/maps/local-maps-list-panel.component.tsx b/src/renderer/components/maps-playlists-panel/maps/local-maps-list-panel.component.tsx index 981b169a..567401d9 100644 --- a/src/renderer/components/maps-playlists-panel/maps/local-maps-list-panel.component.tsx +++ b/src/renderer/components/maps-playlists-panel/maps/local-maps-list-panel.component.tsx @@ -1,6 +1,6 @@ import { MapsManagerService } from "renderer/services/maps-manager.service"; import { BSVersion } from "shared/bs-version.interface"; -import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react"; +import { forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useRef, useState } from "react"; import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"; import { Subscription, BehaviorSubject } from "rxjs"; import { MapFilter, MapTag } from "shared/models/maps/beat-saver.model"; @@ -18,6 +18,8 @@ import { FolderLinkState } from "renderer/services/version-folder-linker.service import { useOnUpdate } from "renderer/hooks/use-on-update.hook"; import { useConstant } from "renderer/hooks/use-constant.hook"; import { BsContentLoader } from "renderer/components/shared/bs-content-loader.component"; +import { InstalledMapsContext } from "../maps-playlists-panel.component"; +import { useObservable } from "renderer/hooks/use-observable.hook"; type Props = { version: BSVersion; @@ -33,8 +35,11 @@ export const LocalMapsListPanel = forwardRef(({ version, classNa const mapsDownloader = useService(MapsDownloaderService); const t = useTranslation(); + const ref = useRef(null); - const [maps, setMaps] = useState(null); + + const {maps$, setMaps} = useContext(InstalledMapsContext); + const maps = useObservable(() => maps$, undefined); const [subs] = useState([]); const [selectedMaps$] = useState(new BehaviorSubject([])); const [itemPerRow, setItemPerRow] = useState(2); @@ -59,10 +64,7 @@ export const LocalMapsListPanel = forwardRef(({ version, classNa }, exportMaps() { mapsManager.exportMaps(version, selectedMaps$.value); - }, - getMaps() { - return maps; - }, + } }), [selectedMaps$.value, maps, version] ); @@ -79,12 +81,12 @@ export const LocalMapsListPanel = forwardRef(({ version, classNa if (targerVersion !== version) { return; } - setMaps(maps => (maps ? [map, ...maps] : [map])); + setMaps((maps ? [map, ...maps] : [map])); }); } return () => { - setMaps(() => null); + setMaps(null); loadPercent$.next(0); subs.forEach(s => s.unsubscribe()); mapsDownloader.removeOnMapDownloadedListener(loadMaps); @@ -122,7 +124,7 @@ export const LocalMapsListPanel = forwardRef(({ version, classNa }, [isActiveOnce, itemPerRow]); const loadMaps = () => { - setMaps(() => null); + setMaps(null); loadPercent$.next(0); const loadMapsObs$ = mapsManager.getMaps(version); @@ -134,7 +136,7 @@ export const LocalMapsListPanel = forwardRef(({ version, classNa last() ) .subscribe({ - next: progress => setMaps(() => progress.maps), + next: progress => setMaps(progress.maps), complete: () => loadPercent$.next(0), }) ); @@ -142,7 +144,7 @@ export const LocalMapsListPanel = forwardRef(({ version, classNa const removeMapsFromList = (mapsToRemove: BsmLocalMap[]) => { const filtredMaps = maps.filter(map => !mapsToRemove.some(toDeleteMaps => map.hash === toDeleteMaps.hash)); - setMaps(() => filtredMaps); + setMaps(filtredMaps); const filtredSelectedMaps = selectedMaps$.value.filter(map => !mapsToRemove.some(toDeleteMaps => map.hash === toDeleteMaps.hash)); selectedMaps$.next(filtredSelectedMaps); 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 331d1db5..48ba662b 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 @@ -155,7 +155,7 @@ export const MapItem = memo(({ hash, title, autor, songAutor, coverUrl, songUrl, }; return ( - setHovered(true)} onHoverEnd={() => setHovered(false)} style={{ zIndex: hovered && 5, transform: "translateZ(0) scale(1.0, 1.0)", backfaceVisibility: "hidden" }}> + setHovered(true)} onHoverEnd={() => setHovered(false)} style={{ zIndex: hovered && 5, transform: "translateZ(0) scale(1.0, 1.0)", backfaceVisibility: "hidden" }}> {(diffsPanelHovered || bottomBarHovered) && ( diff --git a/src/renderer/components/maps-playlists-panel/maps/maps-row.component.tsx b/src/renderer/components/maps-playlists-panel/maps/maps-row.component.tsx index 580ae2a3..555b6452 100644 --- a/src/renderer/components/maps-playlists-panel/maps/maps-row.component.tsx +++ b/src/renderer/components/maps-playlists-panel/maps/maps-row.component.tsx @@ -5,7 +5,8 @@ import { BehaviorSubject } from "rxjs"; import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"; import { ParsedMapDiff, MapItem } from "./map-item.component"; import equal from "fast-deep-equal/es6"; -import { SongDetailDiffCharactertistic } from "shared/models/maps/song-details-cache.model"; +import { SongDetailDiffCharactertistic, SongDetails } from "shared/models/maps/song-details-cache.model"; +import { BsvMapDetail, RawMapInfoData } from "shared/models/maps"; type Props = { maps: BsmLocalMap[]; @@ -21,29 +22,6 @@ export const MapsRow = memo(({ maps, style, selectedMaps$, onMapSelect, onMapDel distinctUntilChanged(equal), ), []); - const extractMapDiffs = (map: BsmLocalMap): Map => { - const res = new Map(); - if (map.songDetails?.difficulties) { - map.songDetails?.difficulties.forEach(diff => { - const arr = res.get(diff.characteristic) || []; - const diffName = map.rawInfo._difficultyBeatmapSets.find(set => set._beatmapCharacteristicName === diff.characteristic)._difficultyBeatmaps.find(rawDiff => rawDiff._difficulty === diff.difficulty)?._customData?._difficultyLabel || diff.difficulty; - arr.push({ name: diffName, type: diff.difficulty, stars: diff.stars }); - res.set(diff.characteristic, arr); - }); - return res; - } - - map.rawInfo._difficultyBeatmapSets.forEach(set => { - set._difficultyBeatmaps.forEach(diff => { - const arr = res.get(set._beatmapCharacteristicName) || []; - arr.push({ name: diff._customData?._difficultyLabel || diff._difficulty, type: diff._difficulty, stars: null }); - res.set(set._beatmapCharacteristicName, arr); - }); - }); - - return res; - }; - const renderMapItem = (map: BsmLocalMap) => { return selected.hash === map.hash)} - diffs={extractMapDiffs(map)} + diffs={extractMapDiffs({ rawMapInfo: map.rawInfo, songDetails: map.songDetails })} mapId={map.songDetails?.id} ranked={map.songDetails?.ranked} autorId={map.songDetails?.uploader.id} @@ -75,3 +53,35 @@ export const MapsRow = memo(({ maps, style, selectedMaps$, onMapSelect, onMapDel ); }, equal); + +export function extractMapDiffs({rawMapInfo, songDetails, bsvMap}: {rawMapInfo?: RawMapInfoData, songDetails?: SongDetails, bsvMap?: BsvMapDetail}): Map { + const res = new Map(); + if (bsvMap && bsvMap.versions?.at(0)?.diffs) { + bsvMap.versions.at(0).diffs.forEach(diff => { + const arr = res.get(diff.characteristic) || []; + arr.push({ name: diff.difficulty, type: diff.difficulty, stars: diff.stars }); + res.set(diff.characteristic, arr); + }); + return res; + } + + if (songDetails?.difficulties) { + songDetails?.difficulties.forEach(diff => { + const arr = res.get(diff.characteristic) || []; + const diffName = rawMapInfo._difficultyBeatmapSets.find(set => set._beatmapCharacteristicName === diff.characteristic)._difficultyBeatmaps.find(rawDiff => rawDiff._difficulty === diff.difficulty)?._customData?._difficultyLabel || diff.difficulty; + arr.push({ name: diffName, type: diff.difficulty, stars: diff.stars }); + res.set(diff.characteristic, arr); + }); + return res; + } + + rawMapInfo._difficultyBeatmapSets.forEach(set => { + set._difficultyBeatmaps.forEach(diff => { + const arr = res.get(set._beatmapCharacteristicName) || []; + arr.push({ name: diff._customData?._difficultyLabel || diff._difficulty, type: diff._difficulty, stars: null }); + res.set(set._beatmapCharacteristicName, arr); + }); + }); + + return res; +} 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 efd76a7d..74c7d1c3 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 @@ -1,4 +1,4 @@ -import { forwardRef, useState } from "react"; +import { forwardRef, useContext, useState } from "react"; import { BsContentLoader } from "renderer/components/shared/bs-content-loader.component"; import { useChangeUntilEqual } from "renderer/hooks/use-change-until-equal.hook"; import { useConstant } from "renderer/hooks/use-constant.hook"; @@ -6,12 +6,15 @@ import { useOnUpdate } from "renderer/hooks/use-on-update.hook"; import { useService } from "renderer/hooks/use-service.hook"; import { PlaylistsManagerService } from "renderer/services/playlists-manager.service"; import { FolderLinkState } from "renderer/services/version-folder-linker.service"; -import { BehaviorSubject, finalize, lastValueFrom, map, tap } from "rxjs"; +import { BehaviorSubject, finalize, lastValueFrom, map, of, tap } from "rxjs"; import { BSVersion } from "shared/bs-version.interface"; import { noop } from "shared/helpers/function.helpers"; -import { LocalBPList } from "shared/models/playlists/local-playlist.models"; +import { LocalBPListsDetails } from "shared/models/playlists/local-playlist.models"; import { PlaylistItem } from "./playlist-item.component"; import { useStateMap } from "renderer/hooks/use-state-map.hook"; +import { ModalService } from "renderer/services/modale.service"; +import { LocalPlaylistDetailsModal } from "renderer/components/modal/modal-types/playlist/local-playlist-details-modal.component"; +import { InstalledMapsContext } from "../maps-playlists-panel.component"; type Props = { version: BSVersion; @@ -23,17 +26,19 @@ type Props = { export const LocalPlaylistsListPanel = forwardRef(({ version, className, isActive, linkedState }, forwardedRef) => { const playlistService = useService(PlaylistsManagerService); + const modals = useService(ModalService); const isActiveOnce = useChangeUntilEqual(isActive, { untilEqual: true }); + const { maps$ } = useContext(InstalledMapsContext); const [playlistsLoading, setPlaylistsLoading] = useState(false); - const [playlists, setPlaylists] = useState([]); + 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 => { + const loadLocalPlaylistsDetails = (): Promise => { setPlaylistsLoading(true); - const obs = playlistService.getVersionPlaylists(version).pipe( + const obs = playlistService.getVersionPlaylistsDetails(version).pipe( tap({ next: load => loadPercent$.next((load.current / load.total) * 100)}), map(load => load.data), finalize(() => setPlaylistsLoading(false)) @@ -42,52 +47,45 @@ export const LocalPlaylistsListPanel = forwardRef(({ version, cl return lastValueFrom(obs); } - const getNbMappersOfPlaylist = (playlist: LocalBPList) => { - return new Set(playlist.songs.map(s => s.songDetails?.uploader?.id ?? s.song.hash)).size; - }; - - const getDurationOfPlaylist = (playlist: LocalBPList) => { - return playlist.songs.reduce((acc, s) => acc + (s.songDetails?.duration ?? 0), 0); - } - - const getMinNpsOfPlaylist = (playlist: LocalBPList) => { - let min = Infinity; - playlist.songs.forEach(s => { - s.songDetails?.difficulties.forEach(d => { - if(d.nps < min){ - min = d.nps; - } - }) - }); - return min === Infinity ? null : min; - } - - const getMaxNpsOfPlaylist = (playlist: LocalBPList) => { - let max = -Infinity; - playlist.songs.forEach(s => { - s.songDetails?.difficulties.forEach(d => { - if(d.nps > max){ - max = d.nps; - } - }) - }); - return max === -Infinity ? null : max; - } - useOnUpdate(() => { if(!isActiveOnce){ return noop(); } - loadPlaylists().then(loadedPlaylists => { + loadLocalPlaylistsDetails().then(loadedPlaylists => { setPlaylists(() => loadedPlaylists); }).catch(() => { - setPlaylists([]) + setPlaylists([]); }).finally(() => { loadPercent$.next(0); }); }, [isActiveOnce, version, linked]); + console.log(maps$.value); + + const openPlaylistDetails = (playlistKey: string) => { + const playlist = playlists.find(p => p.path === playlistKey); + + console.log(playlist.songs); + + modals.openModal(LocalPlaylistDetailsModal, { + data: { + version, + title: playlist.playlistTitle, + image: playlist.image, + author: playlist.playlistAuthor, + description: playlist.playlistDescription, + nbMaps: playlist.nbMaps, + duration: playlist.duration, + maxNps: playlist.maxNps, + minNps: playlist.minNps, + nbMappers: playlist.nbMappers, + installedMaps$: maps$.pipe(map(maps => maps.filter(m => playlist.songs.some(song => song.hash.toLocaleLowerCase() === m.hash.toLocaleLowerCase())))), + }, + noStyle: true, + }) + }; + const render = () => { if(playlistsLoading){ return ( @@ -97,25 +95,22 @@ export const LocalPlaylistsListPanel = forwardRef(({ version, cl if (playlists.length){ return ( - <> -
    - {playlists.map(p => - - )} -
-
- +
    + {playlists.map(p => + openPlaylistDetails(p.path)} + /> + )} +
) } 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 d549f544..3d04855a 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 @@ -8,11 +8,9 @@ import dateFormat from 'dateformat'; import { NpsIcon } from 'renderer/components/svgs/icons/nps-icon.component'; import { GlowEffect } from 'renderer/components/shared/glow-effect.component'; import { useState } from 'react'; -import { FolderOpenIcon } from 'renderer/components/svgs/icons/folder-open-icon.component'; +import { SearchIcon } from 'renderer/components/svgs/icons/search-icon.component'; type Props = { - id: string; - className?: string; title?: string; author?: string; coverBase64?: string; @@ -23,9 +21,10 @@ type Props = { minNps?: number; maxNps?: number; selected?: boolean; + onClickOpen?: () => void; } -export function PlaylistItem({ id, className, title, author, coverUrl, coverBase64, duration, nbMaps, nbMappers, minNps, maxNps, selected }: Props) { +export function PlaylistItem({ title, author, coverUrl, coverBase64, duration, nbMaps, nbMappers, minNps, maxNps, selected, onClickOpen }: Props) { const color = useThemeColor("first-color"); @@ -33,8 +32,9 @@ export function PlaylistItem({ id, className, title, author, coverUrl, coverBase const nbMapsText = nbMaps ? Intl.NumberFormat(undefined, { notation: "compact" }).format(nbMaps).trim() : null; const nbMappersText = nbMappers ? Intl.NumberFormat(undefined, { notation: "compact" }).format(nbMappers).trim() : null; - const minNpsText = minNps ? Math.round(minNps * 10) / 10 : null; - const maxNpsText = maxNps ? Math.round(maxNps * 10) / 10 : null; + const minNpsText = minNps ? Math.round(minNps * 10) / 10 : 0; + const maxNpsText = maxNps ? Math.round(maxNps * 10) / 10 : 0; + const showNps = minNps !== undefined && maxNps !== undefined; const durationText = (() => { if (!duration) { @@ -45,27 +45,33 @@ export function PlaylistItem({ id, className, title, author, coverUrl, coverBase return duration > 3600 ? dateFormat(date, "h:MM:ss") : dateFormat(date, "MM:ss"); })(); + // TODO : Translate + return ( - setHovered(() => true)} onHoverEnd={() => setHovered(() => false)} > + setHovered(() => true)} onHoverEnd={() => setHovered(() => false)} > -
+
- +
- +
-

{title}

+

{title}

Créé par {author}

{ nbMapsText &&
{nbMapsText}
} { nbMappersText &&
{nbMappersText}
} { durationText &&
{durationText}
} - { (minNps && maxNps) &&
{`${minNpsText} - ${maxNpsText}`}
} + { showNps &&
{`${minNpsText} - ${maxNpsText}`}
}
diff --git a/src/renderer/components/modal/modal-types/bs-downgrade/login-to-steam-modal.component.tsx b/src/renderer/components/modal/modal-types/bs-downgrade/login-to-steam-modal.component.tsx index 5d4169d2..4fbef88f 100644 --- a/src/renderer/components/modal/modal-types/bs-downgrade/login-to-steam-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/bs-downgrade/login-to-steam-modal.component.tsx @@ -13,8 +13,8 @@ import { BsmBasicSpinner } from "renderer/components/shared/bsm-basic-spinner/bs export const LoginToSteamModal: ModalComponent< { username: string; password: string; stay: boolean, method: "form"|"qr" }, { qrCode$: Observable, logged$: Observable } -> = ({ resolver, data }) => { - +> = ({ resolver, options: {data} }) => { + const modal = useService(ModalService); const [username, setUsername] = useState(""); @@ -82,10 +82,10 @@ export const LoginToSteamModal: ModalComponent<
- {t("modals.steam-login.inputs.qr.label")} + {t("modals.steam-login.inputs.qr.label")}
- {(qrCodeUrl ? - : + {(qrCodeUrl ? + : )}
diff --git a/src/renderer/components/modal/modal-types/bs-downgrade/steam-mobile-approve-modal.component.tsx b/src/renderer/components/modal/modal-types/bs-downgrade/steam-mobile-approve-modal.component.tsx index 8cbaf967..0c60a583 100644 --- a/src/renderer/components/modal/modal-types/bs-downgrade/steam-mobile-approve-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/bs-downgrade/steam-mobile-approve-modal.component.tsx @@ -6,8 +6,8 @@ import { useTranslation } from "renderer/hooks/use-translation.hook"; import { Observable } from "rxjs"; import { useOnUpdate } from "renderer/hooks/use-on-update.hook"; -export const SteamMobileApproveModal: ModalComponent }> = ({ resolver, data }) => { - +export const SteamMobileApproveModal: ModalComponent }> = ({ resolver, options: {data} }) => { + const t = useTranslation(); useOnUpdate(() => { @@ -36,4 +36,4 @@ export const SteamMobileApproveModal: ModalComponent{t("modals.steam-auth-approve.not-access-to-steam-app")} ); -}; \ No newline at end of file +}; diff --git a/src/renderer/components/modal/modal-types/chabgelog-modal/changelog-modal.component.tsx b/src/renderer/components/modal/modal-types/chabgelog-modal/changelog-modal.component.tsx index 4c461e22..2e5a54cd 100644 --- a/src/renderer/components/modal/modal-types/chabgelog-modal/changelog-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/chabgelog-modal/changelog-modal.component.tsx @@ -7,7 +7,7 @@ import DOMPurify from 'dompurify'; import './changelog-modal.component.css'; import Tippy from "@tippyjs/react"; -export const ChangelogModal: ModalComponent = ({ resolver, data: changelog }) => { +export const ChangelogModal: ModalComponent = ({ resolver, options: {data: changelog} }) => { const linkOpener = useService(LinkOpenerService); const openGithub = () => linkOpener.open("https://github.com/Zagrios/bs-manager"); diff --git a/src/renderer/components/modal/modal-types/create-launch-shortcut-modal.component.tsx b/src/renderer/components/modal/modal-types/create-launch-shortcut-modal.component.tsx index 18f5fc4c..e38b35cf 100644 --- a/src/renderer/components/modal/modal-types/create-launch-shortcut-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/create-launch-shortcut-modal.component.tsx @@ -12,7 +12,7 @@ import { useThemeColor } from "renderer/hooks/use-theme-color.hook"; import { ChevronTopIcon } from "renderer/components/svgs/icons/chevron-top-icon.component"; import Tippy from "@tippyjs/react"; -export const CreateLaunchShortcutModal: ModalComponent = ({resolver, data}) => { +export const CreateLaunchShortcutModal: ModalComponent = ({resolver, options: {data}}) => { const bsLauncher = useService(BSLauncherService); @@ -76,10 +76,10 @@ export const CreateLaunchShortcutModal: ModalComponent
- setAdditionalArgsString(e.target.value)} /> diff --git a/src/renderer/components/modal/modal-types/delete-maps-modal.component.tsx b/src/renderer/components/modal/modal-types/delete-maps-modal.component.tsx index 819d1915..a31dbeef 100644 --- a/src/renderer/components/modal/modal-types/delete-maps-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/delete-maps-modal.component.tsx @@ -10,8 +10,8 @@ import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"; import BeatConflict from "../../../../../assets/images/apngs/beat-conflict.png"; import { useService } from "renderer/hooks/use-service.hook"; -export const DeleteMapsModal: ModalComponent = ({ resolver, data: { linked, maps } }) => { - +export const DeleteMapsModal: ModalComponent = ({ resolver, options: {data : { linked, maps }}}) => { + const config = useService(ConfigurationService); const t = useTranslation(); diff --git a/src/renderer/components/modal/modal-types/download-maps-modal.component.tsx b/src/renderer/components/modal/modal-types/download-maps-modal.component.tsx index 389a889e..fce1d1eb 100644 --- a/src/renderer/components/modal/modal-types/download-maps-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/download-maps-modal.component.tsx @@ -20,8 +20,9 @@ import { useTranslation } from "renderer/hooks/use-translation.hook"; import { OsDiagnosticService } from "renderer/services/os-diagnostic.service"; import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"; import { useService } from "renderer/hooks/use-service.hook"; +import { extractMapDiffs } from "renderer/components/maps-playlists-panel/maps/maps-row.component"; -export const DownloadMapsModal: ModalComponent = ({ data: { ownedMaps, version } }) => { +export const DownloadMapsModal: ModalComponent = ({ options: {data : { ownedMaps, version }} }) => { const beatSaver = useService(BeatSaverService); const mapsDownloader = useService(MapsDownloaderService); const progressBar = useService(ProgressBarService); @@ -84,24 +85,32 @@ export const DownloadMapsModal: ModalComponent setLoading(() => false)); }; - const extractMapDiffs = (map: BsvMapDetail): Map => { - const res = new Map(); - if (map.versions.at(0).diffs) { - map.versions.at(0).diffs.forEach(diff => { - const arr = res.get(diff.characteristic) || []; - arr.push({ name: diff.difficulty, type: diff.difficulty, stars: diff.stars }); - res.set(diff.characteristic, arr); - }); - } - return res; - }; - const renderMap = (map: BsvMapDetail) => { const isMapOwned = map.versions.some(version => ownedMapHashs.includes(version.hash)); const isDownloading = map.id === currentDownload?.map?.id; const inQueue = mapsInQueue.some(toDownload => equal(toDownload.version, version) && toDownload.map.id === map.id); - return ; + return ; }; const handleDownloadMap = useCallback((map: BsvMapDetail) => { diff --git a/src/renderer/components/modal/modal-types/edit-version-modal.component.tsx b/src/renderer/components/modal/modal-types/edit-version-modal.component.tsx index fc1e549b..c770efa9 100644 --- a/src/renderer/components/modal/modal-types/edit-version-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/edit-version-modal.component.tsx @@ -9,7 +9,7 @@ import { ConfigurationService } from "renderer/services/configuration.service"; import { ModalComponent, ModalExitCode } from "renderer/services/modale.service"; import { BSVersion } from "shared/bs-version.interface"; -export const EditVersionModal: ModalComponent<{ name: string; color: string }, { version: BSVersion; clone?: boolean }> = ({ resolver, data }) => { +export const EditVersionModal: ModalComponent<{ name: string; color: string }, { version: BSVersion; clone?: boolean }> = ({ resolver, options: {data} }) => { const { version, clone } = data; const configService = useService(ConfigurationService); diff --git a/src/renderer/components/modal/modal-types/models/delete-models-modal.component.tsx b/src/renderer/components/modal/modal-types/models/delete-models-modal.component.tsx index e219c93d..7adf355c 100644 --- a/src/renderer/components/modal/modal-types/models/delete-models-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/models/delete-models-modal.component.tsx @@ -12,7 +12,7 @@ import { ConfigurationService } from "renderer/services/configuration.service"; import { ModelsManagerService } from "renderer/services/models-management/models-manager.service"; import { useService } from "renderer/hooks/use-service.hook"; -export const DeleteModelsModal: ModalComponent = ({ resolver, data }) => { +export const DeleteModelsModal: ModalComponent = ({ resolver, options: {data} }) => { const config = useService(ConfigurationService); const t = useTranslation(); const [remember, setRemember] = useState(config.get(ModelsManagerService.REMEMBER_CHOICE_DELETE_MODEL_KEY) || false); diff --git a/src/renderer/components/modal/modal-types/models/download-models-modal.component.tsx b/src/renderer/components/modal/modal-types/models/download-models-modal.component.tsx index d6982bd3..41a0033b 100644 --- a/src/renderer/components/modal/modal-types/models/download-models-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/models/download-models-modal.component.tsx @@ -23,7 +23,7 @@ import { catchError, of } from "rxjs"; import Tippy from "@tippyjs/react"; import { BsmLocalModel } from "shared/models/models/bsm-local-model.interface"; -export const DownloadModelsModal: ModalComponent = ({ data: { version, type, owned } }) => { +export const DownloadModelsModal: ModalComponent = ({ options: {data: { version, type, owned }} }) => { const modelsDownloader = useService(ModelsDownloaderService); const modelSaber = useService(ModelSaberService); const os = useService(OsDiagnosticService); 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 ecaafbc7..3350200b 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 @@ -7,7 +7,7 @@ import { ModalComponent, ModalExitCode } from "renderer/services/modale.service" import { MSModelType } from "shared/models/models/model-saber.model"; import BeatRunning from "../../../../../../assets/images/apngs/beat-running.png"; -export const LinkModelsModal: ModalComponent = ({ resolver, data }) => { +export const LinkModelsModal: ModalComponent = ({ resolver, options: {data} }) => { const t = useTranslation(); const [keepMaps, setKeepMaps] = useState(true); diff --git a/src/renderer/components/modal/modal-types/models/unlink-models-modal.component.tsx b/src/renderer/components/modal/modal-types/models/unlink-models-modal.component.tsx index bd801989..5af7a65b 100644 --- a/src/renderer/components/modal/modal-types/models/unlink-models-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/models/unlink-models-modal.component.tsx @@ -7,7 +7,7 @@ import { ModalComponent, ModalExitCode } from "renderer/services/modale.service" import BeatConflict from "../../../../../../assets/images/apngs/beat-conflict.png"; import { MSModelType } from "shared/models/models/model-saber.model"; -export const UnlinkModelsModal: ModalComponent = ({ resolver, data }) => { +export const UnlinkModelsModal: ModalComponent = ({ resolver, options: {data} }) => { const t = useTranslation(); const [keepMaps, setKeepMaps] = useState(true); diff --git a/src/renderer/components/modal/modal-types/playlist/local-playlist-details-modal.component.tsx b/src/renderer/components/modal/modal-types/playlist/local-playlist-details-modal.component.tsx new file mode 100644 index 00000000..39c9a2e5 --- /dev/null +++ b/src/renderer/components/modal/modal-types/playlist/local-playlist-details-modal.component.tsx @@ -0,0 +1,57 @@ +import { ModalComponent } from "renderer/services/modale.service" +import { PlaylistDetailsTemplate, PlaylistDetailsTemplateProps } from "./playlist-details-template.component" +import { Observable } from "rxjs" +import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface" +import { BSVersion } from "shared/bs-version.interface"; +import { useObservable } from "renderer/hooks/use-observable.hook"; +import { MapItem } from "renderer/components/maps-playlists-panel/maps/map-item.component"; +import { extractMapDiffs } from "renderer/components/maps-playlists-panel/maps/maps-row.component"; + +interface Props extends Omit { + version: BSVersion + installedMaps$: Observable; +} + +export const LocalPlaylistDetailsModal: ModalComponent = ({resolver, options}) => { + + const installedMaps = useObservable(() => options.data.installedMaps$, undefined); + + console.log(installedMaps); + + const renderMaps = () => { + if (!installedMaps) { + return null; + } + + return ( +
    + {installedMaps.map(map => ( + + ))} +
+ ) + } + + return ( + + {renderMaps()} + + ) +} diff --git a/src/renderer/components/modal/modal-types/playlist/playlist-details-template.component.tsx b/src/renderer/components/modal/modal-types/playlist/playlist-details-template.component.tsx new file mode 100644 index 00000000..7046abe5 --- /dev/null +++ b/src/renderer/components/modal/modal-types/playlist/playlist-details-template.component.tsx @@ -0,0 +1,66 @@ +import { ReactNode } from "react" +import { BsmImage } from "renderer/components/shared/bsm-image.component"; +import dateFormat from "dateformat"; +import { MapIcon } from "renderer/components/svgs/icons/map-icon.component"; +import { PersonIcon } from "renderer/components/svgs/icons/person-icon.component"; +import { ClockIcon } from "renderer/components/svgs/icons/clock-icon.component"; +import { NpsIcon } from "renderer/components/svgs/icons/nps-icon.component"; +import { useThemeColor } from "renderer/hooks/use-theme-color.hook"; +import { ThemeColorGradientSpliter } from "renderer/components/shared/theme-color-gradient-spliter.component"; + +export type PlaylistDetailsTemplateProps = { + title: string; + image: string; + author: string; + description: string; + nbMaps: number; + duration: number; + nbMappers: number; + minNps: number; + maxNps: number; + children?: ReactNode; +} + +export function PlaylistDetailsTemplate({title, image, author, description, nbMaps, duration, nbMappers, minNps, maxNps, children}: PlaylistDetailsTemplateProps) { + + const color = useThemeColor("first-color"); + + const nbMapsText = nbMaps ? Intl.NumberFormat(undefined, { notation: "compact" }).format(nbMaps).trim() : null; + const nbMappersText = nbMappers ? Intl.NumberFormat(undefined, { notation: "compact" }).format(nbMappers).trim() : null; + const minNpsText = minNps ? Math.round(minNps * 10) / 10 : 0; + const maxNpsText = maxNps ? Math.round(maxNps * 10) / 10 : 0; + const showNps = minNps !== undefined && maxNps !== undefined; + + const durationText = (() => { + if (!duration) { + return null; + } + const date = new Date(0); + date.setSeconds(duration); + return duration > 3600 ? dateFormat(date, "h:MM:ss") : dateFormat(date, "MM:ss"); + })(); + + return ( +
+
+ + +
+

{title}

+

{description}

+

Créé par {author}

+ +
+ { nbMapsText &&
{nbMapsText}
} + { nbMappersText &&
{nbMappersText}
} + { durationText &&
{durationText}
} + { showNps &&
{`${minNpsText} - ${maxNpsText}`}
} +
+ +
+
+ + {children} +
+ ) +} diff --git a/src/renderer/components/modal/modal-types/share-folders-modal.component.tsx b/src/renderer/components/modal/modal-types/share-folders-modal.component.tsx index 09e319a5..7372bb8d 100644 --- a/src/renderer/components/modal/modal-types/share-folders-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/share-folders-modal.component.tsx @@ -14,7 +14,7 @@ import { ModalComponent } from "renderer/services/modale.service"; import { FolderLinkState, VersionFolderLinkerService, VersionLinkerActionType } from "renderer/services/version-folder-linker.service"; import { BSVersion } from "shared/bs-version.interface"; -export const ShareFoldersModal: ModalComponent = ({ data }) => { +export const ShareFoldersModal: ModalComponent = ({ options: {data} }) => { const SHARED_FOLDERS_KEY = "default-shared-folders"; const config = useService(ConfigurationService); diff --git a/src/renderer/components/modal/modal-types/uninstall-all-mods-modal.component.tsx b/src/renderer/components/modal/modal-types/uninstall-all-mods-modal.component.tsx index d236e449..3f0a9ced 100644 --- a/src/renderer/components/modal/modal-types/uninstall-all-mods-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/uninstall-all-mods-modal.component.tsx @@ -5,7 +5,7 @@ import { BsmImage } from "renderer/components/shared/bsm-image.component"; import { useTranslation } from "renderer/hooks/use-translation.hook"; import { BSVersion } from "shared/bs-version.interface"; -export const UninstallAllModsModal: ModalComponent = ({ resolver, data }) => { +export const UninstallAllModsModal: ModalComponent = ({ resolver, options: {data} }) => { const version = data; const t = useTranslation(); diff --git a/src/renderer/components/modal/modal-types/uninstall-mod-modal.component.tsx b/src/renderer/components/modal/modal-types/uninstall-mod-modal.component.tsx index 32e19b4f..4108c0e4 100644 --- a/src/renderer/components/modal/modal-types/uninstall-mod-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/uninstall-mod-modal.component.tsx @@ -5,7 +5,7 @@ import { BsmImage } from "renderer/components/shared/bsm-image.component"; import { useTranslation } from "renderer/hooks/use-translation.hook"; import { Mod } from "shared/models/mods/mod.interface"; -export const UninstallModModal: ModalComponent = ({ resolver, data }) => { +export const UninstallModModal: ModalComponent = ({ resolver, options: {data} }) => { const mod = data; const t = useTranslation(); diff --git a/src/renderer/components/modal/modal-types/uninstall-modal.component.tsx b/src/renderer/components/modal/modal-types/uninstall-modal.component.tsx index c8e84cbd..bc0d543b 100644 --- a/src/renderer/components/modal/modal-types/uninstall-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/uninstall-modal.component.tsx @@ -5,7 +5,7 @@ import { BsmButton } from "renderer/components/shared/bsm-button.component"; import { BsmImage } from "renderer/components/shared/bsm-image.component"; import { useTranslation } from "renderer/hooks/use-translation.hook"; -export const UninstallModal: ModalComponent = ({ resolver, data }) => { +export const UninstallModal: ModalComponent = ({ resolver, options: {data} }) => { const version = data; const t = useTranslation(); diff --git a/src/renderer/components/modal/modal.component.tsx b/src/renderer/components/modal/modal.component.tsx index 53bde46c..8c232824 100644 --- a/src/renderer/components/modal/modal.component.tsx +++ b/src/renderer/components/modal/modal.component.tsx @@ -1,17 +1,16 @@ -import { ModalExitCode, ModalService } from "renderer/services/modale.service"; +import { ModalExitCode, ModalObject, ModalService } from "renderer/services/modale.service"; import { AnimatePresence, motion } from "framer-motion"; import { useObservable } from "renderer/hooks/use-observable.hook"; import { useThemeColor } from "renderer/hooks/use-theme-color.hook"; import { useEffect } from "react"; import { BsmIcon } from "../svgs/bsm-icon.component"; +import { ThemeColorGradientSpliter } from "../shared/theme-color-gradient-spliter.component"; export function Modal() { const modalSevice = ModalService.getInstance(); - const ModalComponent = useObservable(() => modalSevice.getModalToShow()); - - const modalData = modalSevice.getModalData(); - const resolver = modalSevice.getResolver(); + const modals = useObservable(() => modalSevice.getModalToShow()); + const currentModal = modals?.at(-1); const { firstColor, secondColor } = useThemeColor(); @@ -20,10 +19,10 @@ export function Modal() { if (e.key !== "Escape") { return; } - resolver?.(ModalExitCode.NO_CHOICE); + currentModal.resolver({ exitCode: ModalExitCode.CLOSED }); }; - if (ModalComponent) { + if (currentModal) { window.addEventListener("keyup", onEscape); } else { window.removeEventListener("keyup", onEscape); @@ -32,32 +31,40 @@ export function Modal() { return () => { window.removeEventListener("keyup", onEscape); }; - }, [ModalComponent]); + }, [currentModal]); + + const renderModal = (modal: ModalObject) => { + if (!modal?.modal) { return null; } + + if(modal.options?.noStyle){ + return + } + + return ( +
+ +
{ + e.stopPropagation(); + modal.resolver({ exitCode: ModalExitCode.CLOSED }); + }} + > + +
+ +
+ ) + } return ( - - {ModalComponent && resolver && ( -
- modalSevice.resolve({ exitCode: ModalExitCode.NO_CHOICE })} className="absolute top-0 bottom-0 right-0 left-0 bg-black" initial={{ opacity: 0 }} animate={{ opacity: ModalComponent && 0.6 }} exit={{ opacity: 0 }} transition={{ duration: 0.2 }} /> - -
-
- -
-
{ - e.stopPropagation(); - resolver(ModalExitCode.CLOSED); - }} - > - -
- -
+ + {currentModal && currentModal.resolver({ exitCode: ModalExitCode.NO_CHOICE })} className="fixed size-full bg-black z-[90]" initial={{ opacity: 0 }} animate={{ opacity: currentModal && 0.6 }} exit={{ opacity: 0 }} transition={{ duration: 0.2 }} />} + {modals?.map((modal, index) => ( + + {renderModal(modal)} -
- )} -
+ ))} + ); } diff --git a/src/renderer/components/shared/bs-content-tab-panel/bs-content-tab-panel.component.tsx b/src/renderer/components/shared/bs-content-tab-panel/bs-content-tab-panel.component.tsx index 6cc1257e..2ee8f606 100644 --- a/src/renderer/components/shared/bs-content-tab-panel/bs-content-tab-panel.component.tsx +++ b/src/renderer/components/shared/bs-content-tab-panel/bs-content-tab-panel.component.tsx @@ -18,11 +18,11 @@ export function BsContentTabPanel({className, tabIndex, tabs, onTabChange, child
diff --git a/src/renderer/components/shared/theme-color-gradient-spliter.component.tsx b/src/renderer/components/shared/theme-color-gradient-spliter.component.tsx new file mode 100644 index 00000000..a9bea849 --- /dev/null +++ b/src/renderer/components/shared/theme-color-gradient-spliter.component.tsx @@ -0,0 +1,11 @@ +import { ComponentProps } from "react"; +import { useThemeColor } from "renderer/hooks/use-theme-color.hook"; + +export function ThemeColorGradientSpliter(props: ComponentProps<"div">) { + + const colors = useThemeColor(); + + return ( +
+ ) +} diff --git a/src/renderer/components/svgs/icons/search-icon.component.tsx b/src/renderer/components/svgs/icons/search-icon.component.tsx index 82293156..88c8076e 100644 --- a/src/renderer/components/svgs/icons/search-icon.component.tsx +++ b/src/renderer/components/svgs/icons/search-icon.component.tsx @@ -1,9 +1,9 @@ -import { CSSProperties } from "react"; +import { createSvgIcon } from "../svg-icon.type"; -export function SearchIcon(props: { className?: string; style?: CSSProperties }) { +export const SearchIcon = createSvgIcon((props, ref) => { return ( - + ); -} +}); diff --git a/src/renderer/pages/version-viewer.component.tsx b/src/renderer/pages/version-viewer.component.tsx index 3e36b3b7..dc1282c0 100644 --- a/src/renderer/pages/version-viewer.component.tsx +++ b/src/renderer/pages/version-viewer.component.tsx @@ -46,7 +46,7 @@ export function VersionViewer() { const verifyFiles = () => bsDownloader.verifyBsVersion(state); const uninstall = async () => { - const modalCompleted = await modalService.openModal(UninstallModal, state); + const modalCompleted = await modalService.openModal(UninstallModal, { data: state }); if (modalCompleted.exitCode === ModalExitCode.COMPLETED) { bsUninstallerService.uninstall(state).then(() => { bsVersionManagerService.askInstalledVersions().then(versions => { @@ -79,11 +79,11 @@ export function VersionViewer() { }; const openShareFolderModal = () => { - modalService.openModal(ShareFoldersModal, state); + modalService.openModal(ShareFoldersModal, {data: state}); }; const createLaunchShortcut = async () => { - const { exitCode, data } = await modalService.openModal(CreateLaunchShortcutModal, state); + const { exitCode, data } = await modalService.openModal(CreateLaunchShortcutModal, {data: state}); if(exitCode !== ModalExitCode.COMPLETED){ return; } lastValueFrom(bsLauncher.createLaunchShortcut(data)).then(() => { diff --git a/src/renderer/services/auto-updater.service.ts b/src/renderer/services/auto-updater.service.ts index d4de5b2e..1e7fdff8 100644 --- a/src/renderer/services/auto-updater.service.ts +++ b/src/renderer/services/auto-updater.service.ts @@ -109,6 +109,6 @@ export class AutoUpdaterService { public async showChangelog(version:string): Promise{ const changelog = await this.getChangelogVersion(version); - this.modal.openModal(ChangelogModal, changelog); + this.modal.openModal(ChangelogModal, {data: changelog}); } } diff --git a/src/renderer/services/bs-mods-manager.service.ts b/src/renderer/services/bs-mods-manager.service.ts index 2aa9819a..b48eb747 100644 --- a/src/renderer/services/bs-mods-manager.service.ts +++ b/src/renderer/services/bs-mods-manager.service.ts @@ -90,7 +90,7 @@ export class BsModsManagerService { return; } - const modalRes = await this.modals.openModal(UninstallModModal, mod); + const modalRes = await this.modals.openModal(UninstallModModal, {data: mod}); if (modalRes.exitCode !== ModalExitCode.COMPLETED) { return; @@ -117,7 +117,7 @@ export class BsModsManagerService { return; } - const modalRes = await this.modals.openModal(UninstallAllModsModal, version); + const modalRes = await this.modals.openModal(UninstallAllModsModal, {data: version}); if (modalRes.exitCode !== ModalExitCode.COMPLETED) { return; diff --git a/src/renderer/services/bs-version-download/steam-downloader.service.ts b/src/renderer/services/bs-version-download/steam-downloader.service.ts index e679d0ca..d906169f 100644 --- a/src/renderer/services/bs-version-download/steam-downloader.service.ts +++ b/src/renderer/services/bs-version-download/steam-downloader.service.ts @@ -15,7 +15,7 @@ import { DownloaderServiceInterface } from "./bs-store-downloader.interface"; import { AbstractBsDownloaderService } from "./abstract-bs-downloader.service"; export class SteamDownloaderService extends AbstractBsDownloaderService implements DownloaderServiceInterface{ - + private static instance: SteamDownloaderService; public static getInstance(): SteamDownloaderService { @@ -102,7 +102,7 @@ export class SteamDownloaderService extends AbstractBsDownloaderService implemen take(1), ).subscribe(async () => { const logged$ = events$.pipe(filter(event => event.subType === DepotDownloaderInfoEvent.SteamID), take(1)); - const res = await this.modalService.openModal(SteamMobileApproveModal, { logged$ }); + const res = await this.modalService.openModal(SteamMobileApproveModal, {data: { logged$ }}); if(res.exitCode !== ModalExitCode.COMPLETED){ return this.stopDownload(); } @@ -128,7 +128,7 @@ export class SteamDownloaderService extends AbstractBsDownloaderService implemen return this.notificationService.notifySuccess({title: "notifications.bs-download.success.titles.verification-finished"}); } return this.notificationService.notifySuccess({title: "notifications.bs-download.success.titles.download-success"}); - })); + })); return subs; } @@ -182,7 +182,7 @@ export class SteamDownloaderService extends AbstractBsDownloaderService implemen }), share({connector: () => new ReplaySubject(1)}) ); - + } private tryAutoDownloadBsVersion(downloadInfo: DownloadSteamInfo){ @@ -217,7 +217,7 @@ export class SteamDownloaderService extends AbstractBsDownloaderService implemen } this.progressBarService.show(this.downloadProgress$, true); - + const downloadPromise = (async () => { const haveDotNet = await this.isDotNet6Installed().catch(() => false); @@ -227,16 +227,16 @@ export class SteamDownloaderService extends AbstractBsDownloaderService implemen } const downloadInfo: DownloadSteamInfo = {bsVersion, isVerification} - + const autoDownload = await lastValueFrom(this.tryAutoDownloadBsVersion(downloadInfo)).then(() => true).catch(() => false); - + if(autoDownload){ return Promise.resolve(); } const qrCodeDownload$ = this.startQrCodeDownload(downloadInfo); const qrCode$ = qrCodeDownload$.pipe(filter(event => event.type === DepotDownloaderEventType.Info && event.subType === DepotDownloaderInfoEvent.QRCode), map(event => event.data as string)); const logged$ = qrCodeDownload$.pipe(filter(event => event.type === DepotDownloaderEventType.Info && event.subType === DepotDownloaderInfoEvent.SteamID), map(event => event.data as string), take(1)); - const loginRes = await this.modalService.openModal(LoginToSteamModal, { qrCode$, logged$ }); + const loginRes = await this.modalService.openModal(LoginToSteamModal, {data: { qrCode$, logged$ }}); if(loginRes.exitCode !== ModalExitCode.COMPLETED){ return Promise.resolve(); diff --git a/src/renderer/services/bs-version-manager.service.ts b/src/renderer/services/bs-version-manager.service.ts index 313d26ad..5faf00ee 100644 --- a/src/renderer/services/bs-version-manager.service.ts +++ b/src/renderer/services/bs-version-manager.service.ts @@ -66,7 +66,7 @@ export class BSVersionManagerService { } public async editVersion(version: BSVersion): Promise { - const modalRes = await this.modalService.openModal(EditVersionModal, { version, clone: false }); + const modalRes = await this.modalService.openModal(EditVersionModal, {data: { version, clone: false }}); if (modalRes.exitCode !== ModalExitCode.COMPLETED) { return null; } @@ -90,7 +90,7 @@ export class BSVersionManagerService { if (!this.progressBar.require()) { return null; } - const modalRes = await this.modalService.openModal(EditVersionModal, { version, clone: true }); + const modalRes = await this.modalService.openModal(EditVersionModal, {data: { version, clone: true }}); if (modalRes.exitCode !== ModalExitCode.COMPLETED) { return null; } @@ -128,7 +128,7 @@ export class BSVersionManagerService { if(resModal.exitCode !== ModalExitCode.COMPLETED){ return; } - + const store = resModal.data; const folderRes = await lastValueFrom(this.ipcService.sendV2<{ canceled: boolean; filePaths: string[] }>("choose-folder")); diff --git a/src/renderer/services/maps-downloader.service.ts b/src/renderer/services/maps-downloader.service.ts index 7f321ecd..42173beb 100644 --- a/src/renderer/services/maps-downloader.service.ts +++ b/src/renderer/services/maps-downloader.service.ts @@ -78,7 +78,7 @@ export class MapsDownloaderService { } public async openDownloadMapModal(version?: BSVersion, ownedMaps: BsmLocalMap[] = []): Promise> { - const res = await this.modals.openModal(DownloadMapsModal, { version, ownedMaps }); + const res = await this.modals.openModal(DownloadMapsModal, {data: { version, ownedMaps }}); this.progressBar.setStyle(null); return res; } diff --git a/src/renderer/services/maps-manager.service.ts b/src/renderer/services/maps-manager.service.ts index 8aeed33f..a63ca2c3 100644 --- a/src/renderer/services/maps-manager.service.ts +++ b/src/renderer/services/maps-manager.service.ts @@ -89,7 +89,7 @@ export class MapsManagerService { const askModal = maps.length > 1 || !this.config.get(MapsManagerService.REMEMBER_CHOICE_DELETE_MAP_KEY); if (askModal) { - const modalRes = await this.modal.openModal(DeleteMapsModal, { linked: versionLinked, maps }); + const modalRes = await this.modal.openModal(DeleteMapsModal, {data: { linked: versionLinked, maps }}); if (modalRes.exitCode !== ModalExitCode.COMPLETED) { return false; } diff --git a/src/renderer/services/modale.service.ts b/src/renderer/services/modale.service.ts index 650c89aa..e041def2 100644 --- a/src/renderer/services/modale.service.ts +++ b/src/renderer/services/modale.service.ts @@ -1,12 +1,10 @@ import { Observable, BehaviorSubject } from "rxjs"; -import { timeout } from "rxjs/operators"; export class ModalService { private static instance: ModalService; - private _modalToShow$: BehaviorSubject = new BehaviorSubject(null); + private _modalToShow$: BehaviorSubject = new BehaviorSubject([]); private modalData: unknown = null; - private resolver: (value: ModalResponse | PromiseLike) => void = null; private constructor() {} @@ -17,47 +15,33 @@ export class ModalService { return this.instance; } - private close() { - if (this.resolver) { - this.resolve({ exitCode: ModalExitCode.NO_CHOICE }); - } - this._modalToShow$.next(null); - } - public getModalData(): Type { return this.modalData as Type; } - public getResolver(): any { - return this.resolver; - } - - public resolve(data: ModalResponse): void { - this.resolver(data); - } - - public async openModal(modal: ModalComponent, data?: K): Promise> { - this.close(); - await timeout(100); // Must wait resolve + public async openModal(modal: ModalComponent, options?: ModalOptions): Promise> { + let resolver: (value: ModalResponse | PromiseLike) => void; const promise = new Promise>(resolve => { - this.resolver = resolve as (value: ModalResponse | PromiseLike) => void; + resolver = resolve as (value: ModalResponse | PromiseLike) => void; }); - this._modalToShow$.next(modal as ModalComponent); - promise.then(() => this.close()); - if (data) { - this.modalData = data; - } else { - this.modalData = null; - } + const modalObj = {modal: modal as ModalComponent, resolver: resolver, options}; + this._modalToShow$.next([...this._modalToShow$.getValue(), modalObj]); + + promise.then(() => { + this._modalToShow$.next(this._modalToShow$.getValue().filter(m => m !== modalObj)); + }); + return promise; } - public getModalToShow(): Observable { + public getModalToShow(): Observable { return this._modalToShow$.asObservable(); } } -export type ModalComponent = ({ resolver, data }: { resolver: (x: ModalResponse) => void; data?: Receive }) => JSX.Element; +export type ModalOptions = { data?: T, noStyle?: boolean } +export type ModalComponent = ({ resolver, options }: { resolver: (x: ModalResponse) => void; options?: ModalOptions }) => JSX.Element; +export type ModalObject = {modal: ModalComponent, resolver: (value: ModalResponse | PromiseLike) => void, options: ModalOptions}; export const enum ModalExitCode { NO_CHOICE = -1, diff --git a/src/renderer/services/models-management/models-downloader.service.ts b/src/renderer/services/models-management/models-downloader.service.ts index a1b395a2..48ecf1e6 100644 --- a/src/renderer/services/models-management/models-downloader.service.ts +++ b/src/renderer/services/models-management/models-downloader.service.ts @@ -118,7 +118,7 @@ export class ModelsDownloaderService { } public openDownloadModelsModal(version: BSVersion, type?: MSModelType, owned?: BsmLocalModel[]): Promise> { - return this.modal.openModal(DownloadModelsModal, { version, type, owned }); + return this.modal.openModal(DownloadModelsModal, {data: { version, type, owned }}); } public async oneClickInstallModel(model: MSModel): Promise { diff --git a/src/renderer/services/models-management/models-manager.service.ts b/src/renderer/services/models-management/models-manager.service.ts index 0c1c230e..39837c11 100644 --- a/src/renderer/services/models-management/models-manager.service.ts +++ b/src/renderer/services/models-management/models-manager.service.ts @@ -73,7 +73,7 @@ export class ModelsManagerService { } public async linkModels(type: MSModelType, version?: BSVersion): Promise { - const res = await this.modalService.openModal(LinkModelsModal, type); + const res = await this.modalService.openModal(LinkModelsModal, {data: type}); if (res.exitCode !== ModalExitCode.COMPLETED) { return null; @@ -82,13 +82,12 @@ export class ModelsManagerService { return this.versionFolderLinked.linkVersionFolder({ version, relativeFolder: MODEL_TYPE_FOLDERS[type], - type: VersionLinkerActionType.Link, options: { keepContents: res.data !== false }, }); } public async unlinkModels(type: MSModelType, version?: BSVersion): Promise { - const res = await this.modalService.openModal(UnlinkModelsModal, type); + const res = await this.modalService.openModal(UnlinkModelsModal, {data: type}); if (res.exitCode !== ModalExitCode.COMPLETED) { return null; @@ -97,7 +96,6 @@ export class ModelsManagerService { return this.versionFolderLinked.unlinkVersionFolder({ version, relativeFolder: MODEL_TYPE_FOLDERS[type], - type: VersionLinkerActionType.Unlink, options: { keepContents: res.data !== false }, }); } @@ -165,7 +163,7 @@ export class ModelsManagerService { return false; })(); - const res = await this.modalService.openModal(DeleteModelsModal, { models, linked }); + const res = await this.modalService.openModal(DeleteModelsModal, {data: { models, linked }}); if (res.exitCode !== ModalExitCode.COMPLETED) { return Promise.resolve([]); } diff --git a/src/renderer/services/playlists-manager.service.ts b/src/renderer/services/playlists-manager.service.ts index 9aca80cc..461b01f4 100644 --- a/src/renderer/services/playlists-manager.service.ts +++ b/src/renderer/services/playlists-manager.service.ts @@ -3,9 +3,8 @@ import { IpcService } from "./ipc.service"; 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 { LocalBPListsDetails } 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"; @@ -31,8 +30,8 @@ export class PlaylistsManagerService { this.modal = ModalService.getInstance(); } - public getVersionPlaylists(version: BSVersion): Observable> { - return this.ipc.sendV2("get-version-playlists", { args: version }); + public getVersionPlaylistsDetails(version: BSVersion): Observable> { + return this.ipc.sendV2("get-version-playlists-details", { args: version }); } public async linkVersion(version: BSVersion): Promise { diff --git a/src/renderer/windows/App.tsx b/src/renderer/windows/App.tsx index 6ea15823..920d7688 100644 --- a/src/renderer/windows/App.tsx +++ b/src/renderer/windows/App.tsx @@ -24,6 +24,8 @@ import { OsDiagnosticService } from "renderer/services/os-diagnostic.service"; import { useService } from "renderer/hooks/use-service.hook"; import { AutoUpdaterService } from "renderer/services/auto-updater.service"; import { gt } from "semver" +import { ModalService } from "renderer/services/modale.service"; +import { ChooseStore } from "renderer/components/modal/modal-types/bs-downgrade/choose-store-modal.component"; export default function App() { useService(OsDiagnosticService); @@ -34,6 +36,7 @@ export default function App() { const notification = useService(NotificationService); const config = useService(ConfigurationService); const autoUpdater = useService(AutoUpdaterService); + const modals = useService(ModalService); const location = useLocation(); const navigate = useNavigate(); @@ -55,6 +58,7 @@ export default function App() { }; const checkOneClicks = async () => { + if (config.get("not-remind-oneclick") === true) { return; } diff --git a/src/shared/models/maps/beat-saver.model.ts b/src/shared/models/maps/beat-saver.model.ts index 0cb2968b..5980a12d 100644 --- a/src/shared/models/maps/beat-saver.model.ts +++ b/src/shared/models/maps/beat-saver.model.ts @@ -1,3 +1,5 @@ +import { SongDetailDiffCharactertistic, SongDiffName } from "./song-details-cache.model"; + export interface BsvMapDetail { automapper: boolean; createdAt: string; @@ -115,10 +117,10 @@ export interface BsvMapTestplay { export interface BsvMapDifficulty { bombs: number; - characteristic: BsvMapCharacteristic; + characteristic: SongDetailDiffCharactertistic; chroma: boolean; cinema: boolean; - difficulty: BsvMapDifficultyType; + difficulty: SongDiffName; events: number; length: number; maxScore: number; diff --git a/src/shared/models/playlists/local-playlist.models.ts b/src/shared/models/playlists/local-playlist.models.ts index f69d22ea..a04376c5 100644 --- a/src/shared/models/playlists/local-playlist.models.ts +++ b/src/shared/models/playlists/local-playlist.models.ts @@ -1,13 +1,16 @@ -import { SongDetails } from "../maps"; -import { BPList, PlaylistSong } from "./playlist.interface"; +import { BPList } from "./playlist.interface"; -export interface LocalBPList extends BPList { +export interface LocalBPList extends BPList { path: string; } -export interface LocalBpListSong { - song: PlaylistSong; - songDetails?: SongDetails; - songUrl?: string; - coverUrl?: string; +export interface LocalBPListsDetails extends LocalBPList { + nbMaps: number; + id?: number; + nbMappers?: number; + duration?: number; + minNps?: number; + maxNps?: number; } + + diff --git a/src/shared/models/playlists/playlist.interface.ts b/src/shared/models/playlists/playlist.interface.ts index 5fde662d..4667f7ad 100644 --- a/src/shared/models/playlists/playlist.interface.ts +++ b/src/shared/models/playlists/playlist.interface.ts @@ -6,7 +6,7 @@ export interface BPList { playlistAuthor: string; playlistDescription?: string; image: string; - customData: unknown; + customData?: CustomDataBPList; songs: SongType[]; } @@ -24,3 +24,7 @@ export interface DownloadPlaylistProgressionData { playlistInfos: BPList; playlistPath: string; } + +export interface CustomDataBPList { + syncURL?: string; +}