From 3427cae4370fcf6ddf3719aab12c9290261cf4ae Mon Sep 17 00:00:00 2001 From: MathieuG-P <40181755+Zagrios@users.noreply.github.com> Date: Sat, 22 Jun 2024 19:40:49 +0200 Subject: [PATCH] [feature-107] add diffictulties hightlight for playlist maps --- src/main/ipcs/bs-playlist-ipcs.ts | 4 - .../local-playlists-manager.service.ts | 14 +- .../maps/song-details-cache.service.ts | 22 ++- .../maps/local-maps-list-panel.component.tsx | 5 +- .../maps/map-item.component.tsx | 95 +++++----- .../local-playlists-list-panel.component.tsx | 36 +++- .../playlists/playlist-item.component.tsx | 12 +- .../download-maps-modal.component.tsx | 6 +- .../edit-playlist-modal.component.tsx | 166 ++++++++++++------ .../bsv-playlist-details-modal.component.tsx | 38 +--- ...local-playlist-details-modal.component.tsx | 5 +- .../components/shared/bsm-image.component.tsx | 7 +- .../title-bar/title-bar.component.tsx | 4 +- .../map/map-item-component-props.mapper.ts | 29 +-- .../models/playlists/playlist.interface.ts | 15 +- 15 files changed, 277 insertions(+), 181 deletions(-) diff --git a/src/main/ipcs/bs-playlist-ipcs.ts b/src/main/ipcs/bs-playlist-ipcs.ts index f1edd62c..9ac81c89 100644 --- a/src/main/ipcs/bs-playlist-ipcs.ts +++ b/src/main/ipcs/bs-playlist-ipcs.ts @@ -1,12 +1,8 @@ -import { LocalBPList } from "shared/models/playlists/local-playlist.models"; import { LocalPlaylistsManagerService } from "../services/additional-content/local-playlists-manager.service"; import { IpcService } from "../services/ipc.service"; import { from, lastValueFrom, mergeMap, of } from "rxjs"; -import { BPList } from "shared/models/playlists/playlist.interface"; -import path from "path"; import { LocalMapsManagerService } from "../services/additional-content/maps/local-maps-manager.service"; import { Progression } from "main/helpers/fs.helpers"; -import { lstatSync } from "fs-extra"; import { isValidUrl } from "shared/helpers/url.helpers"; import { pathToFileURL } from "url"; 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 48af8e08..56e8a39b 100644 --- a/src/main/services/additional-content/local-playlists-manager.service.ts +++ b/src/main/services/additional-content/local-playlists-manager.service.ts @@ -134,7 +134,7 @@ export class LocalPlaylistsManagerService { throw new Error(`Invalid source ${source}`); } - const bpList = isLocalFile ? JSON.parse(readFileSync(source).toString()) : await this.request.getJSON(source); + const bpList: BPList = isLocalFile ? JSON.parse(readFileSync(source).toString()) : await this.request.getJSON(source); return bpList; } @@ -189,7 +189,17 @@ export class LocalPlaylistsManagerService { id: localBPList.customData?.syncURL ? tryExtractPlaylistId(localBPList.customData.syncURL) : undefined } - const songsDetails = localBPList.songs?.map(s => this.songDetails.getSongDetails(s.hash)).filter(Boolean); + const songsDetails = localBPList.songs?.map(s => { + if(!s){ + return undefined; + } + if(s.hash){ + return this.songDetails.getSongDetails(s.hash); + } + if(s.key){ + return this.songDetails.getSongDetailsById(s.key); + } + }).filter(Boolean); if(songsDetails && songsDetails.length){ bpListDetails.duration = songsDetails.reduce((acc, song) => acc + song.duration, 0); 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 bd04bd4d..e3c9ad7a 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 @@ -37,6 +37,7 @@ export class SongDetailsCacheService { private readonly utils: UtilsService; private songDetailsCache: Record = {}; + private songDetailsIdIndex: Record = {}; private readonly _loaded$ = new BehaviorSubject(null); private constructor(){ @@ -52,6 +53,9 @@ export class SongDetailsCacheService { await this.downloadCacheFile(etag).then(etag => { this.config.set(this.etagKey, etag); + log.info("SongDetailsCache downloaded"); + this.songDetailsIdIndex = this.createIdIndex(this.songDetailsCache); + log.info("SongDetailsIdIndex created"); }).catch(err => { log.error("Unable to download cache file", err); }); @@ -66,6 +70,14 @@ export class SongDetailsCacheService { }) } + private createIdIndex(songDetailsCache: Record): Record { + const res: Record = {}; + for(const hash in songDetailsCache){ + res[songDetailsCache[hash].id] = songDetailsCache[hash]; + } + return res; + } + private async readProtoMessageCacheFile(filePath: string): Promise> { const protobufRoot = protobuf.loadSync(this.getProtoShemaPath()); const cacheMessage = protobufRoot.lookupType("SongDetailsCache"); @@ -82,7 +94,7 @@ export class SongDetailsCacheService { for(const rawSong of messageObj.songs){ const deserialized = RawSongDetailsDeserializer.deserialize(rawSong); - res[deserialized.hash.toLocaleLowerCase()] = deserialized; + res[deserialized.hash.toLowerCase()] = deserialized; } return res; @@ -166,8 +178,12 @@ export class SongDetailsCacheService { })))); } - public getSongDetails(hash: string): SongDetails { - return this.songDetailsCache[hash.toLocaleLowerCase()]; + public getSongDetails(hash: string): SongDetails | undefined { + return this.songDetailsCache[hash.toLowerCase()]; + } + + public getSongDetailsById(id: string): SongDetails | undefined { + return this.songDetailsIdIndex[id.toLowerCase()]; } } 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 ce2dbc53..9a530cad 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 @@ -20,8 +20,9 @@ import { InstalledMapsContext } from "../maps-playlists-panel.component"; import { useObservable } from "renderer/hooks/use-observable.hook"; import equal from "fast-deep-equal"; import { VirtualScroll } from "renderer/components/shared/virtual-scroll/virtual-scroll.component"; -import { MapItem, extractMapDiffs } from "./map-item.component"; +import { MapItem } from "./map-item.component"; import { isLocalMapFitMapFilter } from "./filter-panel.component"; +import { MapItemComponentPropsMapper } from "shared/mappers/map/map-item-component-props.mapper"; type Props = { version: BSVersion; @@ -165,7 +166,7 @@ export const LocalMapsListPanel = forwardRef(({ version, classNa bpm={map.rawInfo._beatsPerMinute} duration={map.songDetails?.duration} selected={renderableMap.selected} - diffs={extractMapDiffs({ rawMapInfo: map.rawInfo, songDetails: map.songDetails })} + diffs={MapItemComponentPropsMapper.extractMapDiffs({ rawMapInfo: map.rawInfo, songDetails: map.songDetails })} mapId={map.songDetails?.id} ranked={map.songDetails?.ranked} autorId={map.songDetails?.uploader.id} 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 135660bd..91512122 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 @@ -4,7 +4,7 @@ import { BsmLink } from "../../shared/bsm-link.component"; import { BsmIcon } from "../../svgs/bsm-icon.component"; import { BsmButton } from "../../shared/bsm-button.component"; import { AnimatePresence, motion } from "framer-motion"; -import { useState, Fragment, memo, useRef } from "react"; +import { useState, Fragment, useRef } from "react"; import { LinkOpenerService } from "renderer/services/link-opener.service"; import dateFormat from "dateformat"; import { AudioPlayerService } from "renderer/services/audio-player.service"; @@ -20,13 +20,15 @@ import { GlowEffect } from "../../shared/glow-effect.component"; import { useDelayedState } from "renderer/hooks/use-delayed-state.hook"; import { useService } from "renderer/hooks/use-service.hook"; import Tippy from "@tippyjs/react"; -import { BsvMapDetail, RawMapInfoData, SongDetailDiffCharactertistic, SongDetails, SongDiffName } from "shared/models/maps"; +import { SongDetailDiffCharactertistic, SongDiffName } from "shared/models/maps"; import { useConstant } from "renderer/hooks/use-constant.hook"; import { CalendarDateTime, getLocalTimeZone } from "@internationalized/date"; import { typedMemo } from "renderer/helpers/typed-memo"; import { Observable, of } from "rxjs"; - -export type ParsedMapDiff = { type: SongDiffName; name: string; stars: number }; +import { ParsedMapDiff } from "shared/mappers/map/map-item-component-props.mapper"; +import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component"; +import { BPListDifficulty } from "shared/models/playlists/playlist.interface"; +import { useOnUpdate } from "renderer/hooks/use-on-update.hook"; export type MapItemComponentProps = { hash: string; @@ -38,6 +40,7 @@ export type MapItemComponentProps = { autorId: number; mapId: string; diffs: Map; + diffsSelected?: BPListDifficulty[]; ranked?: boolean; blRanked?: boolean; bpm?: number; @@ -55,9 +58,10 @@ export type MapItemComponentProps = { onSelected?: (param: T) => void; onCancelDownload?: (param: T) => void; onDoubleClick?: (param: T) => void; + onSelectedDiffsChange?: (diffs: BPListDifficulty[]) => void; }; -export function MapItemComponent ({ hash, title, autor, songAutor, coverUrl, songUrl, autorId, mapId, diffs, ranked, blRanked, bpm, duration, likes, createdAt, selected, selected$, downloading, showOwned, isOwned$, callBackParam, onDelete, onDownload, onSelected, onCancelDownload, onDoubleClick }: MapItemComponentProps) { +export function MapItemComponent ({ hash, title, autor, songAutor, coverUrl, songUrl, autorId, mapId, diffs, diffsSelected, ranked, blRanked, bpm, duration, likes, createdAt, selected, selected$, downloading, showOwned, isOwned$, callBackParam, onDelete, onDownload, onSelected, onCancelDownload, onDoubleClick, onSelectedDiffsChange }: MapItemComponentProps) { const linkOpener = useService(LinkOpenerService); const audioPlayer = useService(AudioPlayerService); @@ -70,6 +74,7 @@ export function MapItemComponent ({ hash, title, autor, songAutor, const [hovered, setHovered] = useState(false); const [bottomBarHovered, setBottomBarHovered, cancelBottomBarHovered] = useDelayedState(false); const [diffsPanelHovered, setDiffsPanelHovered] = useState(false); + const [_diffsSelected, setDiffsSelected] = useState(diffsSelected ?? []); useDoubleClick({ ref, @@ -78,6 +83,11 @@ export function MapItemComponent ({ hash, title, autor, songAutor, onDoubleClick: () => onDoubleClick?.(callBackParam), }); + useOnUpdate(() => { + if(!_diffsSelected?.length){ return; } + onSelectedDiffsChange?.(_diffsSelected); + }, [_diffsSelected]) + const songPlaying = useObservable(() => audioPlayer.playing$.pipe(map(playing => playing && audioPlayer.src === songUrl))); const MAP_DIFFICULTIES = useConstant(() => Object.values(SongDiffName)) @@ -85,6 +95,8 @@ export function MapItemComponent ({ hash, title, autor, songAutor, const mapUrl = mapId ? `https://beatsaver.com/maps/${mapId}` : null; const authorUrl = autorId ? `https://beatsaver.com/profile/${autorId}` : null; const likesText = likes ? Intl.NumberFormat(undefined, { notation: "compact" }).format(likes).split(" ").join("") : null; + const mapCoverUrl = coverUrl ? coverUrl : `https://eu.cdn.beatsaver.com/${hash}.jpg`; + const createdDate = useConstant(() => { if(!createdAt){ return null; } const date = typeof createdAt === "number" ? new Date(createdAt * 1000) : createdAt.toDate(getLocalTimeZone()); @@ -134,6 +146,11 @@ export function MapItemComponent ({ hash, title, autor, songAutor, setBottomBarHovered(false, 100); }; + const isDiffChecked = (diff: {name: string, characteristic: string}) => { + if(!_diffsSelected?.length){ return false; } + return _diffsSelected.some(d => d?.name?.toLowerCase() === diff?.name?.toLowerCase() && d?.characteristic?.toLowerCase() === diff?.characteristic?.toLowerCase()); + } + const diffsPanelHoverStart = () => setDiffsPanelHovered(true); const diffsPanelHoverEnd = () => setDiffsPanelHovered(false); @@ -148,7 +165,7 @@ export function MapItemComponent ({ hash, title, autor, songAutor,
{diffSet.map((diff, index) => ( - + ))}
@@ -166,6 +183,19 @@ export function MapItemComponent ({ hash, title, autor, songAutor, return null; }; + const handleDiffCheckChange = (diff: BPListDifficulty) => { + if(!_diffsSelected?.length){ + setDiffsSelected([diff]); + } + + const index = _diffsSelected?.findIndex(d => d?.name?.toLowerCase() === diff?.name?.toLowerCase() && d?.characteristic?.toLowerCase() === diff?.characteristic?.toLowerCase()); + if(index === -1){ + setDiffsSelected([..._diffsSelected, diff]); + } else { + setDiffsSelected(_diffsSelected.filter((_, i) => i !== index)); + } + } + return ( setHovered(true)} onHoverEnd={() => setHovered(false)} style={{ zIndex: hovered && 5, transform: "translateZ(0) scale(1.0, 1.0)", backfaceVisibility: "hidden" }}> @@ -174,13 +204,16 @@ export function MapItemComponent ({ hash, title, autor, songAutor, {Array.from(diffs.entries()).map(([charac, diffSet], index) => (
    - {diffSet.map(({ type, name, stars }, index) => ( -
  1. - - - {stars ? ★ {stars.toFixed(2)} : {parseDiffLabel(type)}} + {diffSet.map(({ name, libelle, stars }, index) => ( +
  2. + {onSelectedDiffsChange && ( + handleDiffCheckChange({name, characteristic: charac})} /> + )} + + + {stars ? ★ {stars.toFixed(2)} : {parseDiffLabel(name)}} - {parseDiffLabel(name)} + {parseDiffLabel(libelle)}
  3. ))}
@@ -190,7 +223,7 @@ export function MapItemComponent ({ hash, title, autor, songAutor,
- + ({ hash, title, autor, songAutor,
- +

@@ -351,36 +384,4 @@ export function MapItemComponent ({ hash, title, autor, songAutor, ); }; -export const MapItem = typedMemo(MapItemComponent, 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; -} +export const MapItem = typedMemo(MapItemComponent, equal); 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 54a3ad0b..8c02705d 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 @@ -6,7 +6,7 @@ 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, combineLatest, distinctUntilChanged, filter, finalize, lastValueFrom, map, tap } from "rxjs"; +import { BehaviorSubject, Observable, combineLatest, distinctUntilChanged, filter, finalize, lastValueFrom, map, tap } from "rxjs"; import { BSVersion } from "shared/bs-version.interface"; import { noop } from "shared/helpers/function.helpers"; import { LocalBPList, LocalBPListsDetails } from "shared/models/playlists/local-playlist.models"; @@ -39,6 +39,7 @@ import { SyncPlaylistModal } from "renderer/components/modal/modal-types/playlis import { ExportPlaylistModal } from "renderer/components/modal/modal-types/playlist/export-playlist-modal.component"; import { EditPlaylistModal } from "renderer/components/modal/modal-types/playlist/edit-playlist-modal/edit-playlist-modal.component"; import { NeedCloneEditPlaylistModal } from "renderer/components/modal/modal-types/playlist/need-clone-edit-playlist-modal.component"; +import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"; type Props = { version: BSVersion; @@ -83,7 +84,7 @@ export const LocalPlaylistsListPanel = forwardRef( const linked = useStateMap(linkedState, (newState, precMapped) => (newState === FolderLinkState.Pending || newState === FolderLinkState.Processing) ? precMapped : newState === FolderLinkState.Linked, false); const installPlaylist = (playlist: LocalBPList) => { - const ignoreSongsHashs = (maps$.value || []).map(m => m.hash.toLocaleLowerCase()); + const ignoreSongsHashs = (maps$.value || []).map(m => m.hash.toLowerCase()); return playlistDownloader.downloadPlaylist({ downloadSource: playlist.customData?.syncURL ?? playlist.path, version, ignoreSongsHashs, dest: playlist.path }); } @@ -147,7 +148,7 @@ export const LocalPlaylistsListPanel = forwardRef( if(!folderRes || folderRes.canceled || !folderRes.filePaths?.length){ return; } const mapsToExport = exportMaps ? ( - maps$?.value?.filter(m => toExport.some(p => p.songs.some(s => s.hash.toLocaleLowerCase() === m.hash.toLocaleLowerCase()))) ?? [] + maps$?.value?.filter(m => toExport.some(p => p.songs.some(s => s.hash.toLowerCase() === m.hash.toLowerCase()))) ?? [] ) : []; const obs$ = playlistService.exportPlaylists({ version, bpLists: toExport, dest: folderRes.filePaths.at(0), playlistsMaps: mapsToExport }); @@ -263,7 +264,7 @@ export const LocalPlaylistsListPanel = forwardRef( setPlaylists(playlists$.value.filter(p => p.path !== bpList.path)); if(deleteMaps){ - setMaps(maps$.value.filter(m => !bpList.songs.some(s => s.hash.toLocaleLowerCase() === m.hash.toLocaleLowerCase()))); + setMaps(maps$.value.filter(m => !bpList.songs.some(s => s.hash.toLowerCase() === m.hash.toLowerCase()))); } } @@ -273,9 +274,28 @@ export const LocalPlaylistsListPanel = forwardRef( const openPlaylistDetails = (playlistPath: string) => { const localPlaylist$ = playlists$.pipe(map(playlists => playlists.find(p => p.path === playlistPath))); - const installedMaps$ = combineLatest([maps$, localPlaylist$]).pipe( + const installedMaps$: Observable = combineLatest([maps$, localPlaylist$]).pipe( filter(([maps, playlist]) => !!maps && !!playlist), - map(([maps, playlist]) => maps.filter(m => playlist.songs.some(song => song.hash.toLocaleLowerCase() === m.hash.toLocaleLowerCase()))), + map(([maps, playlist]) => { + return playlist.songs.reduce((acc, playlistSong) => { + if(!playlistSong){ return acc; } + + let map: BsmLocalMap; + + if(playlistSong.hash){ + map = maps.find(m => m.hash.toLowerCase() === playlistSong.hash.toLowerCase()); + } + else if(playlistSong.key){ + map = maps.find(m => m?.songDetails?.id === playlistSong.key); + } + + if(map){ + acc.push(map); + } + + return acc; + }, []); + }), distinctUntilChanged(equal) ); @@ -359,8 +379,8 @@ export const LocalPlaylistsListPanel = forwardRef( if(!playlists){ return []; } return playlists.filter(p => { - if(!p.playlistTitle.toLocaleLowerCase().includes(search.toLocaleLowerCase())){ return false; } - if(!p.playlistAuthor.toLocaleLowerCase().includes(search.toLocaleLowerCase())){ return false; } + if(!p.playlistTitle.toLowerCase().includes(search.toLowerCase())){ return false; } + if(!p.playlistAuthor.toLowerCase().includes(search.toLowerCase())){ return false; } if(typeof p.nbMaps === "number" && (typeof playlistFiler?.minNbMaps === "number" || typeof playlistFiler?.maxNbMaps === "number")){ if(playlistFiler?.minNbMaps && p.nbMaps < playlistFiler.minNbMaps){ return false; } 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 8ff7e826..8d26ca0f 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 @@ -7,13 +7,15 @@ import { useThemeColor } from 'renderer/hooks/use-theme-color.hook'; 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 { memo, useState } from 'react'; import { SearchIcon } from 'renderer/components/svgs/icons/search-icon.component'; import { BsmButton } from 'renderer/components/shared/bsm-button.component'; import Tippy from '@tippyjs/react'; import { Observable, of } from 'rxjs'; import { useObservable } from 'renderer/hooks/use-observable.hook'; import { BsmBasicSpinner } from 'renderer/components/shared/bsm-basic-spinner/bsm-basic-spinner.component'; +import defaultImage from "../../../../../assets/images/default-version-img.jpg"; +import equal from 'fast-deep-equal'; export type PlaylistItemComponentProps = { title?: string; @@ -38,7 +40,7 @@ export type PlaylistItemComponentProps = { onClickEdit?: () => void; } -export function PlaylistItem({ title, +export const PlaylistItem = memo(({ title, author, coverUrl, coverBase64, @@ -58,7 +60,7 @@ export function PlaylistItem({ title, onClickDelete, onClickCancelDownload, onClickEdit -}: PlaylistItemComponentProps) { +}: PlaylistItemComponentProps) => { const color = useThemeColor("first-color"); @@ -91,7 +93,7 @@ export function PlaylistItem({ title,
- + {e.stopPropagation(); onClickOpen?.()}} @@ -196,4 +198,4 @@ export function PlaylistItem({ title, ) -} +}, equal); 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 01e0199e..21184b36 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 @@ -1,7 +1,6 @@ -import { motion } from "framer-motion"; import { useCallback, useEffect, useRef, useState } from "react"; import { FilterPanel } from "renderer/components/maps-playlists-panel/maps/filter-panel.component"; -import { MapItem, extractMapDiffs } from "renderer/components/maps-playlists-panel/maps/map-item.component"; +import { MapItem } from "renderer/components/maps-playlists-panel/maps/map-item.component"; import { BsmButton } from "renderer/components/shared/bsm-button.component"; import { BsmDropdownButton } from "renderer/components/shared/bsm-dropdown-button.component"; import { BsmSelect, BsmSelectOption } from "renderer/components/shared/bsm-select.component"; @@ -22,6 +21,7 @@ import { useService } from "renderer/hooks/use-service.hook"; import { useConstant } from "renderer/hooks/use-constant.hook"; import { getLocalTimeZone, parseAbsolute, toCalendarDateTime } from "@internationalized/date"; import { VirtualScroll } from "renderer/components/shared/virtual-scroll/virtual-scroll.component"; +import { MapItemComponentPropsMapper } from "shared/mappers/map/map-item-component-props.mapper"; export const DownloadMapsModal: ModalComponent = ({ options: {data : { ownedMaps, version }} }) => { const beatSaver = useService(BeatSaverService); @@ -149,7 +149,7 @@ export const DownloadMapsModal: ModalComponent = ({ resolver, options: { data: { version, maps$, playlist } } }) => { const t = useTranslation(); @@ -74,7 +77,7 @@ export const EditPlaylistModal: ModalComponent = ({ resolver, opt return Object.values(BsvSearchOrder).map(sort => ({ text: `beat-saver.maps-sorts.${sort}`, value: sort })); }); - const playlistMaps$ = useConstant(() => new BehaviorSubject>(undefined)); + const playlistMaps$ = useConstant(() => new BehaviorSubject>(undefined)); const playlistMaps = useObservable(() => playlistMaps$, undefined); const playlistHashsSelected$ = useConstant(() => new BehaviorSubject([])); @@ -83,7 +86,18 @@ export const EditPlaylistModal: ModalComponent = ({ resolver, opt const [availableMapsSource, setAvailableMapsSource] = useState(0); - const displayablePlaylistMaps = useMemo(() => playlistMaps ? Object.values(playlistMaps).filter(Boolean) : [], [playlistMaps]); + const displayablePlaylistMaps = useMemo(() => { + if(!playlistMaps || !Object.keys(playlistMaps).length){ return []; } + + return Object.values(playlistMaps).reduce((acc, playlistMap) => { + if(!playlistMap?.map){ return acc; } + if(!isMapFitFilter({ map: playlistMap.map, filter: playlistMapsFilter, search: playlistMapsSearch })){ return acc; } + acc.push(playlistMap); + return acc; + }, []); + }, [playlistMaps]); + + console.log(displayablePlaylistMaps); useOnUpdate(() => { const keyDown = (e: KeyboardEvent) => keyPressed$.next(e.key); @@ -103,23 +117,30 @@ export const EditPlaylistModal: ModalComponent = ({ resolver, opt const maps = await lastValueFrom(maps$.pipe(take(1))); const playlistMapsRes = (playlist?.songs ?? []).reduce((acc, song) => { - const map = maps.find(map => map.hash === song.hash); + const songHash = song.hash?.toLowerCase(); + const map = maps.find(map => map.hash === songHash); if(map){ - acc[map.hash] = map; + acc[songHash] = { map, difficulties: song.difficulties }; } else { - acc[song.hash] = undefined; + acc[songHash] = { map: undefined, difficulties: song.difficulties }; } return acc; - }, {} as Record); + }, {} as Record); - const notInstalledHashs = Object.keys(playlistMapsRes).filter(hash => !playlistMapsRes[hash]); + const notInstalledHashs = Object.keys(playlistMapsRes).filter(hash => !playlistMapsRes[hash]?.map); const songsDetails = await lastValueFrom(mapsService.getMapsInfoFromHashs(notInstalledHashs)); songsDetails.forEach(song => { - playlistMapsRes[song.hash] = song; + const songHash = song.hash?.toLowerCase(); + if(playlistMapsRes[song.hash]){ + playlistMapsRes[songHash].map = song; + } + else{ + playlistMapsRes[songHash] = { map: song }; + } }); localMaps$.next([...(maps ?? []), ...(songsDetails ?? [])]); @@ -184,15 +205,28 @@ export const EditPlaylistModal: ModalComponent = ({ resolver, opt selectedHashs$.next([]); }, []); - const renderMapItem = useCallback((map: (BsmLocalMap|BsvMapDetail|SongDetails), onClick: (map: (BsmLocalMap|BsvMapDetail|SongDetails)) => void, isSelected$: Observable, isOwned$?: Observable) => { + const renderMapItem = useCallback(( + map: (BsmLocalMap|BsvMapDetail|SongDetails), + opt?: { + onClick?: (map: (BsmLocalMap|BsvMapDetail|SongDetails)) => void, + isSelected$?: Observable, + isOwned$?: Observable, + diffsSelected?: BPListDifficulty[], + onSelectedDiffsChange?: (diff: BPListDifficulty[]) => void + } + ) => { + + console.log(opt.diffsSelected); return ( ); }, []); @@ -200,35 +234,51 @@ export const EditPlaylistModal: ModalComponent = ({ resolver, opt const renderAvailableMapItem = useCallback((localMap: BsmLocalMap | SongDetails | BsvMapDetail) => { const mapHash = getHashOfMap(localMap); const isSelected$ = availabledHashsSelected$.pipe(map(hashs => hashs.includes(mapHash))); - return renderMapItem(localMap, () => handleOnClickMap({ - map: localMap, - mapsSource$: localMaps$ as BehaviorSubject<(BsmLocalMap|BsvMapDetail|SongDetails)[]>, - selectedHashs$: availabledHashsSelected$, - noKeyPressedFallBack: () => playlistMaps$.next({...playlistMaps$.value, [mapHash]: localMap}) - }), isSelected$); + return renderMapItem(localMap, { + onClick: () => handleOnClickMap({ + map: localMap, + mapsSource$: localMaps$ as BehaviorSubject<(BsmLocalMap|BsvMapDetail|SongDetails)[]>, + selectedHashs$: availabledHashsSelected$, + noKeyPressedFallBack: () => playlistMaps$.next({...playlistMaps$.value, [mapHash]: { map: localMap }}) + }), + isSelected$ + }); }, []); const renderBsvMapItem = useCallback((bsvMap: BsvMapDetail) => { const mapHash = getHashOfMap(bsvMap); const isSelected$ = availabledHashsSelected$.pipe(map(hashs => hashs.includes(mapHash))); const isOwned$ = playlistMaps$.pipe(map(playlistMaps => !!playlistMaps[mapHash])); - return renderMapItem(bsvMap, () => handleOnClickMap({ - map: bsvMap, - mapsSource$: bsvMaps$, - selectedHashs$: availabledHashsSelected$, - noKeyPressedFallBack: () => playlistMaps$.next(Object.assign({[mapHash]: bsvMap}, playlistMaps$.value)) - }), isSelected$, isOwned$); + return renderMapItem(bsvMap, { + onClick: () => handleOnClickMap({ + map: bsvMap, + mapsSource$: bsvMaps$, + selectedHashs$: availabledHashsSelected$, + noKeyPressedFallBack: () => playlistMaps$.next(Object.assign({[mapHash]: bsvMap}, playlistMaps$.value)) + }), + isSelected$, + isOwned$ + }); }, []); - const renderPlaylistMapItem = useCallback((playlistMap: (BsmLocalMap|BsvMapDetail|SongDetails)) => { - const mapHash = getHashOfMap(playlistMap); + const renderPlaylistMapItem = useCallback((playlistMap: PlaylistMap) => { + const mapHash = getHashOfMap(playlistMap.map); const isSelected$ = playlistHashsSelected$.pipe(map(hashs => hashs.includes(mapHash))); - return renderMapItem(playlistMap, () => handleOnClickMap({ - map: playlistMap, - mapsSource$: playlistMaps$.pipe(map(Object.values)), - selectedHashs$: playlistHashsSelected$, - noKeyPressedFallBack: () => playlistMaps$.next(Object.fromEntries(Object.entries(playlistMaps$.value).filter(([hash]) => hash !== mapHash))) - }), isSelected$); + return renderMapItem(playlistMap.map, { + onClick: () => handleOnClickMap({ + map: playlistMap.map, + mapsSource$: playlistMaps$.pipe(map(Object.values)), + selectedHashs$: playlistHashsSelected$, + noKeyPressedFallBack: () => playlistMaps$.next(Object.fromEntries(Object.entries(playlistMaps$.value).filter(([hash]) => hash !== mapHash))) + }), + onSelectedDiffsChange: diffs => { + const newPlaylistMaps = {...playlistMaps$.value}; + newPlaylistMaps[mapHash].difficulties = diffs; + playlistMaps$.next(newPlaylistMaps); + }, + diffsSelected: playlistMap.difficulties, + isSelected$ + }); }, []); const renderList = (maps: T[], render: (item: T) => JSX.Element, scrollEndHandler?: VirtualScrollEndHandler) => { @@ -256,7 +306,7 @@ export const EditPlaylistModal: ModalComponent = ({ resolver, opt ) : tmpLocalMaps; const newPlaylistMaps = {...playlistMaps$.value} ?? {}; - mapsToAdd.forEach(map => newPlaylistMaps[getHashOfMap(map)] = map); + mapsToAdd.forEach(map => newPlaylistMaps[getHashOfMap(map)] = { map }); playlistMaps$.next(newPlaylistMaps); availabledHashsSelected$.next([]); @@ -292,8 +342,14 @@ export const EditPlaylistModal: ModalComponent = ({ resolver, opt } const playlistNbMappers = useMemo(() => { + if(!playlistMaps || Object.keys(playlistMaps).length === 0){ return "0"; } const mappersSet = new Set(); - Object.values(playlistMaps ?? {}).forEach(map => { + Object.values(playlistMaps ?? {}).forEach(playlistMap => { + + if(!playlistMap?.map){ return; } + + const map = playlistMap.map; + if((map as BsmLocalMap).rawInfo?._levelAuthorName){ mappersSet.add((map as BsmLocalMap).rawInfo._levelAuthorName); } @@ -308,7 +364,13 @@ export const EditPlaylistModal: ModalComponent = ({ resolver, opt }, [playlistMaps]); const playlistDuration = useMemo(() => { - const durations = Object.values(playlistMaps ?? {}).map(map => { + if(!playlistMaps || Object.keys(playlistMaps).length === 0){ return "0:00"; } + const durations = Object.values(playlistMaps ?? {}).map(playlistMap => { + + if(!playlistMap?.map){ return 0; } + + const map = playlistMap.map; + if((map as BsmLocalMap)?.songDetails?.duration){ return (map as BsmLocalMap).songDetails.duration; } @@ -326,7 +388,13 @@ export const EditPlaylistModal: ModalComponent = ({ resolver, opt }, [playlistMaps]); const [playlistMinNps, playlistMaxNps] = useMemo(() => { - const nps = Object.values(playlistMaps ?? {}).reduce((acc, map) => { + if(!playlistMaps || Object.keys(playlistMaps).length === 0){ return [0, 0]; } + const nps = Object.values(playlistMaps ?? {}).reduce((acc, playlistMap) => { + + if(!playlistMap?.map){ return acc; } + + const map = playlistMap.map; + if(Array.isArray((map as BsmLocalMap)?.songDetails?.difficulties)){ acc.push(...(map as BsmLocalMap).songDetails.difficulties.map(diff => diff.nps)); } @@ -363,13 +431,15 @@ export const EditPlaylistModal: ModalComponent = ({ resolver, opt playlistAuthor: res.data.playlistAuthor, playlistTitle: res.data.playlistTitle, playlistDescription: res.data.playlistDescription, - songs: Object.values(playlistMaps$.value ?? []).map(map => { - const props = MapItemComponentPropsMapper.from(map); - return { + songs: Object.values(playlistMaps$.value ?? []).map(playlistMap => { + const props = MapItemComponentPropsMapper.from(playlistMap.map); + const playlistSong: PlaylistSong = { key: props.mapId, hash: props.hash, - songName: props.title + songName: props.title, + difficulties: playlistMap.difficulties, } + return playlistSong; }) } @@ -377,12 +447,8 @@ export const EditPlaylistModal: ModalComponent = ({ resolver, opt }; const handlePlaylistMapDragEnd = useCallback((fromIndex: number, toIndex: number) => { - const playlistMapsArray = Object.values(playlistMaps$.value ?? {}); - const newPlaylistMaps = [...playlistMapsArray]; - const [removed] = newPlaylistMaps.splice(fromIndex, 1); - newPlaylistMaps.splice(toIndex, 0, removed); - - playlistMaps$.next(Object.fromEntries(newPlaylistMaps.map(map => [getHashOfMap(map), map]))); + const playlistMapsArray = swapElements(fromIndex, toIndex, Object.entries(playlistMaps$.value)); + return playlistMaps$.next(Object.fromEntries(playlistMapsArray)); }, []); return ( @@ -451,9 +517,9 @@ export const EditPlaylistModal: ModalComponent = ({ resolver, opt rows: "py-2.5 px-2.5" }} itemHeight={110} - items={displayablePlaylistMaps.filter(map => { - return isMapFitFilter({ map, filter: playlistMapsFilter, search: playlistMapsSearch }); - })} + items={displayablePlaylistMaps.filter(playlistMap => ( + isMapFitFilter({ map: playlistMap.map, filter: playlistMapsFilter, search: playlistMapsSearch }) + ))} isDragDisabled={!!Object.keys(playlistMapsFilter).length || !!playlistMapsSearch} renderItem={renderPlaylistMapItem} onDragEnd={handlePlaylistMapDragEnd} 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 5980d86f..084d4aba 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 @@ -6,7 +6,7 @@ import { Observable } from "rxjs"; import { useObservable } from "renderer/hooks/use-observable.hook"; import { ModalComponent, ModalExitCode } from "renderer/services/modale.service"; import { useCallback, useEffect, useState } from "react"; -import { MapItem, extractMapDiffs } from "renderer/components/maps-playlists-panel/maps/map-item.component"; +import { MapItem } 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"; @@ -18,6 +18,7 @@ import BeatConflict from "../../../../../../../assets/images/apngs/beat-conflict 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"; +import { MapItemComponentPropsMapper } from "shared/mappers/map/map-item-component-props.mapper"; type Props = { version: BSVersion; @@ -81,7 +82,7 @@ export const BsvPlaylistDetailsModal: ModalComponent = ({ resolver, mapId={map.id} ranked={map.ranked} title={map.name} songAutor={map.metadata.songAuthorName} - diffs={extractMapDiffs({ bsvMap: map })} + diffs={MapItemComponentPropsMapper.extractMapDiffs({ bsvMap: map })} songUrl={map.versions.at(0).previewURL} key={map.id} onDownload={downloadable && (() => mapsDownloader.addMapToDownload({version, map}))} @@ -143,39 +144,6 @@ export const BsvPlaylistDetailsModal: ModalComponent = ({ resolver, 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; - - // 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/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 1c789271..2a8bbc53 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 @@ -4,7 +4,7 @@ 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"; -import { MapItem, extractMapDiffs } from "renderer/components/maps-playlists-panel/maps/map-item.component"; +import { MapItem } from "renderer/components/maps-playlists-panel/maps/map-item.component"; import { useService } from "renderer/hooks/use-service.hook"; import { AudioPlayerService } from "renderer/services/audio-player.service"; import { BsmImage } from "renderer/components/shared/bsm-image.component"; @@ -16,6 +16,7 @@ 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"; +import { MapItemComponentPropsMapper } from "shared/mappers/map/map-item-component-props.mapper"; // TODO : Translate @@ -72,7 +73,7 @@ export const LocalPlaylistDetailsModal: ModalComponent = ({resolver songAutor={map.rawInfo._songAuthorName} bpm={map.rawInfo._beatsPerMinute} duration={map.songDetails?.duration} - diffs={extractMapDiffs({ rawMapInfo: map.rawInfo, songDetails: map.songDetails })} + diffs={MapItemComponentPropsMapper.extractMapDiffs({ rawMapInfo: map.rawInfo, songDetails: map.songDetails })} mapId={map.songDetails?.id} ranked={map.songDetails?.ranked} autorId={map.songDetails?.uploader.id} diff --git a/src/renderer/components/shared/bsm-image.component.tsx b/src/renderer/components/shared/bsm-image.component.tsx index ef938dbe..3bc8037e 100644 --- a/src/renderer/components/shared/bsm-image.component.tsx +++ b/src/renderer/components/shared/bsm-image.component.tsx @@ -16,9 +16,12 @@ export const BsmImage = forwardRef(({ className, image, const [isLoaded, setIsLoaded] = useState(false); const getBase64Url = () => { + if(!base64){ + return undefined; + } - if(base64?.startsWith("data:image")){ return base64; } - return base64 ? `data:image/png;base64,${base64}` : undefined; + const base64Data = base64.split("base64,").pop(); + return base64Data ? `data:image/jpeg;base64,${base64Data}` : undefined; }; const imageSrc = image || getBase64Url() || placeholder || errorImage; diff --git a/src/renderer/components/title-bar/title-bar.component.tsx b/src/renderer/components/title-bar/title-bar.component.tsx index 6d0ca49f..4b6c3c60 100644 --- a/src/renderer/components/title-bar/title-bar.component.tsx +++ b/src/renderer/components/title-bar/title-bar.component.tsx @@ -25,10 +25,10 @@ export default function TitleBar({ template = "index.html" }: { template: AppWin useEffect(() => { lastValueFrom(ipcService.sendV2("current-version")).then(version => { - if (version.toLocaleLowerCase().includes("alpha")) { + if (version.toLowerCase().includes("alpha")) { return setPreviewVersion("ALPHA"); } - if (version.toLocaleLowerCase().includes("beta")) { + if (version.toLowerCase().includes("beta")) { return setPreviewVersion("BETA"); } }); diff --git a/src/shared/mappers/map/map-item-component-props.mapper.ts b/src/shared/mappers/map/map-item-component-props.mapper.ts index bc64da46..05758ed8 100644 --- a/src/shared/mappers/map/map-item-component-props.mapper.ts +++ b/src/shared/mappers/map/map-item-component-props.mapper.ts @@ -1,16 +1,19 @@ -import { getLocalTimeZone, parseAbsolute, parseDateTime, toCalendarDateTime } from "@internationalized/date"; -import { MapItemComponentProps, ParsedMapDiff } from "renderer/components/maps-playlists-panel/maps/map-item.component"; -import { BsvMapDetail, RawMapInfoData, SongDetailDiffCharactertistic, SongDetails } from "shared/models/maps"; +import { getLocalTimeZone, parseAbsolute, toCalendarDateTime } from "@internationalized/date"; +import { MapItemComponentProps } from "renderer/components/maps-playlists-panel/maps/map-item.component"; +import { BsvMapDetail, RawMapInfoData, SongDetailDiffCharactertistic, SongDetails, SongDiffName } from "shared/models/maps"; import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"; +export type ParsedMapDiff = { name: SongDiffName; libelle: string; stars: number }; + export abstract class MapItemComponentPropsMapper { public static 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 }); + arr.push({ libelle: diff.difficulty, name: diff.difficulty, stars: diff.stars }); res.set(diff.characteristic, arr); }); return res; @@ -19,20 +22,22 @@ export abstract class MapItemComponentPropsMapper { 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 }); + const diffName = rawMapInfo?._difficultyBeatmapSets?.find(set => set._beatmapCharacteristicName === diff.characteristic)._difficultyBeatmaps.find(rawDiff => rawDiff._difficulty === diff.difficulty)?._customData?._difficultyLabel || diff.difficulty; + arr.push({ libelle: diffName, name: 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); + if(rawMapInfo?._difficultyBeatmapSets){ + rawMapInfo._difficultyBeatmapSets.forEach(set => { + set._difficultyBeatmaps.forEach(diff => { + const arr = res.get(set._beatmapCharacteristicName) || []; + arr.push({ libelle: diff._customData?._difficultyLabel || diff._difficulty, name: diff._difficulty, stars: null }); + res.set(set._beatmapCharacteristicName, arr); + }); }); - }); + } return res; } diff --git a/src/shared/models/playlists/playlist.interface.ts b/src/shared/models/playlists/playlist.interface.ts index f6cd9792..81390933 100644 --- a/src/shared/models/playlists/playlist.interface.ts +++ b/src/shared/models/playlists/playlist.interface.ts @@ -12,11 +12,13 @@ export interface BPList { } export interface PlaylistSong { - key: string; - hash: string; - songName: string; - uploader?: string; + key?: string; + hash?: string; + songName?: string; songDetails?: SongDetails; + difficulties?: BPListDifficulty[]; + levelAuthorName?: string; + levelid?: string; } export interface DownloadPlaylistProgressionData { @@ -28,3 +30,8 @@ export interface DownloadPlaylistProgressionData { export interface CustomDataBPList { syncURL?: string; } + +export interface BPListDifficulty { + name: string, + characteristic: string, +}