mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
[feature-107] add diffictulties hightlight for playlist maps
This commit is contained in:
@@ -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()];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
-2
@@ -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);
|
||||
|
||||
+28
-8
@@ -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}
|
||||
|
||||
+116
-50
@@ -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}
|
||||
|
||||
+3
-35
@@ -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>
|
||||
|
||||
+3
-2
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user