diff --git a/assets/proto/song_details_cache_v1.proto b/assets/proto/song_details_cache_v1.proto index 679687ad..495a3734 100644 --- a/assets/proto/song_details_cache_v1.proto +++ b/assets/proto/song_details_cache_v1.proto @@ -6,13 +6,20 @@ message SongDetailsCache { repeated SongDetails songs = 1; uint32 lastUpdated = 2; uint32 total = 3; + UploadersList uploaders = 4; + repeated string difficultyLabels = 5; +} + +message UploadersList { + repeated string names = 1; + repeated uint32 ids = 2; } message SongDetails { uint32 idInt = 1; string hash = 2; uint32 duration = 3; - Uploader uploader = 4; + UploaderRef uploaderRef = 4; uint32 uploadedAt = 5; repeated MapTag tags = 6; bool ranked = 7; @@ -31,7 +38,7 @@ message SongDetails { message Difficulty { DifficultyLabel difficulty = 1; DifficultyCharacteristic characteristic = 2; - string label = 3; + uint32 labelIndex = 3; uint32 starsT100 = 4; uint32 starsBlT100 = 5; uint32 njsT100 = 6; @@ -46,10 +53,9 @@ message Difficulty { uint32 obstacles = 15; } -message Uploader { - string name = 1; - uint32 id = 2; - bool verified = 3; +message UploaderRef { + uint32 uploaderRefIndex = 1; + bool verified = 2; } enum DifficultyLabel { diff --git a/package-lock.json b/package-lock.json index 301c7aea..2bb89b0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "electron-store": "^8.1.0", "electron-updater": "^6.1.8", "fast-deep-equal": "^3.1.3", - "framer-motion": "^11.0.6", + "framer-motion": "^11.2.6", "fs-extra": "^11.2.0", "got": "^14.2.0", "history": "^5.3.0", @@ -38,7 +38,7 @@ "node-fetch": "^3.3.2", "node-stream-zip": "^1.15.0", "pako": "^2.1.0", - "protobufjs": "^7.2.6", + "protobufjs": "^7.3.0", "qrcode.react": "^3.1.0", "query-process": "^0.0.3", "react": "^18.2.0", @@ -1416,6 +1416,7 @@ "version": "0.8.8", "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@emotion/memoize": "0.7.4" } @@ -1423,7 +1424,8 @@ "node_modules/@emotion/memoize": { "version": "0.7.4", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/@esbuild/aix-ppc64": { "version": "0.20.1", @@ -12960,20 +12962,21 @@ } }, "node_modules/framer-motion": { - "version": "11.0.6", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.0.6.tgz", - "integrity": "sha512-BpO3mWF8UwxzO3Ca5AmSkrg14QYTeJa9vKgoLOoBdBdTPj0e81i1dMwnX6EQJXRieUx20uiDBXq8bA6y7N6b8Q==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.2.6.tgz", + "integrity": "sha512-XUrjjBt57e5YoHQtjwc3eNchFBuHvIgN/cS8SC4oIaAn2J/0+bLanUxXizidJKZVeHJam/JrmMnPRjYMglVn5g==", "dependencies": { "tslib": "^2.4.0" }, - "optionalDependencies": { - "@emotion/is-prop-valid": "^0.8.2" - }, "peerDependencies": { + "@emotion/is-prop-valid": "*", "react": "^18.0.0", "react-dom": "^18.0.0" }, "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, "react": { "optional": true }, @@ -18753,9 +18756,9 @@ } }, "node_modules/protobufjs": { - "version": "7.2.6", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.6.tgz", - "integrity": "sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.0.tgz", + "integrity": "sha512-YWD03n3shzV9ImZRX3ccbjqLxj7NokGN0V/ESiBV5xWqrommYHYiihuIyavq03pWSGqlyvYUFmfoMKd+1rPA/g==", "hasInstallScript": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", diff --git a/package.json b/package.json index 9477e9fa..c953b8a5 100644 --- a/package.json +++ b/package.json @@ -266,7 +266,7 @@ "electron-store": "^8.1.0", "electron-updater": "^6.1.8", "fast-deep-equal": "^3.1.3", - "framer-motion": "^11.0.6", + "framer-motion": "^11.2.6", "fs-extra": "^11.2.0", "got": "^14.2.0", "history": "^5.3.0", @@ -277,7 +277,7 @@ "node-fetch": "^3.3.2", "node-stream-zip": "^1.15.0", "pako": "^2.1.0", - "protobufjs": "^7.2.6", + "protobufjs": "^7.3.0", "qrcode.react": "^3.1.0", "query-process": "^0.0.3", "react": "^18.2.0", diff --git a/src/main/ipcs/beat-saver-ipcs.ts b/src/main/ipcs/beat-saver-ipcs.ts index 8d93e1f9..7a3ee0ed 100644 --- a/src/main/ipcs/beat-saver-ipcs.ts +++ b/src/main/ipcs/beat-saver-ipcs.ts @@ -23,3 +23,8 @@ ipc.on("bsv-search-playlist", (args, reply) => { const bsvService = BeatSaverService.getInstance(); reply(from(bsvService.searchPlaylists(args))); }); + +ipc.on("bsv-get-playlist-details-by-id", (args, reply) => { + const bsvService = BeatSaverService.getInstance(); + reply(from(bsvService.getPlaylistDetailsById(args.id, args.page))); +}); diff --git a/src/main/services/additional-content/maps/song-details-cache.service.ts b/src/main/services/additional-content/maps/song-details-cache.service.ts index 6a033a32..4a8d584e 100644 --- a/src/main/services/additional-content/maps/song-details-cache.service.ts +++ b/src/main/services/additional-content/maps/song-details-cache.service.ts @@ -77,6 +77,9 @@ export class SongDetailsCacheService { const res: Record = {}; + RawSongDetailsDeserializer.setUploadersList(messageObj.uploaders); + RawSongDetailsDeserializer.setDifficultyLabels(messageObj.difficultyLabels); + for(const rawSong of messageObj.songs){ res[rawSong.hash.toLocaleLowerCase()] = RawSongDetailsDeserializer.deserialize(rawSong); } diff --git a/src/main/services/thrid-party/beat-saver/beat-saver-api.service.ts b/src/main/services/thrid-party/beat-saver/beat-saver-api.service.ts index 33f3398c..0e8bc1a9 100644 --- a/src/main/services/thrid-party/beat-saver/beat-saver-api.service.ts +++ b/src/main/services/thrid-party/beat-saver/beat-saver-api.service.ts @@ -95,4 +95,8 @@ export class BeatSaverApiService { url.search = new URLSearchParams(this.objectToStringRecord(search)).toString(); return this.request.getJSON(url.toString()); } + + public getPlaylistDetailsById(id: string, page = 0): Promise { + return this.request.getJSON(`${this.bsaverApiUrl}/playlists/id/${id}/${page}`); + } } diff --git a/src/main/services/thrid-party/beat-saver/beat-saver.service.ts b/src/main/services/thrid-party/beat-saver/beat-saver.service.ts index 329332b4..316b4673 100644 --- a/src/main/services/thrid-party/beat-saver/beat-saver.service.ts +++ b/src/main/services/thrid-party/beat-saver/beat-saver.service.ts @@ -1,6 +1,6 @@ import { splitIntoChunk } from "../../../../shared/helpers/array.helpers"; import { BsvMapDetail } from "shared/models/maps"; -import { BsvPlaylist, PlaylistSearchParams, SearchParams } from "shared/models/maps/beat-saver.model"; +import { BsvPlaylist, BsvPlaylistPage, PlaylistSearchParams, SearchParams } from "shared/models/maps/beat-saver.model"; import { BeatSaverApiService } from "./beat-saver-api.service"; import log from "electron-log"; @@ -69,7 +69,15 @@ export class BeatSaverService { .then(res => res.docs) .catch(e => { log.error(e); - return e; + throw e; + }); + } + + public getPlaylistDetailsById(id: string, page = 0): Promise { + return this.bsaverApi.getPlaylistDetailsById(id, page) + .catch(e => { + log.error(e); + throw e; }); } } 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 a736a1ec..fab4705b 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 @@ -22,6 +22,7 @@ import { useService } from "renderer/hooks/use-service.hook"; import Tippy from "@tippyjs/react"; import { SongDetailDiffCharactertistic, SongDiffName } from "shared/models/maps"; import { useConstant } from "renderer/hooks/use-constant.hook"; +import { CalendarDateTime, getLocalTimeZone } from "@internationalized/date"; export type ParsedMapDiff = { type: SongDiffName; name: string; stars: number }; @@ -39,7 +40,7 @@ export type MapItemProps = { bpm: number; duration: number; likes: number; - createdAt: number; + createdAt: number | CalendarDateTime; selected?: boolean; downloading?: boolean; showOwned?: boolean; @@ -72,12 +73,18 @@ export const MapItem = memo(({ hash, title, autor, songAutor, coverUrl, songUrl, const songPlaying = useObservable(() => audioPlayer.playing$.pipe(map(playing => playing && audioPlayer.src === songUrl))); + console.log("aaa", autorId); + const MAP_DIFFICULTIES = useConstant(() => Object.values(SongDiffName)) const previewUrl = mapId ? `https://allpoland.github.io/ArcViewer/?id=${mapId}` : null; const mapUrl = mapId ? `https://beatsaver.com/maps/${mapId}` : null; const authorUrl = autorId ? `https://beatsaver.com/profile/${autorId}` : null; - const createdDate = createdAt ? dateFormat(createdAt * 1000, "d mmm yyyy") : null; const likesText = likes ? Intl.NumberFormat(undefined, { notation: "compact" }).format(likes).split(" ").join("") : null; + const createdDate = useConstant(() => { + if(!createdAt){ return null; } + const date = typeof createdAt === "number" ? new Date(createdAt * 1000) : createdAt.toDate(getLocalTimeZone()); + return dateFormat(date, "d mmm yyyy"); + }); const durationText = (() => { if (!duration) { 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 65c21c20..10fe26d3 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 @@ -13,7 +13,7 @@ import { LocalBPList, LocalBPListsDetails } from "shared/models/playlists/local- import { PlaylistItem } from "./playlist-item.component"; import { useStateMap } from "renderer/hooks/use-state-map.hook"; import { ModalExitCode, ModalService } from "renderer/services/modale.service"; -import { LocalPlaylistDetailsModal } from "renderer/components/modal/modal-types/playlist/local-playlist-details-modal.component"; +import { LocalPlaylistDetailsModal } from "renderer/components/modal/modal-types/playlist/playlist-details-modal/local-playlist-details-modal.component"; import { InstalledMapsContext } from "../maps-playlists-panel.component"; import { IpcService } from "renderer/services/ipc.service"; import equal from "fast-deep-equal"; 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 a5add1c1..19a58fc5 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 @@ -32,6 +32,7 @@ export type PlaylistItemComponentProps = { onClickOpenFile?: () => void; onClickDelete?: () => void; onClickSync?: () => void; + onClickDownload?: () => void; onClickCancelDownload?: () => void; } @@ -50,6 +51,7 @@ export function PlaylistItem({ title, onClickOpen, onClickOpenFile, onClickSync, + onClickDownload, onClickDelete, onClickCancelDownload }: PlaylistItemComponentProps) { @@ -80,7 +82,7 @@ export function PlaylistItem({ title, return ( setHovered(() => true)} onHoverEnd={() => setHovered(() => false)} > -
+
@@ -136,6 +138,23 @@ export function PlaylistItem({ title, ) : (<>) + )} + {onClickDownload && ( + isDownloading ? ( + + ) : !isInQueue ? ( + + + + ) : (<>) + )} {onClickOpenFile && = ({ options: {data : { ownedMaps, version }} }) => { const beatSaver = useService(BeatSaverService); @@ -131,7 +132,7 @@ export const DownloadMapsModal: ModalComponent getLocalTimeZone()); const [curated, setCurated] = useState(params?.curated); const [verified, setVerified] = useState(params?.verified); const [includeEmpty, setIncludeEmpty] = useState(params?.includeEmpty); const [minNps, setMinNps] = useState(params?.minNps); const [maxNps, setMaxNps] = useState(params?.maxNps); - const [dateRange, setDateRange] = useState>({ - start: null, - end: null, + const [dateRange, setDateRange] = useState>({ + start: params?.from ? toCalendarDate(parseAbsoluteToLocal(params.from)) : null, + end: params?.from ? toCalendarDate(parseAbsoluteToLocal(params.from)) : null, }); - const filterValue: FilterPlaylistSearchParams = { curated, verified, includeEmpty, minNps, maxNps, from: dateRange.start?.toString(), to: dateRange.end?.toString() }; + const filterValue: FilterPlaylistSearchParams = { + curated, + verified, + includeEmpty, minNps, + maxNps, + from: dateRange?.start ? toZoned(dateRange.start, timeZone).set({ hour: 0, minute: 0 }).toAbsoluteString() : undefined, + to: dateRange?.end ? toZoned(dateRange.end, timeZone).set({ hour: 23, minute: 59, second: 59 }).toAbsoluteString() : undefined, + }; const firstFilterValue = useConstant(() => ({...filterValue})); const filterHasBeenChanged = !equal(firstFilterValue, filterValue); useOnUpdate(() => { onChange?.(filterValue); - }, [curated, verified, includeEmpty, minNps, maxNps, dateRange.end, dateRange.start]); + }, [curated, verified, includeEmpty, minNps, maxNps, dateRange]); const handleOnNpsChange = ([min, max]: number[]) => { setMinNps(() => min <= MIN_NPS ? undefined : min); @@ -92,11 +102,11 @@ export function DownloadPlaylistFilterPanel({ className, params, onChange, onSub return setDateRange(() => ({ start: null, end: null })); }; - const end = today(getLocalTimeZone()); + const end = today(timeZone); setDateRange(() => ({ start: end.subtract({ [type]: value }), - end: end.add({days: 1}) + end, })) }; diff --git a/src/renderer/components/modal/modal-types/playlist/download-playlist-modal/download-playlist-modal.component.tsx b/src/renderer/components/modal/modal-types/playlist/download-playlist-modal/download-playlist-modal.component.tsx index c9e83286..dc060352 100644 --- a/src/renderer/components/modal/modal-types/playlist/download-playlist-modal/download-playlist-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/playlist/download-playlist-modal/download-playlist-modal.component.tsx @@ -1,5 +1,5 @@ import { useObservable } from "renderer/hooks/use-observable.hook" -import { ModalComponent } from "renderer/services/modale.service" +import { ModalComponent, ModalService } from "renderer/services/modale.service" import { Observable, lastValueFrom } from "rxjs" import { BSVersion } from "shared/bs-version.interface" import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface" @@ -14,6 +14,11 @@ import { PlaylistItem } from "renderer/components/maps-playlists-panel/playlists import { PlaylistItemComponentPropsMapper } from "shared/mappers/playlist/playlist-item-component-props.mapper" import { motion } from "framer-motion" import { PlaylistDownloaderService } from "renderer/services/playlist-downloader.service" +import { BsvPlaylistDetailsModal } from "../playlist-details-modal/bsv-playlist-details-modal.component" +import { BsmImage } from "renderer/components/shared/bsm-image.component" +import BeatWaiting from "../../../../../../../assets/images/apngs/beat-waiting.png" +import BeatConflict from "../../../../../../../assets/images/apngs/beat-conflict.png" +import { cn } from "renderer/helpers/css-class.helpers" // TODO : Translate @@ -21,14 +26,15 @@ export const DownloadPlaylistModal: ModalComponent { + const modal = useService(ModalService); const beatSaver = useService(BeatSaverService); const playlistDownloader = useService(PlaylistDownloaderService); - const [playlists, setPlaylists] = useState([]); + const [playlists, setPlaylists] = useState(null); const ownedPlaylists = useObservable(() => ownedPlaylists$, []); const ownedMaps = useObservable(() => ownedMaps$, []); - const [loading, setLoading] = useState(false); + const [error, setError] = useState(false); const [searchParams, setSearchParams] = useState({ q: "", sortOrder: BsvSearchOrder.Relevance, @@ -36,11 +42,9 @@ export const DownloadPlaylistModal: ModalComponent { - setLoading(() => true); beatSaver.searchPlaylists(searchParams) - .then(playlists => setPlaylists(prev => [...prev, ...(playlists ?? [])])) - .catch(console.error) - .finally(() => setLoading(() => false)); + .then(playlists => setPlaylists(prev => [...(prev ?? []), ...(playlists ?? [])] )) + .catch(() => setError(() => true)); }, [searchParams]) const handleNewSearch = (value: Omit) => { @@ -52,15 +56,33 @@ export const DownloadPlaylistModal: ModalComponent ({ ...prev, page: prev.page + 1 })); }; + const openPlaylist = (playlist: BsvPlaylist) => { + modal.openModal(BsvPlaylistDetailsModal, { data: { playlist, version, installedMaps$: ownedMaps$ }, noStyle: true }) + }; + return (
{(() => { - if(loading && playlists.length === 0){ - return
Loading...
+ if(!Array.isArray(playlists)){ + return ( +
+ +

+ {error ? "Une erreur est survenue lors du chargement des playlists" : "Chargement des playlists..."} +

+
+ ) } else if(playlists.length === 0){ - return
No playlists found
+ return ( +
+ +

+ Aucune playlist trouvée +

+
+ ); } return (
    @@ -68,7 +90,8 @@ export const DownloadPlaylistModal: ModalComponent lastValueFrom(playlistDownloader.downloadPlaylist({ downloadSource: playlist.downloadURL, ignoreSongsHashs: ownedMaps.map(map => map.hash), version }))} + onClickOpen={() => openPlaylist(playlist)} + onClickDownload={() => lastValueFrom(playlistDownloader.downloadPlaylist({ downloadSource: playlist.downloadURL, ignoreSongsHashs: ownedMaps.map(map => map.hash), version }))} /> ))} 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 new file mode 100644 index 00000000..a217caf4 --- /dev/null +++ b/src/renderer/components/modal/modal-types/playlist/playlist-details-modal/bsv-playlist-details-modal.component.tsx @@ -0,0 +1,123 @@ +import { BsvMapDetail, BsvPlaylist } from "shared/models/maps/beat-saver.model"; +import { PlaylistDetailsTemplate } from "./playlist-details-template.component"; +import { BSVersion } from "shared/bs-version.interface"; +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 { MapItem } from "renderer/components/maps-playlists-panel/maps/map-item.component"; +import { extractMapDiffs } from "renderer/components/maps-playlists-panel/maps/maps-row.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"; + +type Props = { + version: BSVersion; + playlist: BsvPlaylist; + installedMaps$: Observable; +} + +// TODO : Translate + +export const BsvPlaylistDetailsModal: ModalComponent = ({ resolver, options: { data: { playlist, installedMaps$, version } } }) => { + + const beatsaver = useService(BeatSaverService); + const mapsDownloader = useService(MapsDownloaderService); + + const downloadingMaps = useObservable(() => mapsDownloader.mapsInQueue$, []); + const installedMaps = useObservable(() => installedMaps$, null); + const [page, setPage] = useState(0); + const [playlistMaps, setPlaylistMaps] = useState(null); + const [error, setError] = useState(false); + + useOnUpdate(() => { + beatsaver.getPlaylistDetailsById(`${playlist.playlistId}`, page) + .then(playlistDetails => setPlaylistMaps(prev => [...(prev ?? []), ...(playlistDetails?.maps?.map(map => map.map) ?? [])])) + .catch(() => setError(() => true)); + }, [page]); + + return ( + resolver({ exitCode: ModalExitCode.CLOSED })} + > + {(() => { + + if(!Array.isArray(playlistMaps)) { + return ( +
    + +

    + {error ? "Une erreur est survenue lors du chargement de la playlist" : "Chargement des maps..."} +

    +
    + ); + } + + if(playlistMaps.length === 0) { + return ( +
    + +

    + Aucune map trouvée pour cette playlist +

    +
    + ); + } + + return ( +
      + {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; + + return ( + mapsDownloader.addMapToDownload({version, map}))} + onCancelDownload={isMapInQueue && !isMapDownloading && (() => mapsDownloader.removeMapToDownload({version, map}))} + callBackParam={map} + />); + })} + setPage(prev => prev + 1)} /> +
    + ) + })()} +
    + ) +} diff --git a/src/renderer/components/modal/modal-types/playlist/local-playlist-details-modal.component.tsx b/src/renderer/components/modal/modal-types/playlist/playlist-details-modal/local-playlist-details-modal.component.tsx similarity index 90% rename from src/renderer/components/modal/modal-types/playlist/local-playlist-details-modal.component.tsx rename to src/renderer/components/modal/modal-types/playlist/playlist-details-modal/local-playlist-details-modal.component.tsx index b1928565..6f3905fc 100644 --- a/src/renderer/components/modal/modal-types/playlist/local-playlist-details-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/playlist/playlist-details-modal/local-playlist-details-modal.component.tsx @@ -1,6 +1,6 @@ import { ModalComponent } from "renderer/services/modale.service" -import { PlaylistDetailsTemplate, PlaylistDetailsTemplateProps } from "./playlist-details-template.component" -import { Observable, combineLatest, first, lastValueFrom, map, mergeAll, mergeMap, shareReplay, switchMap, take, tap } from "rxjs" +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" import { BSVersion } from "shared/bs-version.interface"; import { useObservable } from "renderer/hooks/use-observable.hook"; @@ -8,10 +8,9 @@ import { MapItem } from "renderer/components/maps-playlists-panel/maps/map-item. import { extractMapDiffs } from "renderer/components/maps-playlists-panel/maps/maps-row.component"; import { useService } from "renderer/hooks/use-service.hook"; import { AudioPlayerService } from "renderer/services/audio-player.service"; -import { AnimatePresence, motion } from "framer-motion"; import { BsmImage } from "renderer/components/shared/bsm-image.component"; import { BsmButton } from "renderer/components/shared/bsm-button.component"; -import BeatConflict from "../../../../../../assets/images/apngs/beat-conflict.png"; +import BeatConflict from "../../../../../../../assets/images/apngs/beat-conflict.png"; import { LocalBPListsDetails } from "shared/models/playlists/local-playlist.models"; import { PlaylistDownloaderService } from "renderer/services/playlist-downloader.service"; import { PlaylistHeaderState } from "./playlist-header-state.component"; @@ -34,8 +33,8 @@ export const LocalPlaylistDetailsModal: ModalComponent = ({resolver const installedMaps = useObservable(() => options.data.installedMaps$, null); const isMissingMaps$ = useConstant(() => combineLatest([options.data.installedMaps$, options.data.localPlaylist$]).pipe(map(([maps, playlist]) => maps.length !== playlist.songs.length))); - const isPlaylistDownloading$ = useConstant(() => options.data.localPlaylist$.pipe(switchMap(playlist => playlistDownloader.$isPlaylistDownloading(playlist, options.data.version)))); - const isPlaylistInQueue$ = useConstant(() => options.data.localPlaylist$.pipe(switchMap(playlist => playlistDownloader.$isPlaylistInQueue(playlist, options.data.version)))); + const isPlaylistDownloading$ = useConstant(() => options.data.localPlaylist$.pipe(switchMap(playlist => playlistDownloader.$isPlaylistDownloading(playlist.path, options.data.version)))); + const isPlaylistInQueue$ = useConstant(() => options.data.localPlaylist$.pipe(switchMap(playlist => playlistDownloader.$isPlaylistInQueue(playlist.path, options.data.version)))); const isInQueue = useObservable(() => isPlaylistInQueue$, false); @@ -50,7 +49,12 @@ export const LocalPlaylistDetailsModal: ModalComponent = ({resolver const ignoreSongsHashs = installedMaps?.map(map => map.hash); - const obs$ = playlistDownloader.installPlaylist(localPlaylist, options.data.version, ignoreSongsHashs); + const obs$ = playlistDownloader.downloadPlaylist({ + downloadSource: localPlaylist.path, + dest: localPlaylist.path, + version: options.data.version, + ignoreSongsHashs, + }); return lastValueFrom(obs$); } @@ -125,7 +129,7 @@ export const LocalPlaylistDetailsModal: ModalComponent = ({resolver void; } -export function PlaylistDetailsTemplate({title, image, author, description, nbMaps, duration, nbMappers, minNps, maxNps, children}: PlaylistDetailsTemplateProps) { +export function PlaylistDetailsTemplate({title, imagebase64, imageUrl, author, description, nbMaps, duration, nbMappers, minNps, maxNps, children, onClose}: PlaylistDetailsTemplateProps) { const color = useThemeColor("first-color"); @@ -43,11 +47,14 @@ export function PlaylistDetailsTemplate({title, image, author, description, nbMa return (
    - - + + +

    {title}

    -

    {description}

    +

    {description}

    Créé par {author}

    diff --git a/src/renderer/components/modal/modal-types/playlist/playlist-header-state.component.tsx b/src/renderer/components/modal/modal-types/playlist/playlist-details-modal/playlist-header-state.component.tsx similarity index 97% rename from src/renderer/components/modal/modal-types/playlist/playlist-header-state.component.tsx rename to src/renderer/components/modal/modal-types/playlist/playlist-details-modal/playlist-header-state.component.tsx index 2c82f295..939994fe 100644 --- a/src/renderer/components/modal/modal-types/playlist/playlist-header-state.component.tsx +++ b/src/renderer/components/modal/modal-types/playlist/playlist-details-modal/playlist-header-state.component.tsx @@ -1,7 +1,7 @@ import { AnimatePresence, motion } from "framer-motion"; import { BsmButton } from "renderer/components/shared/bsm-button.component"; import { BsmImage } from "renderer/components/shared/bsm-image.component"; -import BeatConflict from "../../../../../../assets/images/apngs/beat-conflict.png"; +import BeatConflict from "../../../../../../../assets/images/apngs/beat-conflict.png"; import { Observable } from "rxjs"; import { useObservable } from "renderer/hooks/use-observable.hook"; diff --git a/src/renderer/components/modal/modal.component.tsx b/src/renderer/components/modal/modal.component.tsx index 37e918e5..7734351b 100644 --- a/src/renderer/components/modal/modal.component.tsx +++ b/src/renderer/components/modal/modal.component.tsx @@ -1,7 +1,6 @@ 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"; @@ -12,8 +11,6 @@ export function Modal() { const modals = useObservable(() => modalSevice.getModalToShow()); const currentModal = modals?.at(-1); - const { firstColor, secondColor } = useThemeColor(); - useEffect(() => { const onEscape = (e: KeyboardEvent) => { if (e.key !== "Escape") { @@ -61,7 +58,7 @@ export function Modal() { {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/bsm-link.component.tsx b/src/renderer/components/shared/bsm-link.component.tsx index bad002be..3d502432 100644 --- a/src/renderer/components/shared/bsm-link.component.tsx +++ b/src/renderer/components/shared/bsm-link.component.tsx @@ -10,10 +10,13 @@ type Props = { }; export function BsmLink({ className, href, children, style, internal }: Props) { - + const linkOpener = useService(LinkOpenerService); const openLink = () => { + + console.log("Opening link", href, internal); + if (!href) { return; } diff --git a/src/renderer/services/thrird-partys/beat-saver.service.ts b/src/renderer/services/thrird-partys/beat-saver.service.ts index 0c9c0249..1be25d4c 100644 --- a/src/renderer/services/thrird-partys/beat-saver.service.ts +++ b/src/renderer/services/thrird-partys/beat-saver.service.ts @@ -1,5 +1,5 @@ import { BsvMapDetail } from "shared/models/maps"; -import { BsvPlaylist, PlaylistSearchParams, SearchParams } from "shared/models/maps/beat-saver.model"; +import { BsvPlaylist, BsvPlaylistPage, PlaylistSearchParams, SearchParams } from "shared/models/maps/beat-saver.model"; import { IpcService } from "../ipc.service"; import { lastValueFrom } from "rxjs"; @@ -33,4 +33,8 @@ export class BeatSaverService { public async searchPlaylists(search: PlaylistSearchParams): Promise { return lastValueFrom(this.ipc.sendV2("bsv-search-playlist", search)); } + + public async getPlaylistDetailsById(id: string, page = 0): Promise { + return lastValueFrom(this.ipc.sendV2("bsv-get-playlist-details-by-id", { id, page })); + } } diff --git a/src/shared/models/ipc/ipc-routes.ts b/src/shared/models/ipc/ipc-routes.ts index 0ef06fc3..cccc5ba7 100644 --- a/src/shared/models/ipc/ipc-routes.ts +++ b/src/shared/models/ipc/ipc-routes.ts @@ -4,7 +4,7 @@ import { BSVersion } from "shared/bs-version.interface"; import { BSLaunchEventData, LaunchOption } from "shared/models/bs-launch"; import { BsvMapDetail } from "shared/models/maps"; import { BsmLocalMap, BsmLocalMapsProgress, DeleteMapsProgress } from "shared/models/maps/bsm-local-map.interface"; -import { BsvPlaylist, PlaylistSearchParams, SearchParams } from "../maps/beat-saver.model"; +import { BsvPlaylist, BsvPlaylistPage, PlaylistSearchParams, SearchParams } from "../maps/beat-saver.model"; import { ImportVersionOptions } from "main/services/bs-local-version.service"; import { DownloadInfo, DownloadSteamInfo } from "main/services/bs-version-download/bs-steam-downloader.service"; import { DepotDownloaderEvent } from "../bs-version-download/depot-downloader.model"; @@ -42,6 +42,7 @@ export interface IpcChannelMapping { "bsv-get-map-details-from-hashs": {request: string[], response: BsvMapDetail[]}; "bsv-get-map-details-by-id": {request: string, response: BsvMapDetail}; "bsv-search-playlist": {request: PlaylistSearchParams, response: BsvPlaylist[]}; + "bsv-get-playlist-details-by-id": {request: {id: string, page: number}, response: BsvPlaylistPage}; /* ** bs-launcher-ipcs ** */ "create-launch-shortcut": { request: LaunchOption, response: boolean }; diff --git a/src/shared/models/maps/song-details-cache/raw-song-details-cache.model.ts b/src/shared/models/maps/song-details-cache/raw-song-details-cache.model.ts index 92c821de..2748fb88 100644 --- a/src/shared/models/maps/song-details-cache/raw-song-details-cache.model.ts +++ b/src/shared/models/maps/song-details-cache/raw-song-details-cache.model.ts @@ -3,15 +3,22 @@ export interface RawSongDetailsCache { songs: RawSongDetails[]; lastUpdated: number; + uploaders: UploadersList; + difficultyLabels: string[]; total: number; } +export interface UploadersList { + names: string[]; + ids: number[]; +} + // SongDetails Message export interface RawSongDetails { idInt: number; hash: string; duration: number; - uploader: RawUploader; + uploaderRef: UploaderRef; uploadedAt: number; tags: RawMapTag[]; ranked: boolean; @@ -31,7 +38,7 @@ export interface RawSongDetails { export interface RawDifficulty { difficulty: RawDifficultyLabel; characteristic: RawDifficultyCharacteristic; - label: string; + labelIndex: number; starsT100: number; starsBlT100: number; njsT100: number; @@ -47,9 +54,8 @@ export interface RawDifficulty { } // Uploader Message -export interface RawUploader { - name: string; - id: number; +export interface UploaderRef { + uploaderRefIndex: number; verified: boolean; } diff --git a/src/shared/models/maps/song-details-cache/raw-song-details-deserializer.class.ts b/src/shared/models/maps/song-details-cache/raw-song-details-deserializer.class.ts index dc64ab0c..5bd1263e 100644 --- a/src/shared/models/maps/song-details-cache/raw-song-details-deserializer.class.ts +++ b/src/shared/models/maps/song-details-cache/raw-song-details-deserializer.class.ts @@ -1,11 +1,22 @@ import { MapStyle, MapTag, MapType } from "../beat-saver.model"; -import { RawDifficulty, RawDifficultyCharacteristic, RawDifficultyLabel, RawMapTag, RawSongDetails, RawUploader } from "./raw-song-details-cache.model"; +import { RawDifficulty, RawDifficultyCharacteristic, RawDifficultyLabel, RawMapTag, RawSongDetails, UploaderRef, UploadersList } from "./raw-song-details-cache.model"; import { SongDetailDiffCharactertistic, SongDetails, SongDiffName, SongDifficulty, SongUploader } from "./song-details-cache.model"; export abstract class RawSongDetailsDeserializer { + public static uploaderList: UploadersList = { names: [], ids: [] }; + public static difficultyLabels: string[] = []; + private constructor() {} + public static setUploadersList(uploadersList: UploadersList): void { + this.uploaderList = uploadersList; + } + + public static setDifficultyLabels(difficultyLabels: string[]): void { + this.difficultyLabels = difficultyLabels; + } + private static deserializeRawTag(rawTag: RawMapTag): MapTag { switch (rawTag) { case RawMapTag.DANCE: return MapStyle.Dance; @@ -88,7 +99,7 @@ export abstract class RawSongDetailsDeserializer { return { difficulty: this.deserializeRawDifficultyLabel(rawDifficulty.difficulty), characteristic: this.deserializeRawDifficultyCharacteristic(rawDifficulty.characteristic), - label: rawDifficulty.label, + label: RawSongDetailsDeserializer.difficultyLabels[rawDifficulty.labelIndex], stars: Math.round(rawDifficulty.starsT100) / 100, starsBL: Math.round(rawDifficulty.starsBlT100) / 100, njs: Math.round(rawDifficulty.njsT100) / 100, @@ -108,11 +119,12 @@ export abstract class RawSongDetailsDeserializer { return rawDifficulties.map(rawDifficulty => this.deserializeRawDifficulty(rawDifficulty)); } - private static deserializeRawUploader(rawUploader: RawUploader): SongUploader { + private static deserializeRawUploader(uploaderRef: UploaderRef): SongUploader { + return { - name: rawUploader.name, - id: rawUploader.id, - verified: rawUploader.verified + name: RawSongDetailsDeserializer.uploaderList.names[uploaderRef.uploaderRefIndex], + id: RawSongDetailsDeserializer.uploaderList.ids[uploaderRef.uploaderRefIndex], + verified: uploaderRef.verified }; } @@ -138,7 +150,7 @@ export abstract class RawSongDetailsDeserializer { id: this.deserializeMapId(rawSongDetails.idInt), hash: rawSongDetails.hash, duration: rawSongDetails.duration, - uploader: this.deserializeRawUploader(rawSongDetails.uploader), + uploader: this.deserializeRawUploader(rawSongDetails.uploaderRef), uploadedAt: rawSongDetails.uploadedAt, tags: this.deserializeRawTags(rawSongDetails.tags), ranked: rawSongDetails.ranked,