[feature-107] add diffictulties hightlight for playlist maps

This commit is contained in:
MathieuG-P
2024-06-22 19:40:49 +02:00
parent 33be4de2ff
commit 3427cae437
15 changed files with 277 additions and 181 deletions
-4
View File
@@ -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";
@@ -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<BPList>(source);
const bpList: BPList = isLocalFile ? JSON.parse(readFileSync(source).toString()) : await this.request.getJSON<BPList>(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);
@@ -37,6 +37,7 @@ export class SongDetailsCacheService {
private readonly utils: UtilsService;
private songDetailsCache: Record<string, SongDetails> = {};
private songDetailsIdIndex: Record<string, SongDetails> = {};
private readonly _loaded$ = new BehaviorSubject<boolean>(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<string, SongDetails>): Record<string, SongDetails> {
const res: Record<string, SongDetails> = {};
for(const hash in songDetailsCache){
res[songDetailsCache[hash].id] = songDetailsCache[hash];
}
return res;
}
private async readProtoMessageCacheFile(filePath: string): Promise<Record<string, SongDetails>> {
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()];
}
}
@@ -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<unknown, Props>(({ 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}
@@ -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<T = unknown> = {
hash: string;
@@ -38,6 +40,7 @@ export type MapItemComponentProps<T = unknown> = {
autorId: number;
mapId: string;
diffs: Map<SongDetailDiffCharactertistic, ParsedMapDiff[]>;
diffsSelected?: BPListDifficulty[];
ranked?: boolean;
blRanked?: boolean;
bpm?: number;
@@ -55,9 +58,10 @@ export type MapItemComponentProps<T = unknown> = {
onSelected?: (param: T) => void;
onCancelDownload?: (param: T) => void;
onDoubleClick?: (param: T) => void;
onSelectedDiffsChange?: (diffs: BPListDifficulty[]) => void;
};
export function MapItemComponent <T = unknown>({ 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<T>) {
export function MapItemComponent <T = unknown>({ 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<T>) {
const linkOpener = useService(LinkOpenerService);
const audioPlayer = useService(AudioPlayerService);
@@ -70,6 +74,7 @@ export function MapItemComponent <T = unknown>({ hash, title, autor, songAutor,
const [hovered, setHovered] = useState(false);
const [bottomBarHovered, setBottomBarHovered, cancelBottomBarHovered] = useDelayedState(false);
const [diffsPanelHovered, setDiffsPanelHovered] = useState(false);
const [_diffsSelected, setDiffsSelected] = useState<BPListDifficulty[]>(diffsSelected ?? []);
useDoubleClick({
ref,
@@ -78,6 +83,11 @@ export function MapItemComponent <T = unknown>({ 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 <T = unknown>({ 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 <T = unknown>({ 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 <T = unknown>({ hash, title, autor, songAutor,
<BsmIcon className="h-4 w-4 mr-px" icon="bsMapDifficulty" />
<div className="flex py-[2px] gap-[1px] h-full">
{diffSet.map((diff, index) => (
<span key={index} className="h-full w-[6px] rounded-full" style={{ backgroundColor: MAP_DIFFICULTIES_COLORS[diff.type] }} />
<span key={index} className="h-full w-[6px] rounded-full" style={{ backgroundColor: MAP_DIFFICULTIES_COLORS[diff.name] }} />
))}
</div>
</>
@@ -166,6 +183,19 @@ export function MapItemComponent <T = unknown>({ 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 (
<motion.li className="relative h-[100px] min-w-[370px] shrink-0 grow basis-0 text-white group cursor-pointer" onHoverStart={() => setHovered(true)} onHoverEnd={() => setHovered(false)} style={{ zIndex: hovered && 5, transform: "translateZ(0) scale(1.0, 1.0)", backfaceVisibility: "hidden" }}>
<GlowEffect visible={hovered || (isSelected && !!onSelected)} />
@@ -174,13 +204,16 @@ export function MapItemComponent <T = unknown>({ hash, title, autor, songAutor,
<motion.ul key={hash} className="absolute top-[calc(100%-10px)] w-full h-fit max-h-[200%] pt-4 pb-2 px-2 overflow-y-scroll bg-light-main-color-3 dark:bg-main-color-3 text-main-color-1 dark:text-current brightness-125 rounded-md flex flex-col gap-3 scrollbar-default shadow-sm shadow-black" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.15 }} onHoverStart={diffsPanelHoverStart} onHoverEnd={diffsPanelHoverEnd}>
{Array.from(diffs.entries()).map(([charac, diffSet], index) => (
<ol key={index} className="flex flex-col w-full gap-1">
{diffSet.map(({ type, name, stars }, index) => (
<li key={`${type}${name}${stars}${index}`} className="w-full h-4 flex items-center gap-1">
<BsmIcon className="h-full w-fit p-px" icon={charac} />
<span className="h-full px-2 flex items-center text-xs font-bold bg-current rounded-full" style={{ color: MAP_DIFFICULTIES_COLORS[type] }}>
{stars ? <span className="h-full block brightness-[.25]"> {stars.toFixed(2)}</span> : <span className="h-full brightness-[.25] leading-4 pb-[2px] capitalize">{parseDiffLabel(type)}</span>}
{diffSet.map(({ name, libelle, stars }, index) => (
<li key={`${name}${libelle}${stars}${index}`} className="w-full h-4 flex items-center gap-1">
{onSelectedDiffsChange && (
<BsmCheckbox className="h-full aspect-square relative" checked={isDiffChecked({name, characteristic: charac})} onChange={() => handleDiffCheckChange({name, characteristic: charac})} />
)}
<BsmIcon className="h-full w-fit p-px shrink-0" icon={charac} />
<span className="h-full px-2 flex items-center text-xs font-bold bg-current rounded-full" style={{ color: MAP_DIFFICULTIES_COLORS[name] }}>
{stars ? <span className="h-full block brightness-[.25]"> {stars.toFixed(2)}</span> : <span className="h-full brightness-[.25] leading-4 pb-[2px] capitalize">{parseDiffLabel(name)}</span>}
</span>
<span className="text-sm leading-4 pb-[2px] capitalize">{parseDiffLabel(name)}</span>
<span className="text-sm leading-4 pb-[2px] line-clamp-1">{parseDiffLabel(libelle)}</span>
</li>
))}
</ol>
@@ -190,7 +223,7 @@ export function MapItemComponent <T = unknown>({ hash, title, autor, songAutor,
</AnimatePresence>
<div className="h-full w-full relative pl-[100px] rounded-md overflow-hidden flex flex-row justify-end">
<div className={`absolute top-0 left-0 h-full aspect-square cursor-pointer ${isOwned && "border-l-[5px]"}`} style={{ borderColor: isOwned && color }}>
<BsmImage className="size-full object-cover" image={coverUrl} placeholder={defaultImage} errorImage={defaultImage} />
<BsmImage className="size-full object-cover" image={mapCoverUrl} placeholder={defaultImage} errorImage={defaultImage} />
<span
className="absolute flex justify-center items-center size-full pr-1 bg-transparent top-0 left-0 group-hover:bg-black group-hover:bg-opacity-40"
style={{ color }}
@@ -204,7 +237,7 @@ export function MapItemComponent <T = unknown>({ hash, title, autor, songAutor,
</span>
</div>
<div className="relative h-full w-full z-[1] rounded-md overflow-hidden -translate-x-1" ref={ref}>
<BsmImage className="absolute top-0 left-0 size-full -z-[1] object-cover saturate-200" image={coverUrl} placeholder={defaultImage} errorImage={defaultImage} />
<BsmImage className="absolute top-0 left-0 size-full -z-[1] object-cover saturate-200" image={mapCoverUrl} placeholder={defaultImage} errorImage={defaultImage} />
<div className="pt-1 pl-2 pr-7 top-0 left-0 size-full bg-neutral-600 bg-opacity-80 flex flex-col justify-between group-hover:bg-main-color-1 group-hover:bg-opacity-80">
<h1 className="font-bold whitespace-nowrap text-ellipsis overflow-hidden w-full leading-5 tracking-wide text-lg" title={title}>
<BsmLink className="hover:underline" href={mapUrl}>
@@ -351,36 +384,4 @@ export function MapItemComponent <T = unknown>({ hash, title, autor, songAutor,
);
};
export const MapItem = typedMemo(MapItemComponent, equal)
export function extractMapDiffs({rawMapInfo, songDetails, bsvMap}: {rawMapInfo?: RawMapInfoData, songDetails?: SongDetails, bsvMap?: BsvMapDetail}): Map<SongDetailDiffCharactertistic, ParsedMapDiff[]> {
const res = new Map<SongDetailDiffCharactertistic, ParsedMapDiff[]>();
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);
@@ -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<LocalPlaylistsListRef, Props>(
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<LocalPlaylistsListRef, Props>(
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<LocalPlaylistsListRef, Props>(
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<LocalPlaylistsListRef, Props>(
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<BsmLocalMap[]> = 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<LocalPlaylistsListRef, Props>(
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; }
@@ -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,
<div className="absolute inset-0 bg-black opacity-20"/>
</div>
<div className="relative h-full aspect-square p-2.5" style={{ color }}>
<BsmImage className="size-full flex-shrink-0 object-cover rounded-md shadow-center shadow-black bg-main-color-1" image={coverUrl} base64={coverBase64} style={{filter: hovered && "brightness(75%)"}} />
<BsmImage className="size-full flex-shrink-0 object-cover rounded-md shadow-center shadow-black bg-main-color-1" image={coverUrl} base64={coverBase64} placeholder={defaultImage} errorImage={defaultImage} style={{filter: hovered && "brightness(75%)"}} />
<SearchIcon
className="absolute size-full top-0 left-0 p-7 opacity-0 text-white hover:text-current group-hover:opacity-100 transition-opacity"
onClick={e => {e.stopPropagation(); onClickOpen?.()}}
@@ -196,4 +198,4 @@ export function PlaylistItem({ title,
</motion.li>
)
}
}, equal);
@@ -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<void, { version: BSVersion; ownedMaps: BsmLocalMap[] }> = ({ options: {data : { ownedMaps, version }} }) => {
const beatSaver = useService(BeatSaverService);
@@ -149,7 +149,7 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
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 && handleDownloadMap}
@@ -30,10 +30,11 @@ import { ClockIcon } from "renderer/components/svgs/icons/clock-icon.component";
import { NpsIcon } from "renderer/components/svgs/icons/nps-icon.component";
import dateFormat from 'dateformat';
import { getCorrectTextColor } from "renderer/helpers/correct-text-color";
import { BPList } from "shared/models/playlists/playlist.interface";
import { BPList, BPListDifficulty, PlaylistSong } from "shared/models/playlists/playlist.interface";
import { EditPlaylistInfosModal } from "./edit-playlist-infos-modal.component";
import { CrossIcon } from "renderer/components/svgs/icons/cross-icon.component";
import { DraggableVirtualScroll } from "renderer/components/shared/virtual-scroll/draggable-virtual-scroll.component";
import { swapElements } from "shared/helpers/array.helpers";
type Props = {
version?: BSVersion;
@@ -41,6 +42,8 @@ type Props = {
playlist?: LocalBPList;
}
type PlaylistMap = { map: (BsmLocalMap|SongDetails|BsvMapDetail), difficulties?: BPListDifficulty[] };
export const EditPlaylistModal: ModalComponent<BPList, Props> = ({ resolver, options: { data: { version, maps$, playlist } } }) => {
const t = useTranslation();
@@ -74,7 +77,7 @@ export const EditPlaylistModal: ModalComponent<BPList, Props> = ({ resolver, opt
return Object.values(BsvSearchOrder).map(sort => ({ text: `beat-saver.maps-sorts.${sort}`, value: sort }));
});
const playlistMaps$ = useConstant(() => new BehaviorSubject<Record<string, (BsmLocalMap|SongDetails|BsvMapDetail)>>(undefined));
const playlistMaps$ = useConstant(() => new BehaviorSubject<Record<string, PlaylistMap>>(undefined));
const playlistMaps = useObservable(() => playlistMaps$, undefined);
const playlistHashsSelected$ = useConstant(() => new BehaviorSubject<string[]>([]));
@@ -83,7 +86,18 @@ export const EditPlaylistModal: ModalComponent<BPList, Props> = ({ resolver, opt
const [availableMapsSource, setAvailableMapsSource] = useState<number>(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<BPList, Props> = ({ 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<string, (BsmLocalMap|BsvMapDetail|SongDetails)>);
}, {} as Record<string, PlaylistMap>);
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<BPList, Props> = ({ resolver, opt
selectedHashs$.next([]);
}, []);
const renderMapItem = useCallback((map: (BsmLocalMap|BsvMapDetail|SongDetails), onClick: (map: (BsmLocalMap|BsvMapDetail|SongDetails)) => void, isSelected$: Observable<boolean>, isOwned$?: Observable<boolean>) => {
const renderMapItem = useCallback((
map: (BsmLocalMap|BsvMapDetail|SongDetails),
opt?: {
onClick?: (map: (BsmLocalMap|BsvMapDetail|SongDetails)) => void,
isSelected$?: Observable<boolean>,
isOwned$?: Observable<boolean>,
diffsSelected?: BPListDifficulty[],
onSelectedDiffsChange?: (diff: BPListDifficulty[]) => void
}
) => {
console.log(opt.diffsSelected);
return (
<MapItem
key={(map as BsmLocalMap | SongDetails).hash ?? (map as BsvMapDetail).versions?.[0]?.hash}
{ ...MapItemComponentPropsMapper.from(map) }
selected$={isSelected$}
onSelected={onClick}
isOwned$={isOwned$}
selected$={opt?.isSelected$}
isOwned$={opt?.isOwned$}
diffsSelected={opt?.diffsSelected}
onSelected={opt?.onClick}
onSelectedDiffsChange={opt?.onSelectedDiffsChange}
/>
);
}, []);
@@ -200,35 +234,51 @@ export const EditPlaylistModal: ModalComponent<BPList, Props> = ({ 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 = <T, >(maps: T[], render: (item: T) => JSX.Element, scrollEndHandler?: VirtualScrollEndHandler) => {
@@ -256,7 +306,7 @@ export const EditPlaylistModal: ModalComponent<BPList, Props> = ({ 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<BPList, Props> = ({ resolver, opt
}
const playlistNbMappers = useMemo(() => {
if(!playlistMaps || Object.keys(playlistMaps).length === 0){ return "0"; }
const mappersSet = new Set<string>();
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<BPList, Props> = ({ 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<BPList, Props> = ({ 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<BPList, Props> = ({ 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<BPList, Props> = ({ 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<BPList, Props> = ({ 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}
@@ -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<void, Props> = ({ 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<void, Props> = ({ resolver,
margin: 100
}}
/>
// <ul className="min-h-0 w-full grow space-y-2 pl-2.5 pr-2 py-3 overflow-y-scroll overflow-x-hidden scrollbar-default">
// {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 (
// <MapItem
// key={map.id}
// title={map.name}
// autor={map.uploader.name}
// autorId={map.uploader.id}
// bpm={map.metadata.bpm}
// coverUrl={map.versions.at(0).coverURL}
// createdAt={map.createdAt && toCalendarDateTime(parseAbsolute(map.createdAt, getLocalTimeZone()))}
// duration={map.metadata.duration}
// hash={map.versions.at(0).hash}
// mapId={map.id}
// likes={map.stats.upvotes}
// ranked={map.ranked}
// songAutor={map.metadata.songAuthorName}
// songUrl={map.versions.at(0).previewURL}
// diffs={extractMapDiffs({ bsvMap: map })}
// downloading={isMapDownloading}
// onDownload={!isMapOwned && !isMapInQueue && (() => mapsDownloader.addMapToDownload({version, map}))}
// onCancelDownload={isMapInQueue && !isMapDownloading && (() => mapsDownloader.removeMapToDownload({version, map}))}
// callBackParam={map}
// />);
// })}
// <motion.span className="w-full h-8" onViewportEnter={() => setPage(prev => prev + 1)} />
// </ul>
)
})()}
</PlaylistDetailsTemplate>
@@ -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<void, Props> = ({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}
@@ -16,9 +16,12 @@ export const BsmImage = forwardRef<HTMLImageElement, Props>(({ 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;
@@ -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");
}
});
@@ -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<SongDetailDiffCharactertistic, ParsedMapDiff[]> {
const res = new Map<SongDetailDiffCharactertistic, ParsedMapDiff[]>();
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;
}
@@ -12,11 +12,13 @@ export interface BPList<SongType = PlaylistSong> {
}
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,
}