mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
[feature-107] start modal to create and edit playlists
This commit is contained in:
@@ -17,22 +17,23 @@ message UploadersList {
|
||||
|
||||
message SongDetails {
|
||||
uint32 idInt = 1;
|
||||
string hash = 2;
|
||||
uint32 duration = 3;
|
||||
UploaderRef uploaderRef = 4;
|
||||
uint32 uploadedAt = 5;
|
||||
repeated MapTag tags = 6;
|
||||
bool ranked = 7;
|
||||
bool qualified = 8;
|
||||
bool curated = 9;
|
||||
bool rankedBL = 10;
|
||||
bool nominatedBL = 11;
|
||||
bool qualifiedBL = 12;
|
||||
uint32 upVotes = 13;
|
||||
uint32 downVotes = 14;
|
||||
uint32 downloads = 15;
|
||||
bool automapper = 16;
|
||||
repeated Difficulty difficulties = 17;
|
||||
repeated uint32 hashIndices = 2;
|
||||
string name = 3;
|
||||
uint32 duration = 4;
|
||||
UploaderRef uploaderRef = 5;
|
||||
uint32 uploadedAt = 6;
|
||||
repeated MapTag tags = 7;
|
||||
bool ranked = 8;
|
||||
bool qualified = 9;
|
||||
bool curated = 10;
|
||||
bool rankedBL = 11;
|
||||
bool nominatedBL = 12;
|
||||
bool qualifiedBL = 13;
|
||||
uint32 upVotes = 14;
|
||||
uint32 downVotes = 15;
|
||||
uint32 downloads = 16;
|
||||
bool automapper = 17;
|
||||
repeated Difficulty difficulties = 18;
|
||||
}
|
||||
|
||||
message Difficulty {
|
||||
@@ -54,7 +55,7 @@ message Difficulty {
|
||||
}
|
||||
|
||||
message UploaderRef {
|
||||
uint32 uploaderRefIndex = 1;
|
||||
uint32 uploader_ref_index = 1;
|
||||
bool verified = 2;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { SongCacheService } from "main/services/additional-content/maps/song-cache.service";
|
||||
import { LocalMapsManagerService } from "../services/additional-content/maps/local-maps-manager.service";
|
||||
import { IpcService } from "../services/ipc.service";
|
||||
import { from, of, throwError } from "rxjs";
|
||||
import { tryit } from "shared/helpers/error.helpers";
|
||||
import { SongDetailsCacheService } from "main/services/additional-content/maps/song-details-cache.service";
|
||||
import { SongDetails } from "shared/models/maps";
|
||||
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
@@ -62,3 +65,20 @@ ipc.on("is-map-deep-links-enabled", (_, reply) => {
|
||||
|
||||
reply(of(result));
|
||||
});
|
||||
|
||||
ipc.on("get-maps-info-from-cache", (args, reply) => {
|
||||
const songsCache = SongDetailsCacheService.getInstance();
|
||||
|
||||
const res = (args ?? []).reduce((acc, hash) => {
|
||||
const songDetails = songsCache.getSongDetails(hash);
|
||||
|
||||
if(songDetails){
|
||||
acc.push(songDetails);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] as SongDetails[]);
|
||||
|
||||
reply(of(res));
|
||||
|
||||
})
|
||||
|
||||
@@ -81,7 +81,8 @@ export class SongDetailsCacheService {
|
||||
RawSongDetailsDeserializer.setDifficultyLabels(messageObj.difficultyLabels);
|
||||
|
||||
for(const rawSong of messageObj.songs){
|
||||
res[rawSong.hash.toLocaleLowerCase()] = RawSongDetailsDeserializer.deserialize(rawSong);
|
||||
const deserialized = RawSongDetailsDeserializer.deserialize(rawSong);
|
||||
res[deserialized.hash.toLocaleLowerCase()] = deserialized;
|
||||
}
|
||||
|
||||
return res;
|
||||
|
||||
@@ -108,6 +108,7 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
}
|
||||
|
||||
return [
|
||||
{ icon: "add", text: "Créer une playlist", onClick: () => playlistsRef?.current?.createPlaylist?.() },
|
||||
{ icon: "sync", text: "Synchroniser les playlists", onClick: () => playlistsRef?.current?.syncPlaylists?.() },
|
||||
{ icon: "export", text: "Exporter les playlists", onClick: () => playlistsRef?.current?.exportPlaylists?.() },
|
||||
{ icon: "trash", text: "Supprimer les playlists", onClick: () => playlistsRef?.current?.deletePlaylists?.() },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { MapFilter, MapRequirement, MapSpecificity, MapStyle, MapTag, MapType } from "shared/models/maps/beat-saver.model";
|
||||
import { BsvMapDetail, MapFilter, MapRequirement, MapSpecificity, MapStyle, MapTag, MapType } from "shared/models/maps/beat-saver.model";
|
||||
import { motion } from "framer-motion";
|
||||
import { MutableRefObject, useEffect, useRef, useState } from "react";
|
||||
import { BsmCheckbox } from "../../shared/bsm-checkbox.component";
|
||||
@@ -11,6 +11,8 @@ import { BsmButton } from "../../shared/bsm-button.component";
|
||||
import equal from "fast-deep-equal/es6";
|
||||
import clone from "rfdc";
|
||||
import { GlowEffect } from "../../shared/glow-effect.component";
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { SongDetails } from "shared/models/maps";
|
||||
|
||||
export type Props = {
|
||||
className?: string;
|
||||
@@ -158,7 +160,7 @@ export function FilterPanel({ className, ref, playlist = false, filter, localDat
|
||||
};
|
||||
|
||||
return !playlist ? (
|
||||
<motion.div ref={ref} className={`${className} bg-light-main-color-2 dark:bg-main-color-3`} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
|
||||
<motion.div ref={ref} className={`${className} bg-light-main-color-2 dark:bg-main-color-3`} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} draggable>
|
||||
<div className="w-full h-6 grid grid-cols-2 gap-x-12 px-4 mb-6 pt-1">
|
||||
<BsmRange min={MIN_NPS} max={MAX_NPS} values={npss} onChange={onNpssChange} renderLabel={renderNpsLabel} step={0.1} />
|
||||
<BsmRange min={MIN_DURATION} max={MAX_DURATION} values={durations} onChange={onDurationsChange} renderLabel={renderDurationLabel} step={5} />
|
||||
@@ -220,3 +222,159 @@ export function FilterPanel({ className, ref, playlist = false, filter, localDat
|
||||
<></>
|
||||
);
|
||||
}
|
||||
|
||||
// Filter functions
|
||||
|
||||
function isFitEnabledTags(filter: MapFilter, tags: MapTag[]): boolean {
|
||||
if(!Array.isArray(tags)) { return false; }
|
||||
if (!filter?.enabledTags || filter.enabledTags.size === 0) {
|
||||
return true;
|
||||
}
|
||||
return Array.from(filter.enabledTags.values()).every(tag => tags.some(mapTag => mapTag === tag));
|
||||
}
|
||||
|
||||
function isFitExcludedTags(filter: MapFilter, tags: MapTag[]): boolean {
|
||||
if(!Array.isArray(tags)) { return false; }
|
||||
if (!filter?.excludedTags || filter.excludedTags.size === 0) {
|
||||
return true;
|
||||
}
|
||||
return !tags.some(tag => filter.excludedTags.has(tag as MapTag));
|
||||
}
|
||||
|
||||
function isFitMinNps(filter: MapFilter, nps: number): boolean {
|
||||
if (!filter?.minNps) { return true; }
|
||||
return nps > filter.minNps;
|
||||
}
|
||||
|
||||
function isFitMaxNps(filter: MapFilter, nps: number): boolean {
|
||||
if (!filter?.maxNps) { return true; }
|
||||
return nps < filter.maxNps;
|
||||
}
|
||||
|
||||
function isFitMinDuration(filter: MapFilter, duration: number): boolean {
|
||||
if (!filter?.minDuration) { return true; }
|
||||
return duration >= filter.minDuration;
|
||||
}
|
||||
|
||||
function isFitMaxDuration(filter: MapFilter, duration: number): boolean {
|
||||
if (!filter?.maxDuration) { return true; }
|
||||
return duration <= filter.maxDuration;
|
||||
}
|
||||
|
||||
function isFitNoodle(filter: MapFilter, noodle: boolean): boolean {
|
||||
if (!filter?.noodle) { return true; }
|
||||
return noodle;
|
||||
}
|
||||
|
||||
function isFitMe(filter: MapFilter, me: boolean): boolean {
|
||||
if (!filter?.me) { return true; }
|
||||
return me;
|
||||
}
|
||||
|
||||
function isFitCinema(filter: MapFilter, cinema: boolean): boolean {
|
||||
if (!filter?.cinema) { return true; }
|
||||
return cinema;
|
||||
}
|
||||
|
||||
function isFitChroma(filter: MapFilter, chroma: boolean): boolean {
|
||||
if (!filter?.chroma) { return true; }
|
||||
return chroma;
|
||||
}
|
||||
|
||||
function isFitFullSpread(filter: MapFilter, nbDiff: number): boolean {
|
||||
if (!filter?.fullSpread) { return true; }
|
||||
return nbDiff >= 5;
|
||||
}
|
||||
|
||||
function isFitAutomapper(filter: MapFilter, automapper: boolean): boolean {
|
||||
if (!filter?.automapper) { return true; }
|
||||
return automapper;
|
||||
}
|
||||
|
||||
|
||||
function isFitRanked(filter: MapFilter, ranked: boolean): boolean {
|
||||
if (!filter?.ranked) { return true; }
|
||||
return ranked;
|
||||
}
|
||||
|
||||
function isFitCurated(filter: MapFilter, curated: boolean): boolean {
|
||||
if (!filter?.curated) { return true; }
|
||||
return curated;
|
||||
}
|
||||
|
||||
function isFitVerified(filter: MapFilter, verified: boolean): boolean {
|
||||
if (!filter?.verified) { return true; }
|
||||
return verified;
|
||||
}
|
||||
|
||||
function isFitSearch(search: string, {songName, songAuthorName, levelAuthorName}: {songName: string, songAuthorName: string, levelAuthorName: string}): boolean {
|
||||
if (!search) { return true; }
|
||||
return songName?.toLowerCase().includes(search.toLowerCase()) || songAuthorName?.toLowerCase().includes(search.toLowerCase()) || levelAuthorName?.toLowerCase().includes(search.toLowerCase());
|
||||
}
|
||||
|
||||
export const isLocalMapFitMapFilter = ({filter, map, search}: { filter: MapFilter, map: BsmLocalMap, search: string }): boolean => {
|
||||
if (!isFitEnabledTags(filter, map.songDetails?.tags)) { return false; }
|
||||
if (!isFitExcludedTags(filter, map.songDetails?.tags)) { return false; }
|
||||
if (map?.songDetails?.difficulties?.length && !map.songDetails?.difficulties.some(diff => isFitMinNps(filter, diff.nps))) { return false; }
|
||||
if (map?.songDetails?.difficulties?.length && !map.songDetails?.difficulties.some(diff => isFitMaxNps(filter, diff.nps))) { return false; }
|
||||
if (!isFitMinDuration(filter, map.songDetails?.duration)) { return false; }
|
||||
if (!isFitMaxDuration(filter, map.songDetails?.duration)) { return false; }
|
||||
if (!isFitNoodle(filter, map.songDetails?.difficulties.some(diff => !!diff.ne))) { return false; }
|
||||
if (!isFitMe(filter, map.songDetails?.difficulties.some(diff => !!diff.me))) { return false; }
|
||||
if (!isFitCinema(filter, map.songDetails?.difficulties.some(diff => !!diff.cinema))) { return false; }
|
||||
if (!isFitChroma(filter, map.songDetails?.difficulties.some(diff => !!diff.chroma))) { return false; }
|
||||
if (!isFitFullSpread(filter, map.songDetails?.difficulties.length)) { return false; }
|
||||
if (!isFitAutomapper(filter, map.songDetails?.automapper)){ return false; }
|
||||
if (!isFitRanked(filter, map.songDetails?.ranked)) { return false; }
|
||||
if (!isFitCurated(filter, map.songDetails?.curated)) { return false; }
|
||||
if (!isFitVerified(filter, map.songDetails?.uploader.verified)) { return false; }
|
||||
if (!isFitSearch(search, {songName: map.rawInfo?._songName, songAuthorName: map.rawInfo?._songAuthorName, levelAuthorName: map.rawInfo?._levelAuthorName})) { return false; }
|
||||
return true;
|
||||
};
|
||||
|
||||
export const isBsvMapFitMapFilter = ({filter, map, search}: { filter: MapFilter, map: BsvMapDetail, search: string }): boolean => {
|
||||
if (!isFitEnabledTags(filter, map.tags)) { return false; }
|
||||
if (!isFitExcludedTags(filter, map.tags)) { return false; }
|
||||
if (map.versions?.at(0)?.diffs && !map.versions.at(0).diffs.some(diff => isFitMinNps(filter, diff.nps))) { return false; }
|
||||
if (map.versions?.at(0)?.diffs && !map.versions.at(0).diffs.some(diff => isFitMaxNps(filter, diff.nps))) { return false; }
|
||||
if (!isFitMinDuration(filter, map.metadata.duration)) { return false; }
|
||||
if (!isFitMaxDuration(filter, map.metadata.duration)) { return false; }
|
||||
if (!isFitNoodle(filter, map.versions?.at(0)?.diffs.some(diff => !!diff.ne))) { return false; }
|
||||
if (!isFitMe(filter, map.versions?.at(0)?.diffs.some(diff => !!diff.me))) { return false; }
|
||||
if (!isFitCinema(filter, map.versions?.at(0)?.diffs.some(diff => !!diff.cinema))) { return false; }
|
||||
if (!isFitChroma(filter, map.versions?.at(0)?.diffs.some(diff => !!diff.chroma))) { return false; }
|
||||
if (!isFitFullSpread(filter, map.versions?.at(0)?.diffs.length)) { return false; }
|
||||
if (!isFitAutomapper(filter, map.automapper)){ return false; }
|
||||
if (!isFitRanked(filter, map.ranked)) { return false; }
|
||||
if (!isFitCurated(filter, !!map.curator)) { return false; }
|
||||
if (!isFitVerified(filter, !!map.curatedAt)) { return false; }
|
||||
if (!isFitSearch(search, {songName: map.name, songAuthorName: map.metadata.songAuthorName, levelAuthorName: map.metadata.levelAuthorName})) { return false; }
|
||||
return true;
|
||||
};
|
||||
|
||||
export const isSongDetailsFitMapFilter = ({filter, map, search}: { filter: MapFilter, map: SongDetails, search: string }): boolean => {
|
||||
if (!isFitEnabledTags(filter, map.tags)) { return false; }
|
||||
if (!isFitExcludedTags(filter, map.tags)) { return false; }
|
||||
if (map.difficulties && !map.difficulties.some(diff => isFitMinNps(filter, diff.nps))) { return false; }
|
||||
if (map.difficulties && !map.difficulties.some(diff => isFitMaxNps(filter, diff.nps))) { return false; }
|
||||
if (!isFitMinDuration(filter, map.duration)) { return false; }
|
||||
if (!isFitMaxDuration(filter, map.duration)) { return false; }
|
||||
if (!isFitNoodle(filter, map.difficulties.some(diff => !!diff.ne))) { return false; }
|
||||
if (!isFitMe(filter, map.difficulties.some(diff => !!diff.me))) { return false; }
|
||||
if (!isFitCinema(filter, map.difficulties.some(diff => !!diff.cinema))) { return false; }
|
||||
if (!isFitChroma(filter, map.difficulties.some(diff => !!diff.chroma))) { return false; }
|
||||
if (!isFitFullSpread(filter, map.difficulties.length)) { return false; }
|
||||
if (!isFitAutomapper(filter, map.automapper)){ return false; }
|
||||
if (!isFitRanked(filter, map.ranked)) { return false; }
|
||||
if (!isFitCurated(filter, map.curated)) { return false; }
|
||||
if (!isFitVerified(filter, map.uploader.verified)) { return false; }
|
||||
if (!isFitSearch(search, {songName: map.name, songAuthorName: map.uploader.name, levelAuthorName: map.uploader.name})) { return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
export const isMapFitFilter = ({filter, map, search}: { filter: MapFilter, map: BsmLocalMap | BsvMapDetail | SongDetails, search: string }): boolean => {
|
||||
if ((map as BsmLocalMap)?.rawInfo) { return isLocalMapFitMapFilter({filter, map: (map as BsmLocalMap), search}); }
|
||||
if ((map as BsvMapDetail)?.metadata) { return isBsvMapFitMapFilter({filter, map: (map as BsvMapDetail), search}); }
|
||||
if ((map as SongDetails).hash) { return isSongDetailsFitMapFilter({filter, map: (map as SongDetails), search}); }
|
||||
return false;
|
||||
};
|
||||
|
||||
+3
-170
@@ -21,6 +21,7 @@ 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 { isLocalMapFitMapFilter } from "./filter-panel.component";
|
||||
|
||||
type Props = {
|
||||
version: BSVersion;
|
||||
@@ -175,178 +176,10 @@ export const LocalMapsListPanel = forwardRef<unknown, Props>(({ version, classNa
|
||||
callBackParam={map}
|
||||
/>
|
||||
);
|
||||
}, [version])
|
||||
|
||||
const isMapFitFilter = (map: BsmLocalMap): boolean => {
|
||||
// Can be more clean and optimized i think
|
||||
|
||||
const fitEnabledTags = (() => {
|
||||
|
||||
if (!filter?.enabledTags || filter.enabledTags.size === 0) {
|
||||
return true;
|
||||
}
|
||||
if (!map?.songDetails?.tags) {
|
||||
return false;
|
||||
}
|
||||
return Array.from(filter.enabledTags.values()).every(tag => map.songDetails.tags.some(mapTag => mapTag === tag));
|
||||
})();
|
||||
|
||||
if (!fitEnabledTags) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const fitExcluedTags = (() => {
|
||||
if (!filter?.excludedTags || filter.excludedTags.size === 0) {
|
||||
return true;
|
||||
}
|
||||
if (!map?.songDetails?.tags) {
|
||||
return true;
|
||||
}
|
||||
return !map.songDetails?.tags.some(tag => filter.excludedTags.has(tag as MapTag));
|
||||
})();
|
||||
|
||||
if (!fitExcluedTags) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const fitMinNps = (() => {
|
||||
if (!filter?.minNps) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return map.songDetails?.difficulties.some(diff => diff.nps > filter.minNps);
|
||||
})();
|
||||
|
||||
if (!fitMinNps) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const fitMaxNps = (() => {
|
||||
if (!filter?.maxNps) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return map.songDetails?.difficulties.some(diff => diff.nps < filter.maxNps);
|
||||
})();
|
||||
|
||||
if (!fitMaxNps) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const fitMinDuration = (() => {
|
||||
if (!filter?.minDuration) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!map?.songDetails?.duration) {
|
||||
return false;
|
||||
}
|
||||
return map.songDetails?.duration >= filter.minDuration;
|
||||
})();
|
||||
|
||||
if (!fitMinDuration) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const fitMaxDuration = (() => {
|
||||
if (!filter?.maxDuration) {
|
||||
return true;
|
||||
}
|
||||
if (!map?.songDetails?.duration) {
|
||||
return false;
|
||||
}
|
||||
return map.songDetails?.duration <= filter.maxDuration;
|
||||
})();
|
||||
|
||||
if (!fitMaxDuration) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const fitNoodle = (() => {
|
||||
if (!filter?.noodle) {
|
||||
return true;
|
||||
}
|
||||
return map.songDetails?.difficulties.some(diff => !!diff.ne);
|
||||
})();
|
||||
|
||||
if (!fitNoodle) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const fitMe = (() => {
|
||||
if (!filter?.me) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return map.songDetails?.difficulties.some(diff => !!diff.me);
|
||||
})();
|
||||
|
||||
if (!fitMe) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const fitCinema = (() => {
|
||||
if (!filter?.cinema) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return map.songDetails?.difficulties.some(diff => !!diff.cinema);
|
||||
})();
|
||||
|
||||
if (!fitCinema) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const fitChroma = (() => {
|
||||
if (!filter?.chroma) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return map.songDetails?.difficulties.some(diff => !!diff.chroma);
|
||||
})();
|
||||
|
||||
if (!fitChroma) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const fitFullSpread = (() => {
|
||||
if (!filter?.fullSpread) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return map.songDetails?.difficulties.length >= 5;
|
||||
})();
|
||||
|
||||
if (!fitFullSpread) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filter?.automapper && (map.songDetails && !map.songDetails?.automapper)) {
|
||||
return false;
|
||||
}
|
||||
if (!(filter?.ranked ? map.songDetails?.ranked === filter.ranked : true)) {
|
||||
return false;
|
||||
}
|
||||
if (!(filter?.curated ? !!map.songDetails?.curated === filter.curated : true)) {
|
||||
return false;
|
||||
}
|
||||
if (!(filter?.verified ? !!map.songDetails?.uploader?.verified : true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const searchCheck = (() => {
|
||||
return (map.rawInfo?._songName || "")?.toLowerCase().includes(search.toLowerCase()) || (map.rawInfo?._songAuthorName || "")?.toLowerCase().includes(search.toLowerCase()) || (map.rawInfo?._levelAuthorName || "")?.toLowerCase().includes(search.toLowerCase());
|
||||
})();
|
||||
|
||||
if (!searchCheck) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
}, [version]);
|
||||
|
||||
const preppedMaps: RenderableMap[] = (() => {
|
||||
return renderableMaps?.filter(renderableMap => isMapFitFilter(renderableMap.map)) ?? [];
|
||||
return renderableMaps?.filter(renderableMap => isLocalMapFitMapFilter({ map: renderableMap.map, filter, search })) ?? [];
|
||||
})();
|
||||
|
||||
if (!maps) {
|
||||
|
||||
@@ -27,18 +27,18 @@ import { typedMemo } from "renderer/helpers/typed-memo";
|
||||
|
||||
export type ParsedMapDiff = { type: SongDiffName; name: string; stars: number };
|
||||
|
||||
export type MapItemProps<T = unknown> = {
|
||||
export type MapItemComponentProps<T = unknown> = {
|
||||
hash: string;
|
||||
title: string;
|
||||
autor: string;
|
||||
songAutor: string;
|
||||
coverUrl: string;
|
||||
songUrl: string;
|
||||
songAutor?: string;
|
||||
coverUrl?: string;
|
||||
songUrl?: string;
|
||||
autorId: number;
|
||||
mapId: string;
|
||||
diffs: Map<SongDetailDiffCharactertistic, ParsedMapDiff[]>;
|
||||
ranked: boolean;
|
||||
bpm: number;
|
||||
bpm?: number;
|
||||
duration: number;
|
||||
likes: number;
|
||||
createdAt: number | CalendarDateTime;
|
||||
@@ -53,7 +53,7 @@ export type MapItemProps<T = unknown> = {
|
||||
onDoubleClick?: (param: T) => void;
|
||||
};
|
||||
|
||||
export function MapItemComponent <T = unknown>({ hash, title, autor, songAutor, coverUrl, songUrl, autorId, mapId, diffs, ranked, bpm, duration, likes, createdAt, selected, downloading, showOwned, callBackParam, onDelete, onDownload, onSelected, onCancelDownload, onDoubleClick }: MapItemProps<T>) {
|
||||
export function MapItemComponent <T = unknown>({ hash, title, autor, songAutor, coverUrl, songUrl, autorId, mapId, diffs, ranked, bpm, duration, likes, createdAt, selected, downloading, showOwned, callBackParam, onDelete, onDownload, onSelected, onCancelDownload, onDoubleClick }: MapItemComponentProps<T>) {
|
||||
const linkOpener = useService(LinkOpenerService);
|
||||
const audioPlayer = useService(AudioPlayerService);
|
||||
|
||||
@@ -115,7 +115,7 @@ export function MapItemComponent <T = unknown>({ hash, title, autor, songAutor,
|
||||
if (!audioPlayer.playing && audioPlayer.src === songUrl) {
|
||||
return audioPlayer.resume();
|
||||
}
|
||||
audioPlayer.play([{ src: songUrl, bpm }]);
|
||||
audioPlayer.play([{ src: songUrl, bpm: bpm ?? 1 }]);
|
||||
};
|
||||
|
||||
const bottomBarHoverStart = () => {
|
||||
|
||||
-2
@@ -18,8 +18,6 @@ const [MIN_NB_MAPPER, MAX_NB_MAPPER] = [0, 1000];
|
||||
const [MIN_DURATION, MAX_DURATION] = [0, hourToS(9)];
|
||||
const [MIN_NPS, MAX_NPS] = [0, 17];
|
||||
|
||||
console.log(hourToS(9));
|
||||
|
||||
export function LocalPlaylistFilterPanel({ className, filter, onChange }: Props) {
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
+5
-1
@@ -37,6 +37,7 @@ import { ProgressionInterface } from "shared/models/progress-bar";
|
||||
import { enumerate } from "shared/helpers/array.helpers";
|
||||
import { SyncPlaylistModal } from "renderer/components/modal/modal-types/playlist/sync-playlist-modal.component";
|
||||
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.component";
|
||||
|
||||
type Props = {
|
||||
version: BSVersion;
|
||||
@@ -48,6 +49,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export type LocalPlaylistsListRef = {
|
||||
createPlaylist: () => Promise<void>;
|
||||
syncPlaylists: () => Promise<void>;
|
||||
deletePlaylists: () => Promise<void>;
|
||||
exportPlaylists: () => Promise<void>;
|
||||
@@ -87,6 +89,9 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
|
||||
}
|
||||
|
||||
useImperativeHandle(forwardedRef, () => ({
|
||||
createPlaylist: async () => {
|
||||
const modalRes = await modals.openModal(EditPlaylistModal, { noStyle: true, data: { version, maps$ } });
|
||||
},
|
||||
syncPlaylists: async () => {
|
||||
if(!isOnline){ return; }
|
||||
const toSync = selectedPlaylists$.value?.length ? selectedPlaylists$.value : playlists$.value;
|
||||
@@ -270,7 +275,6 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
|
||||
isInQueue$={playlistDownloader.$isPlaylistInQueue(playlist.customData?.syncURL ?? playlist.path, version)}
|
||||
selected$={selectedPlaylists$.pipe(map(selected => selected.some(s => s.path === playlist.path)), distinctUntilChanged(equal))}
|
||||
onClick={() => {
|
||||
console.log(selectedPlaylists$.value, playlist.path);
|
||||
if(selectedPlaylists$.value.some(s => s.path === playlist.path)){
|
||||
selectedPlaylists$.next(selectedPlaylists$.value.filter(s => s.path !== playlist.path));
|
||||
return;
|
||||
|
||||
-1
@@ -139,7 +139,6 @@ export const DownloadPlaylistModal: ModalComponent<void, {version: BSVersion, ow
|
||||
margin: 120
|
||||
}}
|
||||
renderItem={renderPlaylist}
|
||||
rowKey={items => items.map(item => item.playlist.playlistId).join("-")}
|
||||
/>
|
||||
)
|
||||
})()}
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { FilterPanel, isLocalMapFitMapFilter, isMapFitFilter } from "renderer/components/maps-playlists-panel/maps/filter-panel.component";
|
||||
import { MapItem, extractMapDiffs } from "renderer/components/maps-playlists-panel/maps/map-item.component";
|
||||
import { BsmDropdownButton } from "renderer/components/shared/bsm-dropdown-button.component";
|
||||
import { BsmSelect } from "renderer/components/shared/bsm-select.component";
|
||||
import { VirtualScroll } from "renderer/components/shared/virtual-scroll/virtual-scroll.component";
|
||||
import { ChevronTopIcon } from "renderer/components/svgs/icons/chevron-top-icon.component";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import { ModalComponent } from "renderer/services/modale.service"
|
||||
import { Observable, filter, lastValueFrom, take } from "rxjs";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { LocalBPList, LocalBPListsDetails } from "shared/models/playlists/local-playlist.models"
|
||||
import { BsvMapDetail, SongDetails } from "shared/models/maps";
|
||||
import { logRenderError } from "renderer";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { MapsManagerService } from "renderer/services/maps-manager.service";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { MapItemComponentPropsMapper } from "shared/mappers/map/map-item-component-props.mapper";
|
||||
import { MapFilter } from "shared/models/maps/beat-saver.model";
|
||||
|
||||
type Props = {
|
||||
version?: BSVersion;
|
||||
maps$: Observable<BsmLocalMap[]>;
|
||||
playlist?: LocalBPList;
|
||||
}
|
||||
|
||||
export const EditPlaylistModal: ModalComponent<LocalBPListsDetails, Props> = ({ resolver, options: { data: { version, maps$, playlist } } }) => {
|
||||
|
||||
const t = useTranslation();
|
||||
const color = useThemeColor("first-color");
|
||||
|
||||
const mapsService = useService(MapsManagerService);
|
||||
|
||||
const maps = useObservable(() => maps$.pipe(filter(Array.isArray), take(1)), undefined);
|
||||
|
||||
const [availableMapsSearch, setAvailableMapsSearch] = useState<string>("");
|
||||
const [availableMapsFilter, setAvailableMapsFilter] = useState<MapFilter>({});
|
||||
const [playlistMapsSearch, setPlaylistMapsSearch] = useState<string>("");
|
||||
const [playlistMapsFilter, setPlaylistMapsFilter] = useState<MapFilter>({});
|
||||
|
||||
const [availableMapsSource, setAvailableMapsSource] = useState<number>(0);
|
||||
|
||||
const [playlistMaps, setPlaylistMaps] = useState<Record<string, (BsmLocalMap|BsvMapDetail|SongDetails)>>();
|
||||
const displayablePlaylistMaps = useMemo(() => playlistMaps ? Object.values(playlistMaps).filter(Boolean) : [], [playlistMaps]);
|
||||
|
||||
const availableMaps = (() => {
|
||||
|
||||
})();
|
||||
|
||||
useOnUpdate(() => {
|
||||
if(!maps){ return; }
|
||||
|
||||
const playlistMapsRes = (playlist?.songs ?? []).reduce((acc, song) => {
|
||||
const map = maps.find(map => map.hash === song.hash);
|
||||
|
||||
if(map){
|
||||
acc[map.hash] = map;
|
||||
}
|
||||
else {
|
||||
acc[song.hash] = undefined;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {} as Record<string, (BsmLocalMap|BsvMapDetail|SongDetails)>);
|
||||
|
||||
|
||||
(async () => {
|
||||
const notInstalledHashs = Object.keys(playlistMapsRes).filter(hash => !playlistMapsRes[hash]);
|
||||
const songsDetails = await lastValueFrom(mapsService.getMapsInfoFromHashs(notInstalledHashs));
|
||||
|
||||
songsDetails.forEach(song => {
|
||||
playlistMapsRes[song.hash] = song;
|
||||
});
|
||||
|
||||
})()
|
||||
.catch(logRenderError)
|
||||
.finally(() => setPlaylistMaps(playlistMapsRes));
|
||||
}, [maps]);
|
||||
|
||||
useOnUpdate(() => {
|
||||
|
||||
}, [maps, playlistMaps, playlist]);
|
||||
|
||||
const renderMapItem = useCallback((map: (BsmLocalMap|BsvMapDetail|SongDetails), onClick: (map: (BsmLocalMap|BsvMapDetail|SongDetails)) => void) => {
|
||||
|
||||
return (
|
||||
<MapItem
|
||||
key={(map as BsmLocalMap | SongDetails).hash ?? (map as BsvMapDetail).versions?.[0]?.hash}
|
||||
{ ...MapItemComponentPropsMapper.from(map) }
|
||||
onSelected={onClick}
|
||||
/>
|
||||
);
|
||||
}, []);
|
||||
|
||||
const renderAvailableMapItem = useCallback((map: BsmLocalMap) => {
|
||||
return renderMapItem(map, () => setPlaylistMaps(prev => ({ ...prev, [map.hash]: map })));
|
||||
}, []);
|
||||
|
||||
const renderPlaylistMapItem = useCallback((map: (BsmLocalMap|BsvMapDetail|SongDetails)) => {
|
||||
return renderMapItem(map, () => setPlaylistMaps(prev => {
|
||||
|
||||
if(!map){ return prev; }
|
||||
|
||||
const hash = (map as BsmLocalMap | SongDetails).hash ? (map as BsmLocalMap|SongDetails).hash : (map as BsvMapDetail).versions?.[0]?.hash;
|
||||
|
||||
if(!hash){ return prev; }
|
||||
|
||||
const newPlaylistMaps = { ...prev };
|
||||
delete newPlaylistMaps[(map as BsmLocalMap).hash];
|
||||
return newPlaylistMaps;
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const renderList = <T, >(maps: T[], render: (item: T) => JSX.Element) => {
|
||||
return (
|
||||
<VirtualScroll
|
||||
classNames={{
|
||||
mainDiv: "bg-theme-1 rounded-md size-full min-w-0 overflow-hidden",
|
||||
rows: "my-2.5 px-2.5"
|
||||
}}
|
||||
items={maps}
|
||||
itemHeight={110}
|
||||
maxColumns={1}
|
||||
renderItem={render}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-screen h-screen max-h-[calc(100vh-2rem)] max-w-[55rem] lg:max-w-[66rem] xl:max-w-[77rem] bg-theme-3 p-4 rounded-md">
|
||||
{(() => {
|
||||
if(!playlistMaps || !maps){
|
||||
return <div className="flex items-center justify-center w-full h-full">Loading...</div>
|
||||
}
|
||||
else{
|
||||
return (
|
||||
<div className="size-full flex flex-col justify-between gap-3">
|
||||
<header>header</header>
|
||||
<div className="grow flex flex-row min-h-0 gap-2.5">
|
||||
<div className="flex flex-col grow basis-0">
|
||||
<div className="h-8 flex flex-row gap-2 w-full mb-1.5">
|
||||
<BsmSelect className="bg-theme-1 h-full rounded-full text-center" options={[{ text: "Local", value: 0 }, { text: "BeatSaver", value: 1 }]} onChange={setAvailableMapsSource}/>
|
||||
<input className="h-full bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-2 grow pb-0.5" type="text" placeholder={t("pages.version-viewer.maps.search-bar.search-placeholder")} value={availableMapsSearch} onChange={e => setAvailableMapsSearch(() => e.target.value)} />
|
||||
<BsmDropdownButton className="h-full relative z-[1] flex justify-center" buttonClassName="flex items-center justify-center h-full rounded-full px-2 py-1" icon="filter" text="pages.version-viewer.maps.search-bar.filters-btn" textClassName="whitespace-nowrap" withBar={false}>
|
||||
<FilterPanel className="absolute top-[calc(100%+3px)] origin-top w-[500px] h-fit p-2 rounded-md shadow-md shadow-black -translate-x-1/4 lg:translate-x-0" filter={availableMapsFilter} onChange={setAvailableMapsFilter}/>
|
||||
</BsmDropdownButton>
|
||||
</div>
|
||||
{renderList(maps.filter(map => {
|
||||
if(playlistMaps[map.hash]){ return false; }
|
||||
return isLocalMapFitMapFilter({ map, filter: availableMapsFilter, search: availableMapsSearch });
|
||||
}), renderAvailableMapItem)}
|
||||
<div className="w-full h-4 flex justify-end">
|
||||
</div>
|
||||
</div>
|
||||
<div className="shrink-0 flex flex-col gap-2.5 pb-4 pt-10">
|
||||
<button className="grow w-9 rounded-md cursor-pointer" style={{backgroundColor: color}} type="button"><ChevronTopIcon className="origin-center rotate-90"/></button>
|
||||
<button className="grow w-9 rounded-md cursor-pointer" style={{backgroundColor: color}} type="button"><ChevronTopIcon className="origin-center -rotate-90"/></button>
|
||||
</div>
|
||||
<div className="flex flex-col grow basis-0">
|
||||
<div className="h-8 flex flex-row gap-2 w-full mb-1.5">
|
||||
<input className="h-full bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-2 grow pb-0.5" type="text" placeholder={t("pages.version-viewer.maps.search-bar.search-placeholder")} value={playlistMapsSearch} onChange={e => setPlaylistMapsSearch(() => e.target.value)} />
|
||||
<BsmDropdownButton className="h-full relative z-[1] flex justify-center" buttonClassName="flex items-center justify-center h-full rounded-full px-2 py-1" icon="filter" text="pages.version-viewer.maps.search-bar.filters-btn" textClassName="whitespace-nowrap" withBar={false}>
|
||||
<FilterPanel className="absolute top-[calc(100%+3px)] origin-top w-[500px] h-fit p-2 rounded-md shadow-md shadow-black -translate-x-[40%]" filter={playlistMapsFilter} onChange={setPlaylistMapsFilter}/>
|
||||
</BsmDropdownButton>
|
||||
</div>
|
||||
{renderList(displayablePlaylistMaps.filter(map => {
|
||||
return isMapFitFilter({ map, filter: playlistMapsFilter, search: playlistMapsSearch });
|
||||
}), renderPlaylistMapItem)}
|
||||
<div className="w-full h-4 flex justify-end">
|
||||
<span className="text-xs italic leading-4">{Object.keys(playlistMaps ?? {}).length} Maps</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer>footer</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+1
-8
@@ -84,15 +84,8 @@ export const LocalPlaylistDetailsModal: ModalComponent<void, Props> = ({resolver
|
||||
}, []);
|
||||
|
||||
const renderMaps = () => {
|
||||
if (!Array.isArray(installedMaps) && !isInQueue) {
|
||||
return (
|
||||
<div className="grow bg-red-400">
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if(installedMaps.length === 0 && !isInQueue) {
|
||||
if(!installedMaps.length && !isInQueue) {
|
||||
return (
|
||||
<div className="grow flex justify-center items-center flex-col">
|
||||
<BsmImage image={BeatConflict} className="size-28"/>
|
||||
|
||||
@@ -62,10 +62,12 @@ export const BsmDropdownButton = forwardRef(({ className, items, align, withBar
|
||||
return "right-0 origin-top-right";
|
||||
})();
|
||||
|
||||
console.log("alignClass", alignClass);
|
||||
|
||||
return (
|
||||
<div ref={ref as unknown as React.LegacyRef<HTMLDivElement>} className={className}>
|
||||
<BsmButton onClick={() => setExpanded(!expanded)} className={buttonClassName ?? defaultButtonClassName} icon={icon} active={expanded} textClassName={textClassName} onClickOutside={handleClickOutside} withBar={withBar} text={text} />
|
||||
<div className={`py-1 w-fit absolute cursor-pointer top-[calc(100%-4px)] rounded-md bg-inherit text-sm text-gray-800 dark:text-gray-200 shadow-md shadow-black transition-[scale] ease-in-out ${alignClass}`} style={{ scale: expanded ? "1" : "0", translate: `0 ${menuTranslationY}` }}>
|
||||
<div className={`py-1 w-fit absolute cursor-pointer top-[calc(100%-4px)] rounded-md bg-inherit text-sm text-gray-800 dark:text-gray-200 shadow-md shadow-black transition-[scale] duration-150 ease-in-out ${alignClass}`} style={{ scale: expanded ? "1" : "0", translate: `0 ${menuTranslationY}` }}>
|
||||
{items?.map(
|
||||
i =>
|
||||
i && (
|
||||
|
||||
@@ -5,10 +5,10 @@ import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
|
||||
import { VirtualRow } from "./virtual-row.component";
|
||||
import { splitIntoChunk } from "shared/helpers/array.helpers";
|
||||
import { useConstant } from "renderer/hooks/use-constant.hook";
|
||||
import { BehaviorSubject, debounceTime } from "rxjs";
|
||||
import { BehaviorSubject, debounceTime, distinctUntilChanged } from "rxjs";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
|
||||
type ClassNames = {
|
||||
export type VirtualScrollClassNames = {
|
||||
mainDiv?: string;
|
||||
variableList?: string;
|
||||
rows?: string;
|
||||
@@ -21,8 +21,8 @@ type ScrollEndHandler = {
|
||||
|
||||
type Props<T = unknown> = {
|
||||
className?: string;
|
||||
classNames?: ClassNames;
|
||||
minItemWidth: number;
|
||||
classNames?: VirtualScrollClassNames;
|
||||
minItemWidth?: number;
|
||||
maxColumns: number;
|
||||
minColumns?: number;
|
||||
itemHeight: number;
|
||||
@@ -40,13 +40,15 @@ export function VirtualScroll<T = unknown>({ className, classNames, minItemWidth
|
||||
const [itemPerRow, setItemPerRow] = useState(1);
|
||||
const [itemsToRender, setItemsToRender] = useState<T[][]>([]);
|
||||
const listHeight$ = useConstant(() => new BehaviorSubject<number>(0));
|
||||
const listHeight = useObservable(() => listHeight$.pipe(debounceTime(100)), 0);
|
||||
const listHeight = useObservable(() => listHeight$.pipe(distinctUntilChanged(), debounceTime(100)), 0);
|
||||
|
||||
console.log(listHeight);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const updateItemPerRow = (listWidth: number) => {
|
||||
if (!listWidth) return;
|
||||
|
||||
const calculatedColumns = Math.floor(listWidth / minItemWidth);
|
||||
const calculatedColumns = Math.floor(listWidth / (minItemWidth ?? 1));
|
||||
const newColumns = Math.max((minColumns || 1), Math.min(maxColumns, calculatedColumns));
|
||||
setItemPerRow(() => newColumns);
|
||||
};
|
||||
@@ -64,6 +66,7 @@ export function VirtualScroll<T = unknown>({ className, classNames, minItemWidth
|
||||
useOnUpdate(() => {
|
||||
const splitedItems = splitIntoChunk(items, itemPerRow);
|
||||
setItemsToRender(() => splitedItems);
|
||||
|
||||
}, [itemPerRow, items])
|
||||
|
||||
const handleScroll = (e: ListOnScrollProps) => {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { ConfigurationService } from "./configuration.service";
|
||||
import { map, last, catchError } from "rxjs/operators";
|
||||
import { ProgressionInterface } from "shared/models/progress-bar";
|
||||
import { FolderLinkState, VersionFolderLinkerService } from "./version-folder-linker.service";
|
||||
import { SongDetails } from "shared/models/maps";
|
||||
|
||||
export class MapsManagerService {
|
||||
private static instance: MapsManagerService;
|
||||
@@ -145,6 +146,10 @@ export class MapsManagerService {
|
||||
});
|
||||
}
|
||||
|
||||
public getMapsInfoFromHashs(hashs: string[]): Observable<SongDetails[]> {
|
||||
return this.ipcService.sendV2("get-maps-info-from-cache", hashs);
|
||||
}
|
||||
|
||||
public async isDeepLinksEnabled(): Promise<boolean> {
|
||||
return lastValueFrom(this.ipcService.sendV2("is-map-deep-links-enabled"));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
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 { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
|
||||
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 });
|
||||
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;
|
||||
}
|
||||
|
||||
public static fromBsmLocalMap(map: BsmLocalMap): MapItemComponentProps<BsmLocalMap> {
|
||||
return {
|
||||
hash: map.hash,
|
||||
title: map.rawInfo._songName,
|
||||
coverUrl: map.coverUrl,
|
||||
songUrl: map.songUrl,
|
||||
autor: map.rawInfo._levelAuthorName,
|
||||
songAutor: map.rawInfo._songAuthorName,
|
||||
bpm: map.rawInfo._beatsPerMinute,
|
||||
duration: map.songDetails?.duration,
|
||||
diffs: MapItemComponentPropsMapper.extractMapDiffs({ rawMapInfo: map.rawInfo, songDetails: map.songDetails }),
|
||||
mapId: map.songDetails?.id,
|
||||
ranked: map.songDetails?.ranked,
|
||||
autorId: map.songDetails?.uploader.id,
|
||||
likes: map.songDetails?.upVotes,
|
||||
createdAt: map.songDetails?.uploadedAt,
|
||||
callBackParam: map
|
||||
}
|
||||
}
|
||||
|
||||
public static fromBsvMapDetail(map: BsvMapDetail): MapItemComponentProps<BsvMapDetail> {
|
||||
return {
|
||||
autor: map.metadata.levelAuthorName,
|
||||
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,
|
||||
likes: map.stats.upvotes,
|
||||
mapId: map.id,
|
||||
ranked: map.ranked,
|
||||
title: map.name,
|
||||
songAutor: map.metadata.songAuthorName,
|
||||
diffs: MapItemComponentPropsMapper.extractMapDiffs({ bsvMap: map }),
|
||||
songUrl: map.versions.at(0).previewURL,
|
||||
callBackParam: map
|
||||
}
|
||||
}
|
||||
|
||||
public static fromSongDetails(song: SongDetails): MapItemComponentProps<SongDetails> {
|
||||
return {
|
||||
autor: song.uploader.name,
|
||||
autorId: song.uploader.id,
|
||||
createdAt: song.uploadedAt,
|
||||
duration: song.duration,
|
||||
hash: song.hash,
|
||||
likes: song.upVotes,
|
||||
mapId: song.id,
|
||||
ranked: song.ranked,
|
||||
title: song.name,
|
||||
diffs: MapItemComponentPropsMapper.extractMapDiffs({ songDetails: song }),
|
||||
callBackParam: song
|
||||
}
|
||||
}
|
||||
|
||||
public static from(mapDetails: BsmLocalMap|BsvMapDetail|SongDetails): MapItemComponentProps<BsmLocalMap|BsvMapDetail|SongDetails> {
|
||||
if ((mapDetails as BsmLocalMap).rawInfo) {
|
||||
return MapItemComponentPropsMapper.fromBsmLocalMap(mapDetails as BsmLocalMap) as MapItemComponentProps<BsmLocalMap|BsvMapDetail|SongDetails>;
|
||||
} else if ((mapDetails as BsvMapDetail).metadata) {
|
||||
return MapItemComponentPropsMapper.fromBsvMapDetail(mapDetails as BsvMapDetail) as MapItemComponentProps<BsmLocalMap|BsvMapDetail|SongDetails>;;
|
||||
} else {
|
||||
return MapItemComponentPropsMapper.fromSongDetails(mapDetails as SongDetails) as MapItemComponentProps<BsmLocalMap|BsvMapDetail|SongDetails>;;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { Progression } from "main/helpers/fs.helpers";
|
||||
import { Observable } from "rxjs";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { BSLaunchEventData, LaunchOption } from "shared/models/bs-launch";
|
||||
import { BsvMapDetail } from "shared/models/maps";
|
||||
import { BsvMapDetail, SongDetails } from "shared/models/maps";
|
||||
import { BsmLocalMap, BsmLocalMapsProgress, DeleteMapsProgress } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { BsvPlaylist, BsvPlaylistPage, PlaylistSearchParams, SearchParams } from "../maps/beat-saver.model";
|
||||
import { ImportVersionOptions } from "main/services/bs-local-version.service";
|
||||
@@ -58,6 +58,7 @@ export interface IpcChannelMapping {
|
||||
"register-maps-deep-link": { request: void, response: boolean };
|
||||
"unregister-maps-deep-link": { request: void, response: boolean };
|
||||
"is-map-deep-links-enabled": { request: void, response: boolean };
|
||||
"get-maps-info-from-cache": { request: string[], response: SongDetails[] }
|
||||
|
||||
/* ** bs-model-ipcs ** */
|
||||
"one-click-install-model": { request: MSModel, response: void };
|
||||
|
||||
@@ -16,7 +16,8 @@ export interface UploadersList {
|
||||
// SongDetails Message
|
||||
export interface RawSongDetails {
|
||||
idInt: number;
|
||||
hash: string;
|
||||
hashIndices: number[];
|
||||
name: string;
|
||||
duration: number;
|
||||
uploaderRef: UploaderRef;
|
||||
uploadedAt: number;
|
||||
|
||||
@@ -7,6 +7,8 @@ export abstract class RawSongDetailsDeserializer {
|
||||
public static uploaderList: UploadersList = { names: [], ids: [] };
|
||||
public static difficultyLabels: string[] = [];
|
||||
|
||||
private static readonly HASH_CHARS = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"];
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static setUploadersList(uploadersList: UploadersList): void {
|
||||
@@ -135,6 +137,10 @@ export abstract class RawSongDetailsDeserializer {
|
||||
return rawMapId.toString(16);
|
||||
}
|
||||
|
||||
private static deserializeHashIndices(hashIndices: number[]): string {
|
||||
return hashIndices.map(index => this.HASH_CHARS[index]).join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize the raw song details to the song details model.
|
||||
* @param {RawSongDetails} rawSongDetails the raw song details to deserialize (normally get from the proto message)
|
||||
@@ -148,7 +154,8 @@ export abstract class RawSongDetailsDeserializer {
|
||||
|
||||
return {
|
||||
id: this.deserializeMapId(rawSongDetails.idInt),
|
||||
hash: rawSongDetails.hash,
|
||||
hash: this.deserializeHashIndices(rawSongDetails.hashIndices),
|
||||
name: rawSongDetails.name,
|
||||
duration: rawSongDetails.duration,
|
||||
uploader: this.deserializeRawUploader(rawSongDetails.uploaderRef),
|
||||
uploadedAt: rawSongDetails.uploadedAt,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { MapTag } from "../beat-saver.model";
|
||||
export interface SongDetails {
|
||||
id: string;
|
||||
hash: string;
|
||||
name: string;
|
||||
duration: number;
|
||||
uploader: SongUploader;
|
||||
uploadedAt: number;
|
||||
|
||||
Reference in New Issue
Block a user