mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
[feature-107] open playlist details from download modal + use new proto format for maps details cache
This commit is contained in:
@@ -6,13 +6,20 @@ message SongDetailsCache {
|
||||
repeated SongDetails songs = 1;
|
||||
uint32 lastUpdated = 2;
|
||||
uint32 total = 3;
|
||||
UploadersList uploaders = 4;
|
||||
repeated string difficultyLabels = 5;
|
||||
}
|
||||
|
||||
message UploadersList {
|
||||
repeated string names = 1;
|
||||
repeated uint32 ids = 2;
|
||||
}
|
||||
|
||||
message SongDetails {
|
||||
uint32 idInt = 1;
|
||||
string hash = 2;
|
||||
uint32 duration = 3;
|
||||
Uploader uploader = 4;
|
||||
UploaderRef uploaderRef = 4;
|
||||
uint32 uploadedAt = 5;
|
||||
repeated MapTag tags = 6;
|
||||
bool ranked = 7;
|
||||
@@ -31,7 +38,7 @@ message SongDetails {
|
||||
message Difficulty {
|
||||
DifficultyLabel difficulty = 1;
|
||||
DifficultyCharacteristic characteristic = 2;
|
||||
string label = 3;
|
||||
uint32 labelIndex = 3;
|
||||
uint32 starsT100 = 4;
|
||||
uint32 starsBlT100 = 5;
|
||||
uint32 njsT100 = 6;
|
||||
@@ -46,10 +53,9 @@ message Difficulty {
|
||||
uint32 obstacles = 15;
|
||||
}
|
||||
|
||||
message Uploader {
|
||||
string name = 1;
|
||||
uint32 id = 2;
|
||||
bool verified = 3;
|
||||
message UploaderRef {
|
||||
uint32 uploaderRefIndex = 1;
|
||||
bool verified = 2;
|
||||
}
|
||||
|
||||
enum DifficultyLabel {
|
||||
|
||||
Generated
+15
-12
@@ -27,7 +27,7 @@
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-updater": "^6.1.8",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"framer-motion": "^11.0.6",
|
||||
"framer-motion": "^11.2.6",
|
||||
"fs-extra": "^11.2.0",
|
||||
"got": "^14.2.0",
|
||||
"history": "^5.3.0",
|
||||
@@ -38,7 +38,7 @@
|
||||
"node-fetch": "^3.3.2",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"pako": "^2.1.0",
|
||||
"protobufjs": "^7.2.6",
|
||||
"protobufjs": "^7.3.0",
|
||||
"qrcode.react": "^3.1.0",
|
||||
"query-process": "^0.0.3",
|
||||
"react": "^18.2.0",
|
||||
@@ -1416,6 +1416,7 @@
|
||||
"version": "0.8.8",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@emotion/memoize": "0.7.4"
|
||||
}
|
||||
@@ -1423,7 +1424,8 @@
|
||||
"node_modules/@emotion/memoize": {
|
||||
"version": "0.7.4",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.20.1",
|
||||
@@ -12960,20 +12962,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/framer-motion": {
|
||||
"version": "11.0.6",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.0.6.tgz",
|
||||
"integrity": "sha512-BpO3mWF8UwxzO3Ca5AmSkrg14QYTeJa9vKgoLOoBdBdTPj0e81i1dMwnX6EQJXRieUx20uiDBXq8bA6y7N6b8Q==",
|
||||
"version": "11.2.6",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.2.6.tgz",
|
||||
"integrity": "sha512-XUrjjBt57e5YoHQtjwc3eNchFBuHvIgN/cS8SC4oIaAn2J/0+bLanUxXizidJKZVeHJam/JrmMnPRjYMglVn5g==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@emotion/is-prop-valid": "^0.8.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/is-prop-valid": "*",
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/is-prop-valid": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
@@ -18753,9 +18756,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/protobufjs": {
|
||||
"version": "7.2.6",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.6.tgz",
|
||||
"integrity": "sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw==",
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.0.tgz",
|
||||
"integrity": "sha512-YWD03n3shzV9ImZRX3ccbjqLxj7NokGN0V/ESiBV5xWqrommYHYiihuIyavq03pWSGqlyvYUFmfoMKd+1rPA/g==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.2",
|
||||
|
||||
+2
-2
@@ -266,7 +266,7 @@
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-updater": "^6.1.8",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"framer-motion": "^11.0.6",
|
||||
"framer-motion": "^11.2.6",
|
||||
"fs-extra": "^11.2.0",
|
||||
"got": "^14.2.0",
|
||||
"history": "^5.3.0",
|
||||
@@ -277,7 +277,7 @@
|
||||
"node-fetch": "^3.3.2",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"pako": "^2.1.0",
|
||||
"protobufjs": "^7.2.6",
|
||||
"protobufjs": "^7.3.0",
|
||||
"qrcode.react": "^3.1.0",
|
||||
"query-process": "^0.0.3",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -23,3 +23,8 @@ ipc.on("bsv-search-playlist", (args, reply) => {
|
||||
const bsvService = BeatSaverService.getInstance();
|
||||
reply(from(bsvService.searchPlaylists(args)));
|
||||
});
|
||||
|
||||
ipc.on("bsv-get-playlist-details-by-id", (args, reply) => {
|
||||
const bsvService = BeatSaverService.getInstance();
|
||||
reply(from(bsvService.getPlaylistDetailsById(args.id, args.page)));
|
||||
});
|
||||
|
||||
@@ -77,6 +77,9 @@ export class SongDetailsCacheService {
|
||||
|
||||
const res: Record<string, SongDetails> = {};
|
||||
|
||||
RawSongDetailsDeserializer.setUploadersList(messageObj.uploaders);
|
||||
RawSongDetailsDeserializer.setDifficultyLabels(messageObj.difficultyLabels);
|
||||
|
||||
for(const rawSong of messageObj.songs){
|
||||
res[rawSong.hash.toLocaleLowerCase()] = RawSongDetailsDeserializer.deserialize(rawSong);
|
||||
}
|
||||
|
||||
@@ -95,4 +95,8 @@ export class BeatSaverApiService {
|
||||
url.search = new URLSearchParams(this.objectToStringRecord(search)).toString();
|
||||
return this.request.getJSON<PlaylistSearchResponse>(url.toString());
|
||||
}
|
||||
|
||||
public getPlaylistDetailsById(id: string, page = 0): Promise<BsvPlaylistPage> {
|
||||
return this.request.getJSON<BsvPlaylistPage>(`${this.bsaverApiUrl}/playlists/id/${id}/${page}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { splitIntoChunk } from "../../../../shared/helpers/array.helpers";
|
||||
import { BsvMapDetail } from "shared/models/maps";
|
||||
import { BsvPlaylist, PlaylistSearchParams, SearchParams } from "shared/models/maps/beat-saver.model";
|
||||
import { BsvPlaylist, BsvPlaylistPage, PlaylistSearchParams, SearchParams } from "shared/models/maps/beat-saver.model";
|
||||
import { BeatSaverApiService } from "./beat-saver-api.service";
|
||||
import log from "electron-log";
|
||||
|
||||
@@ -69,7 +69,15 @@ export class BeatSaverService {
|
||||
.then(res => res.docs)
|
||||
.catch(e => {
|
||||
log.error(e);
|
||||
return e;
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
public getPlaylistDetailsById(id: string, page = 0): Promise<BsvPlaylistPage> {
|
||||
return this.bsaverApi.getPlaylistDetailsById(id, page)
|
||||
.catch(e => {
|
||||
log.error(e);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { useService } from "renderer/hooks/use-service.hook";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import { SongDetailDiffCharactertistic, SongDiffName } from "shared/models/maps";
|
||||
import { useConstant } from "renderer/hooks/use-constant.hook";
|
||||
import { CalendarDateTime, getLocalTimeZone } from "@internationalized/date";
|
||||
|
||||
export type ParsedMapDiff = { type: SongDiffName; name: string; stars: number };
|
||||
|
||||
@@ -39,7 +40,7 @@ export type MapItemProps<T = unknown> = {
|
||||
bpm: number;
|
||||
duration: number;
|
||||
likes: number;
|
||||
createdAt: number;
|
||||
createdAt: number | CalendarDateTime;
|
||||
selected?: boolean;
|
||||
downloading?: boolean;
|
||||
showOwned?: boolean;
|
||||
@@ -72,12 +73,18 @@ export const MapItem = memo(({ hash, title, autor, songAutor, coverUrl, songUrl,
|
||||
|
||||
const songPlaying = useObservable(() => audioPlayer.playing$.pipe(map(playing => playing && audioPlayer.src === songUrl)));
|
||||
|
||||
console.log("aaa", autorId);
|
||||
|
||||
const MAP_DIFFICULTIES = useConstant(() => Object.values(SongDiffName))
|
||||
const previewUrl = mapId ? `https://allpoland.github.io/ArcViewer/?id=${mapId}` : null;
|
||||
const mapUrl = mapId ? `https://beatsaver.com/maps/${mapId}` : null;
|
||||
const authorUrl = autorId ? `https://beatsaver.com/profile/${autorId}` : null;
|
||||
const createdDate = createdAt ? dateFormat(createdAt * 1000, "d mmm yyyy") : null;
|
||||
const likesText = likes ? Intl.NumberFormat(undefined, { notation: "compact" }).format(likes).split(" ").join("") : null;
|
||||
const createdDate = useConstant(() => {
|
||||
if(!createdAt){ return null; }
|
||||
const date = typeof createdAt === "number" ? new Date(createdAt * 1000) : createdAt.toDate(getLocalTimeZone());
|
||||
return dateFormat(date, "d mmm yyyy");
|
||||
});
|
||||
|
||||
const durationText = (() => {
|
||||
if (!duration) {
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ import { LocalBPList, LocalBPListsDetails } from "shared/models/playlists/local-
|
||||
import { PlaylistItem } from "./playlist-item.component";
|
||||
import { useStateMap } from "renderer/hooks/use-state-map.hook";
|
||||
import { ModalExitCode, ModalService } from "renderer/services/modale.service";
|
||||
import { LocalPlaylistDetailsModal } from "renderer/components/modal/modal-types/playlist/local-playlist-details-modal.component";
|
||||
import { LocalPlaylistDetailsModal } from "renderer/components/modal/modal-types/playlist/playlist-details-modal/local-playlist-details-modal.component";
|
||||
import { InstalledMapsContext } from "../maps-playlists-panel.component";
|
||||
import { IpcService } from "renderer/services/ipc.service";
|
||||
import equal from "fast-deep-equal";
|
||||
|
||||
@@ -32,6 +32,7 @@ export type PlaylistItemComponentProps = {
|
||||
onClickOpenFile?: () => void;
|
||||
onClickDelete?: () => void;
|
||||
onClickSync?: () => void;
|
||||
onClickDownload?: () => void;
|
||||
onClickCancelDownload?: () => void;
|
||||
}
|
||||
|
||||
@@ -50,6 +51,7 @@ export function PlaylistItem({ title,
|
||||
onClickOpen,
|
||||
onClickOpenFile,
|
||||
onClickSync,
|
||||
onClickDownload,
|
||||
onClickDelete,
|
||||
onClickCancelDownload
|
||||
}: PlaylistItemComponentProps) {
|
||||
@@ -80,7 +82,7 @@ export function PlaylistItem({ title,
|
||||
return (
|
||||
<motion.li className='relative flex-grow basis-0 min-w-80 h-28 cursor-pointer group' onHoverStart={() => setHovered(() => true)} onHoverEnd={() => setHovered(() => false)} >
|
||||
<GlowEffect visible={selected || hovered}/>
|
||||
<div className="size-full relative flex flex-row justify-start items-center overflow-hidden rounded-md *:z-[1]">
|
||||
<div className="size-full relative flex flex-row justify-start items-center overflow-hidden bg-black rounded-md *:z-[1]">
|
||||
<div className="absolute inset-0 flex justify-center items-center z-0">
|
||||
<BsmImage className="size-full object-cover saturate-150 blur-lg" image={coverUrl} base64={coverBase64} />
|
||||
<div className="absolute inset-0 bg-black opacity-20"/>
|
||||
@@ -136,6 +138,23 @@ export function PlaylistItem({ title,
|
||||
</Tippy>
|
||||
) : (<></>)
|
||||
|
||||
)}
|
||||
{onClickDownload && (
|
||||
isDownloading ? (
|
||||
<BsmBasicSpinner className="hover:!bg-main-color-1" spinnerClassName="brightness-75 dark:brightness-200" style={{ color }} thikness="3px"/>
|
||||
) : !isInQueue ? (
|
||||
<Tippy content="Télécharger la playlist" placement="left" theme="default">
|
||||
<BsmButton
|
||||
icon="download"
|
||||
className="hover:!bg-main-color-1"
|
||||
iconClassName="size-full brightness-75 dark:brightness-200"
|
||||
style={{color}}
|
||||
onClick={onClickDownload}
|
||||
withBar={false}
|
||||
/>
|
||||
</Tippy>
|
||||
) : (<></>)
|
||||
|
||||
)}
|
||||
{onClickOpenFile && <Tippy content="Afficher le fichier" placement="left" theme="default">
|
||||
<BsmButton
|
||||
|
||||
@@ -21,6 +21,7 @@ import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { extractMapDiffs } from "renderer/components/maps-playlists-panel/maps/maps-row.component";
|
||||
import { useConstant } from "renderer/hooks/use-constant.hook";
|
||||
import { getLocalTimeZone, parseAbsolute, toCalendarDateTime } from "@internationalized/date";
|
||||
|
||||
export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; ownedMaps: BsmLocalMap[] }> = ({ options: {data : { ownedMaps, version }} }) => {
|
||||
const beatSaver = useService(BeatSaverService);
|
||||
@@ -131,7 +132,7 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
|
||||
autorId={map.uploader.id}
|
||||
bpm={map.metadata.bpm}
|
||||
coverUrl={map.versions.at(0).coverURL}
|
||||
createdAt={map.createdAt}
|
||||
createdAt={map.createdAt && toCalendarDateTime(parseAbsolute(map.createdAt, getLocalTimeZone()))}
|
||||
duration={map.metadata.duration}
|
||||
hash={map.versions.at(0).hash}
|
||||
likes={map.stats.upvotes}
|
||||
|
||||
+19
-9
@@ -5,8 +5,8 @@ import { cn } from "renderer/helpers/css-class.helpers";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { PlaylistSearchParams } from "shared/models/maps/beat-saver.model"
|
||||
import {DateRangePicker} from "@nextui-org/date-picker";
|
||||
import { DateValue, Radio, RadioGroup, RadioProps, RangeValue } from "@nextui-org/react";
|
||||
import { today, getLocalTimeZone, parseAbsoluteToLocal, now } from "@internationalized/date";
|
||||
import { Radio, RadioGroup, RadioProps, RangeValue } from "@nextui-org/react";
|
||||
import { today, getLocalTimeZone, parseAbsoluteToLocal, toCalendarDate, toZoned, CalendarDate } from "@internationalized/date";
|
||||
import { motion } from "framer-motion";
|
||||
import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
@@ -26,27 +26,37 @@ type Props = {
|
||||
const MAX_NPS = 17;
|
||||
const MIN_NPS = 0;
|
||||
|
||||
// TODO : Translate
|
||||
|
||||
export function DownloadPlaylistFilterPanel({ className, params, onChange, onSubmit }: Props) {
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
const timeZone = useConstant(() => getLocalTimeZone());
|
||||
const [curated, setCurated] = useState<boolean>(params?.curated);
|
||||
const [verified, setVerified] = useState<boolean>(params?.verified);
|
||||
const [includeEmpty, setIncludeEmpty] = useState<boolean>(params?.includeEmpty);
|
||||
const [minNps, setMinNps] = useState<number>(params?.minNps);
|
||||
const [maxNps, setMaxNps] = useState<number>(params?.maxNps);
|
||||
const [dateRange, setDateRange] = useState<RangeValue<DateValue>>({
|
||||
start: null,
|
||||
end: null,
|
||||
const [dateRange, setDateRange] = useState<RangeValue<CalendarDate>>({
|
||||
start: params?.from ? toCalendarDate(parseAbsoluteToLocal(params.from)) : null,
|
||||
end: params?.from ? toCalendarDate(parseAbsoluteToLocal(params.from)) : null,
|
||||
});
|
||||
|
||||
const filterValue: FilterPlaylistSearchParams = { curated, verified, includeEmpty, minNps, maxNps, from: dateRange.start?.toString(), to: dateRange.end?.toString() };
|
||||
const filterValue: FilterPlaylistSearchParams = {
|
||||
curated,
|
||||
verified,
|
||||
includeEmpty, minNps,
|
||||
maxNps,
|
||||
from: dateRange?.start ? toZoned(dateRange.start, timeZone).set({ hour: 0, minute: 0 }).toAbsoluteString() : undefined,
|
||||
to: dateRange?.end ? toZoned(dateRange.end, timeZone).set({ hour: 23, minute: 59, second: 59 }).toAbsoluteString() : undefined,
|
||||
};
|
||||
const firstFilterValue = useConstant(() => ({...filterValue}));
|
||||
const filterHasBeenChanged = !equal(firstFilterValue, filterValue);
|
||||
|
||||
useOnUpdate(() => {
|
||||
onChange?.(filterValue);
|
||||
}, [curated, verified, includeEmpty, minNps, maxNps, dateRange.end, dateRange.start]);
|
||||
}, [curated, verified, includeEmpty, minNps, maxNps, dateRange]);
|
||||
|
||||
const handleOnNpsChange = ([min, max]: number[]) => {
|
||||
setMinNps(() => min <= MIN_NPS ? undefined : min);
|
||||
@@ -92,11 +102,11 @@ export function DownloadPlaylistFilterPanel({ className, params, onChange, onSub
|
||||
return setDateRange(() => ({ start: null, end: null }));
|
||||
};
|
||||
|
||||
const end = today(getLocalTimeZone());
|
||||
const end = today(timeZone);
|
||||
|
||||
setDateRange(() => ({
|
||||
start: end.subtract({ [type]: value }),
|
||||
end: end.add({days: 1})
|
||||
end,
|
||||
}))
|
||||
};
|
||||
|
||||
|
||||
+34
-11
@@ -1,5 +1,5 @@
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook"
|
||||
import { ModalComponent } from "renderer/services/modale.service"
|
||||
import { ModalComponent, ModalService } from "renderer/services/modale.service"
|
||||
import { Observable, lastValueFrom } from "rxjs"
|
||||
import { BSVersion } from "shared/bs-version.interface"
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"
|
||||
@@ -14,6 +14,11 @@ import { PlaylistItem } from "renderer/components/maps-playlists-panel/playlists
|
||||
import { PlaylistItemComponentPropsMapper } from "shared/mappers/playlist/playlist-item-component-props.mapper"
|
||||
import { motion } from "framer-motion"
|
||||
import { PlaylistDownloaderService } from "renderer/services/playlist-downloader.service"
|
||||
import { BsvPlaylistDetailsModal } from "../playlist-details-modal/bsv-playlist-details-modal.component"
|
||||
import { BsmImage } from "renderer/components/shared/bsm-image.component"
|
||||
import BeatWaiting from "../../../../../../../assets/images/apngs/beat-waiting.png"
|
||||
import BeatConflict from "../../../../../../../assets/images/apngs/beat-conflict.png"
|
||||
import { cn } from "renderer/helpers/css-class.helpers"
|
||||
|
||||
// TODO : Translate
|
||||
|
||||
@@ -21,14 +26,15 @@ export const DownloadPlaylistModal: ModalComponent<void, {version: BSVersion, ow
|
||||
{ resolver, options: { data: { version, ownedPlaylists$, ownedMaps$ }} }
|
||||
) => {
|
||||
|
||||
const modal = useService(ModalService);
|
||||
const beatSaver = useService(BeatSaverService);
|
||||
const playlistDownloader = useService(PlaylistDownloaderService);
|
||||
|
||||
const [playlists, setPlaylists] = useState<BsvPlaylist[]>([]);
|
||||
const [playlists, setPlaylists] = useState<BsvPlaylist[]>(null);
|
||||
const ownedPlaylists = useObservable(() => ownedPlaylists$, []);
|
||||
const ownedMaps = useObservable(() => ownedMaps$, []);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
const [searchParams, setSearchParams] = useState<PlaylistSearchParams>({
|
||||
q: "",
|
||||
sortOrder: BsvSearchOrder.Relevance,
|
||||
@@ -36,11 +42,9 @@ export const DownloadPlaylistModal: ModalComponent<void, {version: BSVersion, ow
|
||||
});
|
||||
|
||||
useOnUpdate(() => {
|
||||
setLoading(() => true);
|
||||
beatSaver.searchPlaylists(searchParams)
|
||||
.then(playlists => setPlaylists(prev => [...prev, ...(playlists ?? [])]))
|
||||
.catch(console.error)
|
||||
.finally(() => setLoading(() => false));
|
||||
.then(playlists => setPlaylists(prev => [...(prev ?? []), ...(playlists ?? [])] ))
|
||||
.catch(() => setError(() => true));
|
||||
}, [searchParams])
|
||||
|
||||
const handleNewSearch = (value: Omit<PlaylistSearchParams, "page">) => {
|
||||
@@ -52,15 +56,33 @@ export const DownloadPlaylistModal: ModalComponent<void, {version: BSVersion, ow
|
||||
setSearchParams(prev => ({ ...prev, page: prev.page + 1 }));
|
||||
};
|
||||
|
||||
const openPlaylist = (playlist: BsvPlaylist) => {
|
||||
modal.openModal(BsvPlaylistDetailsModal, { data: { playlist, version, installedMaps$: ownedMaps$ }, noStyle: true })
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-[95vw] w-[970px] h-[85vh] flex flex-col gap-3">
|
||||
<DownloadPlaylistModalHeader className="h-9 w-full" value={searchParams} onSubmit={handleNewSearch}/>
|
||||
{(() => {
|
||||
if(loading && playlists.length === 0){
|
||||
return <div className="flex justify-center items-center h-full w-full">Loading...</div>
|
||||
if(!Array.isArray(playlists)){
|
||||
return (
|
||||
<div className="w-full flex flex-col justify-center items-center mt-44">
|
||||
<BsmImage className={cn(["size-32", !error && "spin-loading"])} image={error ? BeatConflict : BeatWaiting} />
|
||||
<p className="text-lg font-bold px-10 text-center">
|
||||
{error ? "Une erreur est survenue lors du chargement des playlists" : "Chargement des playlists..."}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
else if(playlists.length === 0){
|
||||
return <div className="flex justify-center items-center h-full w-full">No playlists found</div>
|
||||
return (
|
||||
<div className="w-full flex flex-col justify-center items-center mt-44">
|
||||
<BsmImage className="size-32" image={BeatConflict} />
|
||||
<p className="text-lg font-bold px-10 text-center">
|
||||
Aucune playlist trouvée
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ul className="p-2 size-full flex flex-row flex-wrap justify-start content-start gap-3 grow overflow-y-scroll overflow-x-hidden z-[1]">
|
||||
@@ -68,7 +90,8 @@ export const DownloadPlaylistModal: ModalComponent<void, {version: BSVersion, ow
|
||||
<PlaylistItem
|
||||
key={playlist.playlistId}
|
||||
{...PlaylistItemComponentPropsMapper.fromBsvPlaylist(playlist)}
|
||||
onClickSync={() => lastValueFrom(playlistDownloader.downloadPlaylist({ downloadSource: playlist.downloadURL, ignoreSongsHashs: ownedMaps.map(map => map.hash), version }))}
|
||||
onClickOpen={() => openPlaylist(playlist)}
|
||||
onClickDownload={() => lastValueFrom(playlistDownloader.downloadPlaylist({ downloadSource: playlist.downloadURL, ignoreSongsHashs: ownedMaps.map(map => map.hash), version }))}
|
||||
/>
|
||||
))}
|
||||
<motion.span className="block w-full h-8" onViewportEnter={loadMorePlaylists}/>
|
||||
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
import { BsvMapDetail, BsvPlaylist } from "shared/models/maps/beat-saver.model";
|
||||
import { PlaylistDetailsTemplate } from "./playlist-details-template.component";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { Observable } from "rxjs";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { ModalComponent, ModalExitCode } from "renderer/services/modale.service";
|
||||
import { useState } from "react";
|
||||
import { MapItem } from "renderer/components/maps-playlists-panel/maps/map-item.component";
|
||||
import { extractMapDiffs } from "renderer/components/maps-playlists-panel/maps/maps-row.component";
|
||||
import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { BeatSaverService } from "renderer/services/thrird-partys/beat-saver.service";
|
||||
import { getLocalTimeZone, parseAbsolute, toCalendarDateTime } from "@internationalized/date";
|
||||
import { motion } from "framer-motion";
|
||||
import { MapsDownloaderService } from "renderer/services/maps-downloader.service";
|
||||
import equal from "fast-deep-equal";
|
||||
import BeatWaiting from "../../../../../../../assets/images/apngs/beat-waiting.png"
|
||||
import BeatConflict from "../../../../../../../assets/images/apngs/beat-conflict.png"
|
||||
import { BsmImage } from "renderer/components/shared/bsm-image.component";
|
||||
import { cn } from "renderer/helpers/css-class.helpers";
|
||||
|
||||
type Props = {
|
||||
version: BSVersion;
|
||||
playlist: BsvPlaylist;
|
||||
installedMaps$: Observable<BsmLocalMap[]>;
|
||||
}
|
||||
|
||||
// TODO : Translate
|
||||
|
||||
export const BsvPlaylistDetailsModal: ModalComponent<void, Props> = ({ resolver, options: { data: { playlist, installedMaps$, version } } }) => {
|
||||
|
||||
const beatsaver = useService(BeatSaverService);
|
||||
const mapsDownloader = useService(MapsDownloaderService);
|
||||
|
||||
const downloadingMaps = useObservable(() => mapsDownloader.mapsInQueue$, []);
|
||||
const installedMaps = useObservable(() => installedMaps$, null);
|
||||
const [page, setPage] = useState(0);
|
||||
const [playlistMaps, setPlaylistMaps] = useState<BsvMapDetail[]>(null);
|
||||
const [error, setError] = useState<boolean>(false);
|
||||
|
||||
useOnUpdate(() => {
|
||||
beatsaver.getPlaylistDetailsById(`${playlist.playlistId}`, page)
|
||||
.then(playlistDetails => setPlaylistMaps(prev => [...(prev ?? []), ...(playlistDetails?.maps?.map(map => map.map) ?? [])]))
|
||||
.catch(() => setError(() => true));
|
||||
}, [page]);
|
||||
|
||||
return (
|
||||
<PlaylistDetailsTemplate
|
||||
title={playlist.name}
|
||||
author={playlist.owner.name}
|
||||
description={playlist.description}
|
||||
duration={playlist.stats.totalDuration}
|
||||
imageUrl={playlist.playlistImage}
|
||||
maxNps={playlist.stats.maxNps}
|
||||
minNps={playlist.stats.minNps}
|
||||
nbMappers={playlist.stats.mapperCount}
|
||||
nbMaps={playlist.stats.totalMaps}
|
||||
onClose={() => resolver({ exitCode: ModalExitCode.CLOSED })}
|
||||
>
|
||||
{(() => {
|
||||
|
||||
if(!Array.isArray(playlistMaps)) {
|
||||
return (
|
||||
<div className="w-full flex flex-col justify-center items-center mt-10">
|
||||
<BsmImage className={cn(["size-32", !error && "spin-loading"])} image={error ? BeatConflict : BeatWaiting} />
|
||||
<p className="text-lg font-bold px-10 text-center">
|
||||
{error ? "Une erreur est survenue lors du chargement de la playlist" : "Chargement des maps..."}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if(playlistMaps.length === 0) {
|
||||
return (
|
||||
<div className="w-full flex flex-col justify-center items-center mt-10">
|
||||
<BsmImage className="size-32" image={BeatConflict} />
|
||||
<p className="text-lg font-bold px-10 text-center">
|
||||
Aucune map trouvée pour cette playlist
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
+12
-8
@@ -1,6 +1,6 @@
|
||||
import { ModalComponent } from "renderer/services/modale.service"
|
||||
import { PlaylistDetailsTemplate, PlaylistDetailsTemplateProps } from "./playlist-details-template.component"
|
||||
import { Observable, combineLatest, first, lastValueFrom, map, mergeAll, mergeMap, shareReplay, switchMap, take, tap } from "rxjs"
|
||||
import { PlaylistDetailsTemplate } from "./playlist-details-template.component"
|
||||
import { Observable, combineLatest, lastValueFrom, map, switchMap } from "rxjs"
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
@@ -8,10 +8,9 @@ import { MapItem } from "renderer/components/maps-playlists-panel/maps/map-item.
|
||||
import { extractMapDiffs } from "renderer/components/maps-playlists-panel/maps/maps-row.component";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { AudioPlayerService } from "renderer/services/audio-player.service";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { BsmImage } from "renderer/components/shared/bsm-image.component";
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import BeatConflict from "../../../../../../assets/images/apngs/beat-conflict.png";
|
||||
import BeatConflict from "../../../../../../../assets/images/apngs/beat-conflict.png";
|
||||
import { LocalBPListsDetails } from "shared/models/playlists/local-playlist.models";
|
||||
import { PlaylistDownloaderService } from "renderer/services/playlist-downloader.service";
|
||||
import { PlaylistHeaderState } from "./playlist-header-state.component";
|
||||
@@ -34,8 +33,8 @@ export const LocalPlaylistDetailsModal: ModalComponent<void, Props> = ({resolver
|
||||
const installedMaps = useObservable(() => options.data.installedMaps$, null);
|
||||
|
||||
const isMissingMaps$ = useConstant(() => combineLatest([options.data.installedMaps$, options.data.localPlaylist$]).pipe(map(([maps, playlist]) => maps.length !== playlist.songs.length)));
|
||||
const isPlaylistDownloading$ = useConstant(() => options.data.localPlaylist$.pipe(switchMap(playlist => playlistDownloader.$isPlaylistDownloading(playlist, options.data.version))));
|
||||
const isPlaylistInQueue$ = useConstant(() => options.data.localPlaylist$.pipe(switchMap(playlist => playlistDownloader.$isPlaylistInQueue(playlist, options.data.version))));
|
||||
const isPlaylistDownloading$ = useConstant(() => options.data.localPlaylist$.pipe(switchMap(playlist => playlistDownloader.$isPlaylistDownloading(playlist.path, options.data.version))));
|
||||
const isPlaylistInQueue$ = useConstant(() => options.data.localPlaylist$.pipe(switchMap(playlist => playlistDownloader.$isPlaylistInQueue(playlist.path, options.data.version))));
|
||||
|
||||
const isInQueue = useObservable(() => isPlaylistInQueue$, false);
|
||||
|
||||
@@ -50,7 +49,12 @@ export const LocalPlaylistDetailsModal: ModalComponent<void, Props> = ({resolver
|
||||
|
||||
const ignoreSongsHashs = installedMaps?.map(map => map.hash);
|
||||
|
||||
const obs$ = playlistDownloader.installPlaylist(localPlaylist, options.data.version, ignoreSongsHashs);
|
||||
const obs$ = playlistDownloader.downloadPlaylist({
|
||||
downloadSource: localPlaylist.path,
|
||||
dest: localPlaylist.path,
|
||||
version: options.data.version,
|
||||
ignoreSongsHashs,
|
||||
});
|
||||
|
||||
return lastValueFrom(obs$);
|
||||
}
|
||||
@@ -125,7 +129,7 @@ export const LocalPlaylistDetailsModal: ModalComponent<void, Props> = ({resolver
|
||||
<PlaylistDetailsTemplate
|
||||
author={localPlaylist?.playlistAuthor}
|
||||
description={localPlaylist?.playlistDescription}
|
||||
image={localPlaylist?.image}
|
||||
imagebase64={localPlaylist?.image}
|
||||
duration={localPlaylist?.duration}
|
||||
maxNps={localPlaylist?.maxNps}
|
||||
minNps={localPlaylist?.minNps}
|
||||
+12
-5
@@ -7,10 +7,13 @@ import { ClockIcon } from "renderer/components/svgs/icons/clock-icon.component";
|
||||
import { NpsIcon } from "renderer/components/svgs/icons/nps-icon.component";
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import { ThemeColorGradientSpliter } from "renderer/components/shared/theme-color-gradient-spliter.component";
|
||||
import { CrossIcon } from "renderer/components/svgs/icons/cross-icon.component";
|
||||
import { ModalComponent, ModalExitCode, ModalObject, ModalResponse } from "renderer/services/modale.service";
|
||||
|
||||
export type PlaylistDetailsTemplateProps = {
|
||||
title: string;
|
||||
image: string;
|
||||
imagebase64?: string;
|
||||
imageUrl?: string;
|
||||
author: string;
|
||||
description: string;
|
||||
nbMaps: number;
|
||||
@@ -19,9 +22,10 @@ export type PlaylistDetailsTemplateProps = {
|
||||
minNps: number;
|
||||
maxNps: number;
|
||||
children?: ReactNode;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export function PlaylistDetailsTemplate({title, image, author, description, nbMaps, duration, nbMappers, minNps, maxNps, children}: PlaylistDetailsTemplateProps) {
|
||||
export function PlaylistDetailsTemplate({title, imagebase64, imageUrl, author, description, nbMaps, duration, nbMappers, minNps, maxNps, children, onClose}: PlaylistDetailsTemplateProps) {
|
||||
|
||||
const color = useThemeColor("first-color");
|
||||
|
||||
@@ -43,11 +47,14 @@ export function PlaylistDetailsTemplate({title, image, author, description, nbMa
|
||||
return (
|
||||
<div className="flex flex-col w-screen max-w-2xl h-screen max-h-[calc(100vh-1.25rem)] translate-y-2.5 bg-main-color-1 rounded-t-lg overflow-hidden">
|
||||
<header className="shrink-0 relative h-36 overflow-hidden flex flex-row p-3">
|
||||
<BsmImage className="absolute top-0 left-0 size-full object-cover blur-xl scale-150 brightness-75 saturate-200" base64={image}/>
|
||||
<BsmImage className="h-full aspect-square object-cover z-[1] rounded-md shadow-black shadow-center" base64={image}/>
|
||||
<button className="absolute top-2 right-2 z-[1]" onClickCapture={onClose}>
|
||||
<CrossIcon className="size-3.5 text-gray-200"/>
|
||||
</button>
|
||||
<BsmImage className="absolute top-0 left-0 size-full object-cover blur-xl scale-150 brightness-75 saturate-200" base64={imagebase64} image={imageUrl}/>
|
||||
<BsmImage className="h-full aspect-square object-cover z-[1] rounded-md shadow-black shadow-center" base64={imagebase64} image={imageUrl}/>
|
||||
<div className="h-full px-3 text-white z-[1]">
|
||||
<h1 className="font-bold text-xl tracking-wide line-clamp-1 w-fit">{title}</h1>
|
||||
<p>{description}</p>
|
||||
<p className="line-clamp-2" title={description}>{description}</p>
|
||||
<p className="text-xs font-bold">Créé par <span className="brightness-200" style={{color}}>{author}</span></p>
|
||||
|
||||
<div className="flex flex-row flex-wrap w-full gap-2 mt-1">
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import { BsmImage } from "renderer/components/shared/bsm-image.component";
|
||||
import BeatConflict from "../../../../../../assets/images/apngs/beat-conflict.png";
|
||||
import BeatConflict from "../../../../../../../assets/images/apngs/beat-conflict.png";
|
||||
import { Observable } from "rxjs";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ModalExitCode, ModalObject, ModalService } from "renderer/services/modale.service";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import { useEffect } from "react";
|
||||
import { BsmIcon } from "../svgs/bsm-icon.component";
|
||||
import { ThemeColorGradientSpliter } from "../shared/theme-color-gradient-spliter.component";
|
||||
@@ -12,8 +11,6 @@ export function Modal() {
|
||||
const modals = useObservable(() => modalSevice.getModalToShow());
|
||||
const currentModal = modals?.at(-1);
|
||||
|
||||
const { firstColor, secondColor } = useThemeColor();
|
||||
|
||||
useEffect(() => {
|
||||
const onEscape = (e: KeyboardEvent) => {
|
||||
if (e.key !== "Escape") {
|
||||
@@ -61,7 +58,7 @@ export function Modal() {
|
||||
<AnimatePresence>
|
||||
{currentModal && <motion.span key="modal-overlay" onClick={() => currentModal.resolver({ exitCode: ModalExitCode.NO_CHOICE })} className="fixed size-full bg-black z-[90]" initial={{ opacity: 0 }} animate={{ opacity: currentModal && 0.6 }} exit={{ opacity: 0 }} transition={{ duration: 0.2 }} />}
|
||||
{modals?.map((modal, index) => (
|
||||
<motion.div className="fixed z-[90] left-1/2 top-1/2" key={index} initial={{ y: "100vh", x: "-50%" }} animate={{y: "-50%", scale: modal === currentModal ? 1 : 0, opacity: modal === currentModal ? 1 : 0}} exit={{ y: "100vh" }}>
|
||||
<motion.div key={index} className="fixed z-[90] top-1/2 left-1/2" initial={{ y: "100vh", x: "-50%" }} animate={{y: "-50%", scale: modal === currentModal ? 1 : 0, opacity: modal === currentModal ? 1 : 0, display: modal === currentModal ? "block" : ["block", "none"]}} exit={{ y: "100vh" }}>
|
||||
{renderModal(modal)}
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
@@ -10,10 +10,13 @@ type Props = {
|
||||
};
|
||||
|
||||
export function BsmLink({ className, href, children, style, internal }: Props) {
|
||||
|
||||
|
||||
const linkOpener = useService(LinkOpenerService);
|
||||
|
||||
const openLink = () => {
|
||||
|
||||
console.log("Opening link", href, internal);
|
||||
|
||||
if (!href) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BsvMapDetail } from "shared/models/maps";
|
||||
import { BsvPlaylist, PlaylistSearchParams, SearchParams } from "shared/models/maps/beat-saver.model";
|
||||
import { BsvPlaylist, BsvPlaylistPage, PlaylistSearchParams, SearchParams } from "shared/models/maps/beat-saver.model";
|
||||
import { IpcService } from "../ipc.service";
|
||||
import { lastValueFrom } from "rxjs";
|
||||
|
||||
@@ -33,4 +33,8 @@ export class BeatSaverService {
|
||||
public async searchPlaylists(search: PlaylistSearchParams): Promise<BsvPlaylist[]> {
|
||||
return lastValueFrom(this.ipc.sendV2("bsv-search-playlist", search));
|
||||
}
|
||||
|
||||
public async getPlaylistDetailsById(id: string, page = 0): Promise<BsvPlaylistPage> {
|
||||
return lastValueFrom(this.ipc.sendV2("bsv-get-playlist-details-by-id", { id, page }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { BSVersion } from "shared/bs-version.interface";
|
||||
import { BSLaunchEventData, LaunchOption } from "shared/models/bs-launch";
|
||||
import { BsvMapDetail } from "shared/models/maps";
|
||||
import { BsmLocalMap, BsmLocalMapsProgress, DeleteMapsProgress } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { BsvPlaylist, PlaylistSearchParams, SearchParams } from "../maps/beat-saver.model";
|
||||
import { BsvPlaylist, BsvPlaylistPage, PlaylistSearchParams, SearchParams } from "../maps/beat-saver.model";
|
||||
import { ImportVersionOptions } from "main/services/bs-local-version.service";
|
||||
import { DownloadInfo, DownloadSteamInfo } from "main/services/bs-version-download/bs-steam-downloader.service";
|
||||
import { DepotDownloaderEvent } from "../bs-version-download/depot-downloader.model";
|
||||
@@ -42,6 +42,7 @@ export interface IpcChannelMapping {
|
||||
"bsv-get-map-details-from-hashs": {request: string[], response: BsvMapDetail[]};
|
||||
"bsv-get-map-details-by-id": {request: string, response: BsvMapDetail};
|
||||
"bsv-search-playlist": {request: PlaylistSearchParams, response: BsvPlaylist[]};
|
||||
"bsv-get-playlist-details-by-id": {request: {id: string, page: number}, response: BsvPlaylistPage};
|
||||
|
||||
/* ** bs-launcher-ipcs ** */
|
||||
"create-launch-shortcut": { request: LaunchOption, response: boolean };
|
||||
|
||||
@@ -3,15 +3,22 @@
|
||||
export interface RawSongDetailsCache {
|
||||
songs: RawSongDetails[];
|
||||
lastUpdated: number;
|
||||
uploaders: UploadersList;
|
||||
difficultyLabels: string[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface UploadersList {
|
||||
names: string[];
|
||||
ids: number[];
|
||||
}
|
||||
|
||||
// SongDetails Message
|
||||
export interface RawSongDetails {
|
||||
idInt: number;
|
||||
hash: string;
|
||||
duration: number;
|
||||
uploader: RawUploader;
|
||||
uploaderRef: UploaderRef;
|
||||
uploadedAt: number;
|
||||
tags: RawMapTag[];
|
||||
ranked: boolean;
|
||||
@@ -31,7 +38,7 @@ export interface RawSongDetails {
|
||||
export interface RawDifficulty {
|
||||
difficulty: RawDifficultyLabel;
|
||||
characteristic: RawDifficultyCharacteristic;
|
||||
label: string;
|
||||
labelIndex: number;
|
||||
starsT100: number;
|
||||
starsBlT100: number;
|
||||
njsT100: number;
|
||||
@@ -47,9 +54,8 @@ export interface RawDifficulty {
|
||||
}
|
||||
|
||||
// Uploader Message
|
||||
export interface RawUploader {
|
||||
name: string;
|
||||
id: number;
|
||||
export interface UploaderRef {
|
||||
uploaderRefIndex: number;
|
||||
verified: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
import { MapStyle, MapTag, MapType } from "../beat-saver.model";
|
||||
import { RawDifficulty, RawDifficultyCharacteristic, RawDifficultyLabel, RawMapTag, RawSongDetails, RawUploader } from "./raw-song-details-cache.model";
|
||||
import { RawDifficulty, RawDifficultyCharacteristic, RawDifficultyLabel, RawMapTag, RawSongDetails, UploaderRef, UploadersList } from "./raw-song-details-cache.model";
|
||||
import { SongDetailDiffCharactertistic, SongDetails, SongDiffName, SongDifficulty, SongUploader } from "./song-details-cache.model";
|
||||
|
||||
export abstract class RawSongDetailsDeserializer {
|
||||
|
||||
public static uploaderList: UploadersList = { names: [], ids: [] };
|
||||
public static difficultyLabels: string[] = [];
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static setUploadersList(uploadersList: UploadersList): void {
|
||||
this.uploaderList = uploadersList;
|
||||
}
|
||||
|
||||
public static setDifficultyLabels(difficultyLabels: string[]): void {
|
||||
this.difficultyLabels = difficultyLabels;
|
||||
}
|
||||
|
||||
private static deserializeRawTag(rawTag: RawMapTag): MapTag {
|
||||
switch (rawTag) {
|
||||
case RawMapTag.DANCE: return MapStyle.Dance;
|
||||
@@ -88,7 +99,7 @@ export abstract class RawSongDetailsDeserializer {
|
||||
return {
|
||||
difficulty: this.deserializeRawDifficultyLabel(rawDifficulty.difficulty),
|
||||
characteristic: this.deserializeRawDifficultyCharacteristic(rawDifficulty.characteristic),
|
||||
label: rawDifficulty.label,
|
||||
label: RawSongDetailsDeserializer.difficultyLabels[rawDifficulty.labelIndex],
|
||||
stars: Math.round(rawDifficulty.starsT100) / 100,
|
||||
starsBL: Math.round(rawDifficulty.starsBlT100) / 100,
|
||||
njs: Math.round(rawDifficulty.njsT100) / 100,
|
||||
@@ -108,11 +119,12 @@ export abstract class RawSongDetailsDeserializer {
|
||||
return rawDifficulties.map(rawDifficulty => this.deserializeRawDifficulty(rawDifficulty));
|
||||
}
|
||||
|
||||
private static deserializeRawUploader(rawUploader: RawUploader): SongUploader {
|
||||
private static deserializeRawUploader(uploaderRef: UploaderRef): SongUploader {
|
||||
|
||||
return {
|
||||
name: rawUploader.name,
|
||||
id: rawUploader.id,
|
||||
verified: rawUploader.verified
|
||||
name: RawSongDetailsDeserializer.uploaderList.names[uploaderRef.uploaderRefIndex],
|
||||
id: RawSongDetailsDeserializer.uploaderList.ids[uploaderRef.uploaderRefIndex],
|
||||
verified: uploaderRef.verified
|
||||
};
|
||||
}
|
||||
|
||||
@@ -138,7 +150,7 @@ export abstract class RawSongDetailsDeserializer {
|
||||
id: this.deserializeMapId(rawSongDetails.idInt),
|
||||
hash: rawSongDetails.hash,
|
||||
duration: rawSongDetails.duration,
|
||||
uploader: this.deserializeRawUploader(rawSongDetails.uploader),
|
||||
uploader: this.deserializeRawUploader(rawSongDetails.uploaderRef),
|
||||
uploadedAt: rawSongDetails.uploadedAt,
|
||||
tags: this.deserializeRawTags(rawSongDetails.tags),
|
||||
ranked: rawSongDetails.ranked,
|
||||
|
||||
Reference in New Issue
Block a user