From 15ebe8e92912f66057cc6993ba750b304a4c8201 Mon Sep 17 00:00:00 2001 From: MathieuG-P <40181755+Zagrios@users.noreply.github.com> Date: Mon, 10 Jun 2024 16:22:52 +0200 Subject: [PATCH] [feature-107] minor fixes + use virtual scroll in playlists details view --- .../local-playlists-manager.service.ts | 64 +++----- .../maps/local-maps-manager.service.ts | 4 +- .../local-playlists-list-panel.component.tsx | 13 +- .../download-maps-modal.component.tsx | 2 +- .../bsv-playlist-details-modal.component.tsx | 144 +++++++++++++----- ...local-playlist-details-modal.component.tsx | 62 +++++--- .../services/playlists-manager.service.ts | 5 +- src/shared/models/ipc/ipc-routes.ts | 2 +- 8 files changed, 181 insertions(+), 115 deletions(-) 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 2302456a..ea2710c8 100644 --- a/src/main/services/additional-content/local-playlists-manager.service.ts +++ b/src/main/services/additional-content/local-playlists-manager.service.ts @@ -1,5 +1,5 @@ import path from "path"; -import { Observable, Subject, from, lastValueFrom, mergeMap, take, takeUntil, tap } from "rxjs"; +import { Observable, Subject, from, lastValueFrom, takeUntil, tap } from "rxjs"; import { BSVersion } from "shared/bs-version.interface"; import { BSLocalVersionService } from "../bs-local-version.service"; import { DeepLinkService } from "../deep-link.service"; @@ -7,11 +7,11 @@ import { RequestService } from "../request.service"; import { LocalMapsManagerService } from "./maps/local-maps-manager.service"; import log from "electron-log"; import { WindowManagerService } from "../window-manager.service"; -import { BPList, DownloadPlaylistProgressionData, PlaylistSong } from "shared/models/playlists/playlist.interface"; +import { BPList, DownloadPlaylistProgressionData } from "shared/models/playlists/playlist.interface"; import { readFileSync } from "fs"; import { BeatSaverService } from "../thrid-party/beat-saver/beat-saver.service"; -import { copy, copyFile, ensureDir, pathExists, pathExistsSync, readdirSync, realpath, writeFile, writeFileSync } from "fs-extra"; -import { Progression, ensurePathNotAlreadyExist, ensurePathNotAlreadyExistSync, pathExist, unlinkPath } from "../../helpers/fs.helpers"; +import { copy, ensureDir, pathExists, pathExistsSync, readdirSync, realpath, writeFileSync } from "fs-extra"; +import { Progression, unlinkPath } 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"; @@ -20,9 +20,9 @@ import { SongCacheService } from "./maps/song-cache.service"; import { InstallationLocationService } from "../installation-location.service"; import sanitize from "sanitize-filename"; import { isValidUrl } from "shared/helpers/url.helpers"; -import { allSettled } from "shared/helpers/promise.helpers"; import { Archive } from "main/models/archive.class"; import { CustomError } from "shared/models/exceptions/custom-error.class"; +import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"; export class LocalPlaylistsManagerService { private static instance: LocalPlaylistsManagerService; @@ -291,7 +291,7 @@ export class LocalPlaylistsManagerService { return from(unlinkPath(bpList.path)); } - public exportPlaylists(opt: {version?: BSVersion, bpLists: LocalBPList[], dest: string, exportMaps?: boolean}): Observable> { + public exportPlaylists(opt: {version?: BSVersion, bpLists: LocalBPList[], dest: string, playlistsMaps?: BsmLocalMap[]}): Observable> { if(!pathExistsSync(opt.dest)) { throw new CustomError(`Destination folder not found ${opt.dest}`, "DEST_ENOENT"); @@ -310,55 +310,31 @@ export class LocalPlaylistsManagerService { for(const bpList of opt.bpLists) { if(!pathExistsSync(bpList.path)) { - throw new CustomError(`Playlist file not found ${bpList.path}`, "PLAYLIST_ENOENT"); + log.warn(`Playlist file not found for export`, bpList.path); + continue; } archive.addFile(bpList.path, path.join(this.PLAYLISTS_FOLDER, path.basename(bpList.path))); } - if(!opt.exportMaps) { + if(!Array.isArray(opt.playlistsMaps) || opt.playlistsMaps.length === 0){ return archive.finalize(); } - const mapsHashsToExport = Array.from( - new Set(opt.bpLists.reduce((acc, bpList) => acc.concat((bpList.songs ?? []).map(s => s.hash)), [])).values() - ); + for(const map of opt.playlistsMaps) { - const zipMaps$ = new Observable>(obs => { - (async () => { - const progress: Progression = { total: mapsHashsToExport.length, current: 0, data: zipDest }; + if(!map?.path || !pathExistsSync(map.path)) { + log.warn(`Map file not found for playlist export`, map?.path); + continue; + } - for(const hash of mapsHashsToExport) { - const mapInfo = await this.maps.getMapInfoFromHash(hash, opt.version); - - if(!mapInfo || !pathExistsSync(mapInfo.path)) { continue; } - - archive.addDirectory( - mapInfo.path, - path.join("Maps", path.basename(mapInfo.path)) // Dont't know why, but "CustomLevels" not work - ); - progress.current += 1; - - obs.next(progress); - } - - })() - .catch(err => obs.error(err)) - .finally(() => obs.complete()); - }); - - return new Observable>(obs => { - (async () => { - const maps$ = zipMaps$.pipe(tap({ next: p => obs.next(p) })); - const archive$ = archive.finalize().pipe(tap({ next: p => obs.next(p) })); - - await lastValueFrom(maps$); - await lastValueFrom(archive$); - })() - .catch(err => obs.error(err)) - .finally(() => obs.complete()); - }) + archive.addDirectory( + map.path, + path.join("Maps", path.basename(map.path)) // Dont't know why, but "CustomLevels" not work + ); + } + return archive.finalize(); } public oneClickInstallPlaylist(bpListUrl: string): Observable> { 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 ca55f084..757ee6a5 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 @@ -287,9 +287,9 @@ export class LocalMapsManagerService { const versionMapsPath = await this.getMapsFolderPath(version); const mapInfo = this.songCache.getMapInfoFromHash(hash); - const cachedMapPath = path.join(versionMapsPath, mapInfo.dirname); + const cachedMapPath = (versionMapsPath && mapInfo?.dirname) && path.join(versionMapsPath, mapInfo.dirname); - if(pathExistsSync(cachedMapPath)){ + if(cachedMapPath && pathExistsSync(cachedMapPath)){ return this.loadMapInfoFromPath(cachedMapPath); } 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 b4340c57..ed1e70aa 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 @@ -53,6 +53,8 @@ export type LocalPlaylistsListRef = { exportPlaylists: () => Promise; } +// TODO : Translate + export const LocalPlaylistsListPanel = forwardRef(({ version, className, filter: playlistFiler, search, isActive, linkedState }, forwardedRef) => { const t = useTranslation(); @@ -113,14 +115,17 @@ export const LocalPlaylistsListPanel = forwardRef( if(!toExport.length){ return; } - const modalRes = await modals.openModal(ExportPlaylistModal, { data: toExport }); - if(modalRes.exitCode !== ModalExitCode.COMPLETED){ return; } + const { exitCode, data: exportMaps } = await modals.openModal(ExportPlaylistModal, { data: toExport }); + if(exitCode !== ModalExitCode.COMPLETED){ return; } const folderRes = await lastValueFrom(ipc.sendV2("choose-folder")); if(!folderRes || folderRes.canceled || !folderRes.filePaths?.length){ return; } - if(modalRes.exitCode !== ModalExitCode.COMPLETED){ return; } - const obs$ = playlistService.exportPlaylists({ version, bpLists: toExport, dest: folderRes.filePaths.at(0), exportMaps: modalRes.data }); + const mapsToExport = exportMaps ? ( + maps$?.value?.filter(m => toExport.some(p => p.songs.some(s => s.hash.toLocaleLowerCase() === m.hash.toLocaleLowerCase()))) ?? [] + ) : []; + + const obs$ = playlistService.exportPlaylists({ version, bpLists: toExport, dest: folderRes.filePaths.at(0), playlistsMaps: mapsToExport }); progess.show(obs$, true); 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 e364d458..01e0199e 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 @@ -60,7 +60,7 @@ export const DownloadMapsModal: ModalComponent { loadMaps(searchParams); diff --git a/src/renderer/components/modal/modal-types/playlist/playlist-details-modal/bsv-playlist-details-modal.component.tsx b/src/renderer/components/modal/modal-types/playlist/playlist-details-modal/bsv-playlist-details-modal.component.tsx index 686fbbb9..5980d86f 100644 --- a/src/renderer/components/modal/modal-types/playlist/playlist-details-modal/bsv-playlist-details-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/playlist/playlist-details-modal/bsv-playlist-details-modal.component.tsx @@ -5,19 +5,19 @@ import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"; import { Observable } from "rxjs"; import { useObservable } from "renderer/hooks/use-observable.hook"; import { ModalComponent, ModalExitCode } from "renderer/services/modale.service"; -import { useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { MapItem, extractMapDiffs } from "renderer/components/maps-playlists-panel/maps/map-item.component"; import { useOnUpdate } from "renderer/hooks/use-on-update.hook"; import { useService } from "renderer/hooks/use-service.hook"; import { BeatSaverService } from "renderer/services/thrird-partys/beat-saver.service"; import { getLocalTimeZone, parseAbsolute, toCalendarDateTime } from "@internationalized/date"; -import { motion } from "framer-motion"; import { MapsDownloaderService } from "renderer/services/maps-downloader.service"; import equal from "fast-deep-equal"; import BeatWaiting from "../../../../../../../assets/images/apngs/beat-waiting.png" import BeatConflict from "../../../../../../../assets/images/apngs/beat-conflict.png" import { BsmImage } from "renderer/components/shared/bsm-image.component"; import { cn } from "renderer/helpers/css-class.helpers"; +import { VirtualScroll } from "renderer/components/shared/virtual-scroll/virtual-scroll.component"; type Props = { version: BSVersion; @@ -32,10 +32,12 @@ export const BsvPlaylistDetailsModal: ModalComponent = ({ resolver, const beatsaver = useService(BeatSaverService); const mapsDownloader = useService(MapsDownloaderService); + const currentMapDownload = useObservable(() => mapsDownloader.currentMapDownload$, null); const downloadingMaps = useObservable(() => mapsDownloader.mapsInQueue$, []); const installedMaps = useObservable(() => installedMaps$, null); const [page, setPage] = useState(0); const [playlistMaps, setPlaylistMaps] = useState(null); + const [downloadbleMaps, setDownloadbleMaps] = useState([]); const [error, setError] = useState(false); useOnUpdate(() => { @@ -44,17 +46,61 @@ export const BsvPlaylistDetailsModal: ModalComponent = ({ resolver, .catch(() => setError(() => true)); }, [page]); + + useEffect(() => { + const ownedMapHashs = installedMaps?.map(map => map.hash) ?? []; + + if(!Array.isArray(playlistMaps)){ + return setDownloadbleMaps([]); + } + + setDownloadbleMaps(() => playlistMaps.map(map => { + const isMapOwned = map.versions.some(version => ownedMapHashs.includes(version.hash)); + const isDownloading = map.id === currentMapDownload?.map?.id; + const inQueue = downloadingMaps.some(toDownload => equal(toDownload.version, version) && toDownload.map.id === map.id); + + return { map, isOwned: isMapOwned, idDownloading: isDownloading, isInQueue: inQueue }; + })); + }, [playlistMaps, currentMapDownload, downloadingMaps, installedMaps]) + + const renderMapItem = useCallback((downloadableMap: DownloadableMap) => { + const map = downloadableMap.map; + + const downloadable = !downloadableMap.isOwned && !downloadableMap.isInQueue; + const cancelable = downloadableMap.isInQueue && !downloadableMap.idDownloading; + + return mapsDownloader.addMapToDownload({version, map}))} + onCancelDownload={cancelable && (() => mapsDownloader.removeMapToDownload({version, map}))} + downloading={downloadableMap.idDownloading} + callBackParam={map} />; + }, [version]); + return ( resolver({ exitCode: ModalExitCode.CLOSED })} > {(() => { @@ -82,41 +128,63 @@ export const BsvPlaylistDetailsModal: ModalComponent = ({ resolver, } return ( -
    - {playlistMaps.map(map => { + setPage(prev => prev + 1), + margin: 100 + }} + /> + //
      + // {playlistMaps.map(map => { - const downloadingMap = downloadingMaps.at(0); - const isMapOwned = installedMaps?.some(installedMap => installedMap.hash === map.versions.at(0).hash); - const isMapInQueue = downloadingMaps.some(downloadingMap => downloadingMap.map.versions.at(0).hash === map.versions.at(0).hash); - const isMapDownloading = (isMapInQueue && downloadingMap) ? equal(downloadingMap.version, version) && downloadingMap.map.versions.at(0).hash === map.versions.at(0).hash : false; + // const downloadingMap = downloadingMaps.at(0); + // const isMapOwned = installedMaps?.some(installedMap => installedMap.hash === map.versions.at(0).hash); + // const isMapInQueue = downloadingMaps.some(downloadingMap => downloadingMap.map.versions.at(0).hash === map.versions.at(0).hash); + // const isMapDownloading = (isMapInQueue && downloadingMap) ? equal(downloadingMap.version, version) && downloadingMap.map.versions.at(0).hash === map.versions.at(0).hash : false; - return ( - mapsDownloader.addMapToDownload({version, map}))} - onCancelDownload={isMapInQueue && !isMapDownloading && (() => mapsDownloader.removeMapToDownload({version, map}))} - callBackParam={map} - />); - })} - setPage(prev => prev + 1)} /> -
    + // return ( + // mapsDownloader.addMapToDownload({version, map}))} + // onCancelDownload={isMapInQueue && !isMapDownloading && (() => mapsDownloader.removeMapToDownload({version, map}))} + // callBackParam={map} + // />); + // })} + // setPage(prev => prev + 1)} /> + //
) })()}
) } + +type DownloadableMap = { + map: BsvMapDetail; + isOwned: boolean; + idDownloading: boolean; + isInQueue: boolean; +}; diff --git a/src/renderer/components/modal/modal-types/playlist/playlist-details-modal/local-playlist-details-modal.component.tsx b/src/renderer/components/modal/modal-types/playlist/playlist-details-modal/local-playlist-details-modal.component.tsx index 71f050b4..9f08114a 100644 --- a/src/renderer/components/modal/modal-types/playlist/playlist-details-modal/local-playlist-details-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/playlist/playlist-details-modal/local-playlist-details-modal.component.tsx @@ -1,4 +1,4 @@ -import { ModalComponent } from "renderer/services/modale.service" +import { ModalComponent, ModalExitCode } from "renderer/services/modale.service" import { PlaylistDetailsTemplate } from "./playlist-details-template.component" import { Observable, combineLatest, lastValueFrom, map, switchMap } from "rxjs" import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface" @@ -14,6 +14,8 @@ import { LocalBPListsDetails } from "shared/models/playlists/local-playlist.mode import { PlaylistDownloaderService } from "renderer/services/playlist-downloader.service"; import { PlaylistHeaderState } from "./playlist-header-state.component"; import { useConstant } from "renderer/hooks/use-constant.hook"; +import { useCallback } from "react"; +import { VirtualScroll } from "renderer/components/shared/virtual-scroll/virtual-scroll.component"; // TODO : Translate @@ -58,6 +60,29 @@ export const LocalPlaylistDetailsModal: ModalComponent = ({resolver return lastValueFrom(obs$); } + const renderMapItem = useCallback((map: BsmLocalMap) => { + return ( + + ); + }, []); + const renderMaps = () => { if (!Array.isArray(installedMaps) && !isInQueue) { return ( @@ -98,28 +123,18 @@ export const LocalPlaylistDetailsModal: ModalComponent = ({resolver isPlaylistInQueue$={isPlaylistInQueue$} installPlaylist={installPlaylist} /> -
    - {installedMaps.map(map => ( - - ))} -
+ ) } @@ -135,6 +150,7 @@ export const LocalPlaylistDetailsModal: ModalComponent = ({resolver nbMaps={localPlaylist?.nbMaps} nbMappers={localPlaylist?.nbMappers} title={localPlaylist?.playlistTitle} + onClose={() => resolver({ exitCode: ModalExitCode.CLOSED })} > {renderMaps()} diff --git a/src/renderer/services/playlists-manager.service.ts b/src/renderer/services/playlists-manager.service.ts index 244b002e..c6d64897 100644 --- a/src/renderer/services/playlists-manager.service.ts +++ b/src/renderer/services/playlists-manager.service.ts @@ -7,6 +7,7 @@ import { LocalBPList, LocalBPListsDetails } from "shared/models/playlists/local- import { ModalExitCode, ModalService } from "./modale.service"; import { UnlinkPlaylistModal } from "renderer/components/modal/modal-types/unlink-playlist-modal.component"; import { LinkPlaylistModal } from "renderer/components/modal/modal-types/link-playlist-modal.component"; +import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"; export class PlaylistsManagerService { private static instance: PlaylistsManagerService; @@ -38,12 +39,12 @@ export class PlaylistsManagerService { return this.ipc.sendV2("delete-playlist", opt); } - public exportPlaylists(opt: {version: BSVersion, bpLists: LocalBPList[], dest: string, exportMaps?: boolean}): Observable> { + public exportPlaylists(opt: {version: BSVersion, bpLists: LocalBPList[], dest: string, playlistsMaps?: BsmLocalMap[]}): Observable> { return this.ipc.sendV2("export-playlists", { version: opt.version, bpLists: opt.bpLists, dest: opt.dest, - exportMaps: opt.exportMaps + playlistsMaps: opt.playlistsMaps }); } diff --git a/src/shared/models/ipc/ipc-routes.ts b/src/shared/models/ipc/ipc-routes.ts index 9f00e169..984499ad 100644 --- a/src/shared/models/ipc/ipc-routes.ts +++ b/src/shared/models/ipc/ipc-routes.ts @@ -84,7 +84,7 @@ export interface IpcChannelMapping { "download-playlist": {request: {downloadSource: string, dest?: string, version?: BSVersion, ignoreSongsHashs?: string[]}, response: Progression}; "get-version-playlists-details": {request: BSVersion, response: Progression}; "delete-playlist": {request: {version: BSVersion, bpList: LocalBPList, deleteMaps?: boolean}, response: Progression}; - "export-playlists": {request: {version?: BSVersion, bpLists: LocalBPList[], dest: string, exportMaps?: boolean}, response: Progression}; + "export-playlists": {request: {version?: BSVersion, bpLists: LocalBPList[], dest: string, playlistsMaps?: BsmLocalMap[]}, response: Progression}; /* ** bs-uninstall-ipcs ** */ "bs.uninstall": { request: BSVersion, response: boolean };