[feature-107] we can now download playlist from beatsaver + other internal changes

This commit is contained in:
MathieuG-P
2024-05-12 17:15:13 +02:00
parent c833df9884
commit c02ac39184
31 changed files with 391 additions and 193 deletions
+1 -1
View File
@@ -718,7 +718,7 @@
"ReleaseDate": "1712681263",
"year": "2024"
},
{
{
"BSVersion": "1.36.2",
"BSManifest": "4832237377879752064",
"OculusBinaryId": "6983660338400352",
+25 -2
View File
@@ -11,6 +11,7 @@
"@node-steam/vdf": "^2.2.0",
"@tippyjs/react": "^4.2.6",
"archiver": "^6.0.1",
"clsx": "^2.1.1",
"color": "^4.2.3",
"crypto-js": "^4.2.0",
"dateformat": "^5.0.3",
@@ -49,6 +50,7 @@
"semver": "^7.6.0",
"serialize-error": "^11.0.3",
"striptags": "^4.0.0-alpha.4",
"tailwind-merge": "^2.3.0",
"tailwind-scrollbar-hide": "^1.1.7",
"tailwindcss-scoped-groups": "^2.0.0",
"tippy.js": "^6.3.7",
@@ -740,8 +742,9 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.23.9",
"license": "MIT",
"version": "7.24.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz",
"integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@@ -6035,6 +6038,14 @@
"node": ">=4"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"engines": {
"node": ">=6"
}
},
"node_modules/co": {
"version": "4.6.0",
"dev": true,
@@ -17627,6 +17638,18 @@
"url": "https://opencollective.com/unts"
}
},
"node_modules/tailwind-merge": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.3.0.tgz",
"integrity": "sha512-vkYrLpIP+lgR0tQCG6AP7zZXCTLc1Lnv/CCRT3BqJ9CZ3ui2++GPaGb1x/ILsINIMSYqqvrpqjUFsMNLlW99EA==",
"dependencies": {
"@babel/runtime": "^7.24.1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
}
},
"node_modules/tailwind-scrollbar": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/tailwind-scrollbar/-/tailwind-scrollbar-3.1.0.tgz",
+2
View File
@@ -251,6 +251,7 @@
"@node-steam/vdf": "^2.2.0",
"@tippyjs/react": "^4.2.6",
"archiver": "^6.0.1",
"clsx": "^2.1.1",
"color": "^4.2.3",
"crypto-js": "^4.2.0",
"dateformat": "^5.0.3",
@@ -289,6 +290,7 @@
"semver": "^7.6.0",
"serialize-error": "^11.0.3",
"striptags": "^4.0.0-alpha.4",
"tailwind-merge": "^2.3.0",
"tailwind-scrollbar-hide": "^1.1.7",
"tailwindcss-scoped-groups": "^2.0.0",
"tippy.js": "^6.3.7",
+5 -1
View File
@@ -16,6 +16,10 @@ ipc.on("bsv-get-map-details-from-hashs", (args, reply) => {
ipc.on("bsv-get-map-details-by-id", (args, reply) => {
const bsvService = BeatSaverService.getInstance();
console.log("AAAAAAZEZE", args);
reply(from(bsvService.getMapDetailsById(args)));
});
ipc.on("bsv-search-playlist", (args, reply) => {
const bsvService = BeatSaverService.getInstance();
reply(from(bsvService.searchPlaylists(args)));
});
+7 -16
View File
@@ -6,6 +6,9 @@ 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";
const ipc = IpcService.getInstance();
@@ -29,28 +32,16 @@ ipc.on("is-playlists-deep-links-enabled", (args, reply) => {
reply(of(maps.isDeepLinksEnabled()));
});
ipc.on("install-playlist", (args, reply) => {
ipc.on("download-playlist", (args, reply) => {
const playlists = LocalPlaylistsManagerService.getInstance();
const isLocalBPList = (bpList: BPList): bpList is LocalBPList => {
return (bpList as LocalBPList).path !== undefined && path.isAbsolute((bpList as LocalBPList).path);
}
const playListUrl = (() => {
if (args.playlist.customData?.syncURL) {
return args.playlist.customData.syncURL;
}
if (isLocalBPList(args.playlist)) {
return args.playlist.path;
}
return undefined;
})();
const downloadUrl = isValidUrl(args.downloadSource) ? args.downloadSource : pathToFileURL(args.downloadSource).href;
return reply(playlists.downloadPlaylist({
bpListUrl: playListUrl,
bpListUrl: downloadUrl,
version: args.version,
ignoreSongsHashs: args.ignoreSongsHashs,
dest: (args.playlist as LocalBPList)?.path
dest: args.dest
}));
});
@@ -1,5 +1,5 @@
import { BsvMapDetail } from "shared/models/maps";
import { BsvPlaylist, BsvPlaylistPage, MapFilter, SearchParams, SearchResponse } from "shared/models/maps/beat-saver.model";
import { BsvPlaylist, BsvPlaylistPage, MapFilter, PlaylistSearchParams, PlaylistSearchResponse, SearchParams, SearchResponse } from "shared/models/maps/beat-saver.model";
import { RequestService } from "../../request.service";
export class BeatSaverApiService {
@@ -20,6 +20,10 @@ export class BeatSaverApiService {
this.request = RequestService.getInstance();
}
private objectToStringRecord(obj: Record<string, any>): Record<string, string> {
return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, String(value)] as [string, string]));
}
private mapFilterToUrlParams(filter: MapFilter): URLSearchParams {
if (!filter) {
return new URLSearchParams();
@@ -30,24 +34,7 @@ export class BeatSaverApiService {
const tags = enbledTagsString || excludedTagsString ? [...enbledTagsString, excludedTagsString].join("|") : null;
const params = {
...(filter.automapper && { automapper: String(filter.automapper) }),
...(filter.chroma && { chroma: String(filter.chroma) }),
...(filter.cinema && { cinema: String(filter.cinema) }),
...(filter.me && { me: String(filter.me) }),
...(filter.noodle && { noodle: String(filter.noodle) }),
...(filter.ranked && { ranked: String(filter.ranked) }),
...(filter.verified && { verified: String(filter.verified) }),
...(filter.curated && { curated: String(filter.curated) }),
...(filter.fullSpread && { fullSpread: String(filter.fullSpread) }),
...(filter.from && { from: String(filter.from) }),
...(filter.to && { to: String(filter.to) }),
...(tags && { tags: String(tags) }),
...(filter.minDuration && { minDuration: String(filter.minDuration) }),
...(filter.maxDuration && { maxDuration: String(filter.maxDuration) }),
...(filter.minNps && { minNps: String(filter.minNps) }),
...(filter.maxNps && { maxNps: String(filter.maxNps) }),
};
const params: Record<string, string> = this.objectToStringRecord({...filter, tags});
return new URLSearchParams(params);
}
@@ -95,14 +82,15 @@ export class BeatSaverApiService {
return this.request.getJSON<BsvMapDetail>(`${this.bsaverApiUrl}/maps/id/${id}`);
}
public async searchMaps(search: SearchParams): Promise<SearchResponse> {
public searchMaps(search: SearchParams): Promise<SearchResponse> {
const url = new URL(`${this.bsaverApiUrl}/search/text/${search?.page ?? 0}`);
url.search = this.searchParamsToUrlParams(search).toString();
return this.request.getJSON<SearchResponse>(url.toString());
}
public async getPlaylistDetails(id: string): Promise<BsvPlaylist> {
const res = await this.request.getJSON<BsvPlaylistPage>(`${this.bsaverApiUrl}/playlists/id/${id}/0`);
return res.playlist;
public searchPlaylists(search: PlaylistSearchParams): Promise<PlaylistSearchResponse> {
const url = new URL(`${this.bsaverApiUrl}/playlists/search/${search?.page ?? 0}`);
url.search = new URLSearchParams(this.objectToStringRecord(search)).toString();
return this.request.getJSON<PlaylistSearchResponse>(url.toString());
}
}
@@ -1,6 +1,6 @@
import { splitIntoChunk } from "../../../../shared/helpers/array.helpers";
import { BsvMapDetail } from "shared/models/maps";
import { BsvPlaylist, SearchParams } from "shared/models/maps/beat-saver.model";
import { BsvPlaylist, PlaylistSearchParams, SearchParams } from "shared/models/maps/beat-saver.model";
import { BeatSaverApiService } from "./beat-saver-api.service";
import log from "electron-log";
@@ -59,11 +59,17 @@ export class BeatSaverService {
})
.catch(e => {
log.error(e);
return [];
return e;
});
}
public async getPlaylistPage(id: string): Promise<BsvPlaylist> {
return this.bsaverApi.getPlaylistDetails(id);
public searchPlaylists(search: PlaylistSearchParams): Promise<BsvPlaylist[]> {
return this.bsaverApi
.searchPlaylists(search)
.then(res => res.docs)
.catch(e => {
log.error(e);
return e;
});
}
}
@@ -14,12 +14,13 @@ import { BsmButton } from "../shared/bsm-button.component";
import { MapIcon } from "../svgs/icons/map-icon.component";
import { PlaylistIcon } from "../svgs/icons/playlist-icon.component";
import { useObservable } from "renderer/hooks/use-observable.hook";
import { BehaviorSubject, Observable, of } from "rxjs";
import { BehaviorSubject, of } from "rxjs";
import { LocalPlaylistsListPanel } from "./playlists/local-playlists-list-panel.component";
import { PlaylistsManagerService } from "renderer/services/playlists-manager.service";
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
import { useConstant } from "renderer/hooks/use-constant.hook";
import { LocalBPListsDetails } from "shared/models/playlists/local-playlist.models";
import { PlaylistDownloaderService } from "renderer/services/playlist-downloader.service";
type Props = {
version?: BSVersion;
@@ -38,6 +39,7 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
const mapsService = useService(MapsManagerService);
const mapsDownloader = useService(MapsDownloaderService);
const playlistsService = useService(PlaylistsManagerService);
const playlistsDownloader = useService(PlaylistDownloaderService);
const t = useTranslation();
const [tabIndex, setTabIndex] = useState(0);
@@ -68,9 +70,12 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
return setPlaylistSearch(() => value);
};
const handleMapsAddClick = () => {
mapsDownloader.openDownloadMapModal(version, maps$.value);
};
const handleAddClick = () => {
switch (tabIndex) {
case 0: return mapsDownloader.openDownloadMapModal(version, maps$.value);
case 1: return playlistsDownloader.openDownloadPlaylistModal(version, playlists$, maps$);
}
}
const handleMapsLinkClick = () => {
if(mapsLinkedState === FolderLinkState.Pending || mapsLinkedState === FolderLinkState.Processing){ return Promise.resolve(false); }
@@ -111,8 +116,7 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
text="misc.add"
typeColor="primary"
withBar={false}
disabled={tabIndex === 1}
onClick={handleMapsAddClick}
onClick={handleAddClick}
/>
<div className="h-full rounded-full bg-light-main-color-2 dark:bg-main-color-2 grow p-[6px]">
<input type="text" className="h-full w-full bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-2" placeholder={t("pages.version-viewer.maps.search-bar.search-placeholder")} value={tabIndex === 0 ? mapSearch : playlistSearch} onChange={e => handleSearch(e.target.value)} tabIndex={-1} />
@@ -1,4 +1,4 @@
import { MapFilter, MapSpecificity, MapStyle, MapTag, MapType } from "shared/models/maps/beat-saver.model";
import { 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";
@@ -6,10 +6,7 @@ import { minToS } from "../../../../shared/helpers/time.helpers";
import dateFormat from "dateformat";
import { BsmRange } from "../../shared/bsm-range.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { MAP_SPECIFICITIES } from "renderer/partials/maps/map-general/map-specificity";
import { MAP_REQUIREMENTS } from "renderer/partials/maps/map-requirements/map-requirements";
import { MAP_DIFFICULTIES_COLORS } from "renderer/partials/maps/map-difficulties/map-difficulties-colors";
import { MapExclude, MAP_EXCLUDES } from "renderer/partials/maps/map-excludes/map-excludes";
import { MAP_DIFFICULTIES_COLORS } from "shared/models/maps/difficulties-colors";
import { BsmButton } from "../../shared/bsm-button.component";
import equal from "fast-deep-equal/es6";
import clone from "rfdc";
@@ -147,10 +144,6 @@ export function FilterPanel({ className, ref, playlist = false, filter, localDat
return t(`maps.map-specificities.${specificity}`);
};
const translateMapExclude = (exclude: MapExclude): string => {
return t(`maps.map-excludes.${exclude}`);
};
type BooleanKeys<T> = { [k in keyof T]: T[k] extends boolean ? k : never }[keyof T];
const handleCheckbox = (key: BooleanKeys<MapFilter>) => {
@@ -180,14 +173,14 @@ export function FilterPanel({ className, ref, playlist = false, filter, localDat
<div className="w-full h-full flex gap-x-2">
<section className="shrink-0">
<h2 className="mb-1 uppercase text-sm">{t("maps.map-filter-panel.specificities")}</h2>
{MAP_SPECIFICITIES.map(specificity => (
{Object.values(MapSpecificity).map(specificity => (
<div key={specificity} className="flex justify-start items-center h-[22px] z-20 relative py-0.5 cursor-pointer" onClick={() => handleCheckbox(specificity)}>
<BsmCheckbox className="h-full aspect-square relative bg-inherit mr-1" checked={filter?.[specificity]} onChange={() => handleCheckbox(specificity)} />
<span className="grow capitalize">{translateMapSpecificity(specificity)}</span>
</div>
))}
<h2 className="my-1 uppercase text-sm">{t("maps.map-filter-panel.requirements")}</h2>
{MAP_REQUIREMENTS.map(requirement => (
{Object.values(MapRequirement).map(requirement => (
<div key={requirement} className="flex justify-start items-center h-[22px] z-20 relative py-0.5 cursor-pointer" onClick={() => handleCheckbox(requirement)}>
<BsmCheckbox className="h-full aspect-square relative bg-inherit mr-1" checked={filter?.[requirement]} onChange={() => handleCheckbox(requirement)} />
<span className="grow capitalize">{requirement}</span>
@@ -196,12 +189,10 @@ export function FilterPanel({ className, ref, playlist = false, filter, localDat
{ !localData && (
<>
<h2 className="my-1 uppercase text-sm">{t("maps.map-filter-panel.exclude")}</h2>
{MAP_EXCLUDES.map(exclude => (
<div key={exclude} className="flex justify-start items-center h-[22px] z-20 relative py-0.5 cursor-pointer" onClick={() => handleCheckbox(exclude)}>
<BsmCheckbox className="h-full aspect-square relative bg-inherit mr-1" checked={filter?.[exclude]} onChange={() => handleCheckbox(exclude)} />
<span className="grow capitalize">{translateMapExclude(exclude)}</span>
</div>
))}
<div className="flex justify-start items-center h-[22px] z-20 relative py-0.5 cursor-pointer" onClick={() => handleCheckbox("installed")}>
<BsmCheckbox className="h-full aspect-square relative bg-inherit mr-1" checked={filter?.installed} onChange={() => handleCheckbox("installed")} />
<span className="grow capitalize">{t("maps.map-excludes.installed")}</span>
</div>
</>
)}
</section>
@@ -14,14 +14,14 @@ import equal from "fast-deep-equal/es6";
import { BsmBasicSpinner } from "../../shared/bsm-basic-spinner/bsm-basic-spinner.component";
import defaultImage from "../../../../../assets/images/default-version-img.jpg";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { MAP_DIFFICULTIES } from "renderer/partials/maps/map-difficulties/map-difficulties";
import { MAP_DIFFICULTIES_COLORS } from "renderer/partials/maps/map-difficulties/map-difficulties-colors";
import { MAP_DIFFICULTIES_COLORS } from "shared/models/maps/difficulties-colors";
import useDoubleClick from "use-double-click";
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 { SongDetailDiffCharactertistic, SongDiffName } from "shared/models/maps";
import { useConstant } from "renderer/hooks/use-constant.hook";
export type ParsedMapDiff = { type: SongDiffName; name: string; stars: number };
@@ -72,6 +72,7 @@ export const MapItem = memo(({ hash, title, autor, songAutor, coverUrl, songUrl,
const songPlaying = useObservable(() => audioPlayer.playing$.pipe(map(playing => playing && audioPlayer.src === songUrl)));
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;
@@ -23,6 +23,7 @@ import { ProgressBarService } from "renderer/services/progress-bar.service";
import { NotificationService } from "renderer/services/notification.service";
import { DeletePlaylistModal } from "renderer/components/modal/modal-types/playlist/delete-playlist-modal.component";
import { OsDiagnosticService } from "renderer/services/os-diagnostic.service";
import { PlaylistItemComponentPropsMapper } from "shared/mappers/playlist/playlist-item-component-props.mapper";
type Props = {
version: BSVersion;
@@ -78,6 +79,7 @@ export const LocalPlaylistsListPanel = forwardRef<unknown, Props>(({ version, cl
});
const onPlaylistDownloadedCB = (downloaded: LocalBPListsDetails) => {
console.log("onPlaylistDownloadedCB", downloaded);
const newPlaylist = (() => {
console.log(playlists);
const index = playlists$.value.findIndex(p => p.path === downloaded.path);
@@ -92,10 +94,10 @@ export const LocalPlaylistsListPanel = forwardRef<unknown, Props>(({ version, cl
setPlaylists(newPlaylist);
}
playlistDownloader.addOnPlaylistDownloadedListener(version, onPlaylistDownloadedCB);
const sub = playlistDownloader.currentDownload$.pipe(filter(download => download?.downloaded && equal(download?.info.version, version))).subscribe(download => onPlaylistDownloadedCB(download.downloaded));
return () => {
playlistDownloader.removeOnPlaylistDownloadedListener(version, onPlaylistDownloadedCB);
sub.unsubscribe();
}
}, [isActiveOnce, version, linked]);
@@ -104,7 +106,7 @@ export const LocalPlaylistsListPanel = forwardRef<unknown, Props>(({ version, cl
const ignoreSongsHashs = (maps$.value || []).map(m => m.hash.toLocaleLowerCase());
const obs$ = playlistDownloader.installPlaylist(playlist, version, ignoreSongsHashs);
const obs$ = playlistDownloader.downloadPlaylist({ downloadSource: playlist.customData?.syncURL ?? playlist.path, version, ignoreSongsHashs, dest: playlist.path });
return lastValueFrom(obs$).then(res => {
if(res.current === res.total){
@@ -142,46 +144,37 @@ export const LocalPlaylistsListPanel = forwardRef<unknown, Props>(({ version, cl
})
};
const render = () => {
if(playlistsLoading){
return (
<BsContentLoader className="w-full h-full flex justify-center flex-col items-center" value$={loadPercent$} text="aaaa"/>
)
}
if (playlists?.length){
return (
<ul className="relative size-full flex flex-row flex-wrap justify-start content-start p-3 gap-3">
{playlists.map(p =>
<PlaylistItem
key={p.path}
title={p.playlistTitle}
author={p.playlistAuthor}
coverBase64={p.image}
nbMaps={p.nbMaps}
nbMappers={p.nbMappers}
duration={p.duration}
maxNps={p.maxNps}
minNps={p.minNps}
isDownloading$={playlistDownloader.$isPlaylistDownloading(p, version)}
isInQueue$={playlistDownloader.$isPlaylistInQueue(p, version)}
onClickOpen={() => openPlaylistDetails(p.path)}
onClickDelete={() => deletePlaylist(p)}
onClickSync={isOnline && (() => installPlaylist(p))}
onClickOpenFile={() => viewPlaylistFile(p.path)}
onClickCancelDownload={() => playlistDownloader.cancelDownload(p, version)}
/>
)}
</ul>
)
}
return null;
}
return (
<div className={className}>
{render()}
{(() => {
if(playlistsLoading){
return (
<BsContentLoader className="w-full h-full flex justify-center flex-col items-center" value$={loadPercent$} text="aaaa"/>
)
}
if (playlists?.length){
return (
<ul className="relative size-full flex flex-row flex-wrap justify-start content-start p-3 gap-3">
{playlists.map(p =>
<PlaylistItem
key={p.path}
{...PlaylistItemComponentPropsMapper.fromLocalBPListDetails(p)}
isDownloading$={playlistDownloader.$isPlaylistDownloading(p.customData?.syncURL ?? p.path, version)}
isInQueue$={playlistDownloader.$isPlaylistInQueue(p.customData?.syncURL ?? p.path, version)}
onClickOpen={() => openPlaylistDetails(p.path)}
onClickDelete={() => deletePlaylist(p)}
onClickSync={isOnline && (() => installPlaylist(p))}
onClickOpenFile={() => viewPlaylistFile(p.path)}
onClickCancelDownload={() => playlistDownloader.cancelDownload(p.customData?.syncURL ?? p.path, version)}
/>
)}
</ul>
)
}
return <span>TODO</span>;
})()}
</div>
)
});
@@ -15,7 +15,7 @@ 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';
type Props = {
export type PlaylistItemComponentProps = {
title?: string;
author?: string;
coverBase64?: string;
@@ -52,7 +52,7 @@ export function PlaylistItem({ title,
onClickSync,
onClickDelete,
onClickCancelDownload
}: Props) {
}: PlaylistItemComponentProps) {
const color = useThemeColor("first-color");
@@ -80,8 +80,8 @@ 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">
<div className="absolute inset-0 flex justify-center items-center -z-[1]">
<div className="size-full relative flex flex-row justify-start items-center overflow-hidden 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"/>
</div>
@@ -6,12 +6,11 @@ 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";
import { useObservable } from "renderer/hooks/use-observable.hook";
import { BSV_SORT_ORDER } from "renderer/partials/beat-saver/sort-order";
import { BeatSaverService } from "renderer/services/thrird-partys/beat-saver.service";
import { MapsDownloaderService } from "renderer/services/maps-downloader.service";
import { ModalComponent } from "renderer/services/modale.service";
import { BSVersion } from "shared/bs-version.interface";
import { BsvMapDetail, MapFilter, SearchOrder, SearchParams } from "shared/models/maps/beat-saver.model";
import { BsvMapDetail, MapFilter, BsvSearchOrder, SearchParams } from "shared/models/maps/beat-saver.model";
import BeatWaitingImg from "../../../../../assets/images/apngs/beat-waiting.png";
import BeatConflictImg from "../../../../../assets/images/apngs/beat-conflict.png";
import equal from "fast-deep-equal/es6";
@@ -21,6 +20,7 @@ import { OsDiagnosticService } from "renderer/services/os-diagnostic.service";
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";
export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; ownedMaps: BsmLocalMap[] }> = ({ options: {data : { ownedMaps, version }} }) => {
const beatSaver = useService(BeatSaverService);
@@ -35,7 +35,7 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
const [filter, setFilter] = useState<MapFilter>({});
const [query, setQuery] = useState("");
const [maps, setMaps] = useState<BsvMapDetail[]>([]);
const [sortOrder, setSortOrder] = useState<SearchOrder>(BSV_SORT_ORDER.at(0));
const [sortOrder, setSortOrder] = useState<BsvSearchOrder>(BsvSearchOrder.Latest);
const [ownedMapHashs, setOwnedMapHashs] = useState<string[]>(ownedMaps?.map(map => map.hash) ?? []);
const [loading, setLoading] = useState(false);
const isOnline = useObservable(() => os.isOnline$);
@@ -48,9 +48,9 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
const loaderRef = useRef(null);
const sortOptions: BsmSelectOption<SearchOrder>[] = (() => {
return BSV_SORT_ORDER.map(sort => ({ text: `beat-saver.maps-sorts.${sort}`, value: sort }));
})();
const sortOptions: BsmSelectOption<BsvSearchOrder>[] = useConstant(() => {
return Object.values(BsvSearchOrder).map(sort => ({ text: `beat-saver.maps-sorts.${sort}`, value: sort }));
});
useEffect(() => {
loadMaps(searchParams);
@@ -157,7 +157,7 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
mapsDownloader.removeMapToDownload({ map, version });
}, []);
const handleSortChange = (newSort: SearchOrder) => {
const handleSortChange = (newSort: BsvSearchOrder) => {
setSortOrder(() => newSort);
setMaps(() => []);
setSearchParams(() => ({ ...searchParams, sortOrder: newSort }));
@@ -193,7 +193,7 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
<BsmDropdownButton ref={filterContainerRef} className="shrink-0 h-full relative z-[1] flex justify-start" buttonClassName="flex items-center justify-center h-full rounded-full px-2 py-1 !bg-light-main-color-1 dark:!bg-main-color-1" icon="filter" text="pages.version-viewer.maps.search-bar.filters-btn" withBar={false}>
<FilterPanel className="absolute top-[calc(100%+3px)] origin-top w-[450px] h-fit p-2 rounded-md shadow-md shadow-black" localData={false} filter={filter} onChange={setFilter} onApply={handleSearch} onClose={() => filterContainerRef.current.close()} />
</BsmDropdownButton>
<input className="h-full bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-2 grow pb-0.5" type="text" name="" id="" placeholder={t("pages.version-viewer.maps.search-bar.search-placeholder")} value={query} onChange={e => setQuery(e.target.value)} />
<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={query} onChange={e => setQuery(e.target.value)} />
<BsmButton
className="shrink-0 rounded-full py-1 px-3 !bg-light-main-color-1 dark:!bg-main-color-1 flex justify-center items-center capitalize"
icon="search"
@@ -0,0 +1,42 @@
import { useState } from "react";
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";
import { cn } from "renderer/helpers/css-class.helpers";
import { useConstant } from "renderer/hooks/use-constant.hook";
import { PlaylistSearchParams, BsvSearchOrder } from "shared/models/maps/beat-saver.model";
type Props = {
className?: string;
value?: Omit<PlaylistSearchParams, "page">
onSubmit?: (value: Omit<PlaylistSearchParams, "page">) => void;
}
// TODO : Translate
export function DownloadPlaylistModalHeader({ className, value, onSubmit }: Props) {
const [query, setQuery] = useState<string>(value?.q || "");
const [order, setOrder] = useState<BsvSearchOrder>(value?.sortOrder || BsvSearchOrder.Latest);
const sortOptions: BsmSelectOption<BsvSearchOrder>[] = useConstant(() => {
return Object.values(BsvSearchOrder).map(sort => ({ text: `beat-saver.maps-sorts.${sort}`, value: sort }));
});
const submit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if(!onSubmit){ return; }
onSubmit({ q: query, sortOrder: order });
};
return (
<form className={cn('flex flex-row gap-2', className)} onSubmit={submit}>
<BsmDropdownButton buttonClassName="flex items-center justify-center h-full rounded-full px-2 py-1 !bg-light-main-color-1 dark:!bg-main-color-1" icon="filter" text="pages.version-viewer.maps.search-bar.filters-btn" withBar={false}>
<span>Filter Panel</span>
</BsmDropdownButton>
<input className="h-full theme-color-1 rounded-full px-2 grow pb-0.5" type="text" placeholder="Rechercher une playlist" value={query} onChange={e => setQuery(e.target.value)} />
<BsmButton type="submit" className="shrink-0 rounded-full py-1 px-3 !theme-color-1 flex justify-center items-center capitalize" icon="search" text="modals.download-maps.search-btn" withBar={false} />
<BsmSelect className="theme-color-1 rounded-full px-1 pb-0.5 text-center cursor-pointer" options={sortOptions} selected={order} onChange={setOrder}/>
</form>
)
}
@@ -0,0 +1,66 @@
import { useObservable } from "renderer/hooks/use-observable.hook"
import { ModalComponent } 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"
import { LocalBPListsDetails } from "shared/models/playlists/local-playlist.models"
import { DownloadPlaylistModalHeader } from "./download-playlist-modal-header.component"
import { BsvPlaylist, BsvSearchOrder, PlaylistSearchParams } from "shared/models/maps/beat-saver.model"
import { useState } from "react"
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 { PlaylistItem } from "renderer/components/maps-playlists-panel/playlists/playlist-item.component"
import { PlaylistItemComponentPropsMapper } from "shared/mappers/playlist/playlist-item-component-props.mapper"
import { motion } from "framer-motion"
import { PlaylistDownloaderService } from "renderer/services/playlist-downloader.service"
export const DownloadPlaylistModal: ModalComponent<void, {version: BSVersion, ownedPlaylists$: Observable<LocalBPListsDetails[]>, ownedMaps$: Observable<BsmLocalMap[]>}> = (
{ resolver, options: { data: { version, ownedPlaylists$, ownedMaps$ }} }
) => {
const beatSaver = useService(BeatSaverService);
const playlistDownloader = useService(PlaylistDownloaderService);
const [playlists, setPlaylists] = useState<BsvPlaylist[]>([]);
const ownedPlaylists = useObservable(() => ownedPlaylists$, []);
const ownedMaps = useObservable(() => ownedMaps$, []);
const [loading, setLoading] = useState(false);
const [searchParams, setSearchParams] = useState<PlaylistSearchParams>({
q: "",
sortOrder: BsvSearchOrder.Relevance,
page: 0,
});
useOnUpdate(() => {
setLoading(() => true);
beatSaver.searchPlaylists(searchParams)
.then(playlists => setPlaylists(prev => [...prev, ...playlists]))
.finally(() => setLoading(() => false));
}, [searchParams])
const handleNewSearch = (value: Omit<PlaylistSearchParams, "page">) => {
setSearchParams({ ...value, page: 0});
};
const loadMorePlaylists = () => {
setSearchParams(prev => ({ ...prev, page: prev.page + 1 }));
};
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}/>
<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-10">
{playlists.map(playlist => (
<PlaylistItem
key={playlist.playlistId}
{...PlaylistItemComponentPropsMapper.fromBsvPlaylist(playlist)}
onClickSync={() => lastValueFrom(playlistDownloader.downloadPlaylist({ downloadSource: playlist.downloadURL, ignoreSongsHashs: ownedMaps.map(map => map.hash), version }))}
/>
))}
<motion.span className="w-full h-8" onViewportEnter={loadMorePlaylists}/>
</ul>
</div>
)
}
+11
View File
@@ -0,0 +1,11 @@
import clsx, { ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
/**
* cn for classnames, merge tailwind classes with tailwind-merge and clsx
* @param {ClassValue[]} classes
* @returns {string}
*/
export function cn(...classes: ClassValue[]) {
return twMerge(clsx(classes));
}
+11 -4
View File
@@ -91,18 +91,25 @@
}
.tippy-box[data-theme~='default'][data-placement^='top'] > .tippy-arrow::before {
@apply border-t-neutral-900
@apply border-t-neutral-900;
}
.tippy-box[data-theme~='default'][data-placement^='bottom'] > .tippy-arrow::before {
@apply border-b-neutral-900
@apply border-b-neutral-900;
}
.tippy-box[data-theme~='default'][data-placement^='left'] > .tippy-arrow::before {
@apply border-l-neutral-900
@apply border-l-neutral-900;
}
.tippy-box[data-theme~='default'][data-placement^='right'] > .tippy-arrow::before {
@apply border-r-neutral-900
@apply border-r-neutral-900;
}
.theme-color-1 { @apply bg-light-main-color-1 dark:bg-main-color-1; }
.theme-color-2 { @apply bg-light-main-color-2 dark:bg-main-color-2; }
.theme-color-3 { @apply bg-light-main-color-3 dark:bg-main-color-3; }
.\!theme-color-1 { @apply !bg-light-main-color-1 dark:!bg-main-color-1; }
.\!theme-color-2 { @apply !bg-light-main-color-2 dark:!bg-main-color-2; }
.\!theme-color-3 { @apply !bg-light-main-color-3 dark:!bg-main-color-3; }
@keyframes glowing {
0% {
background-position: 0 0;
@@ -1,3 +0,0 @@
import { SearchOrder } from "shared/models/maps/beat-saver.model";
export const BSV_SORT_ORDER: SearchOrder[] = ["Latest", "Relevance", "Rating", "Curated"];
@@ -1,2 +0,0 @@
import { SongDiffName } from "shared/models/maps/song-details-cache/song-details-cache.model";
export const MAP_DIFFICULTIES = Object.values(SongDiffName);
@@ -1,3 +0,0 @@
export type MapExclude = "installed";
export const MAP_EXCLUDES: MapExclude[] = ["installed"];
@@ -1,3 +0,0 @@
import { MapSpecificity } from "shared/models/maps/beat-saver.model";
export const MAP_SPECIFICITIES: MapSpecificity[] = ["automapper", "ranked", "curated", "verified", "fullSpread"];
@@ -1,3 +0,0 @@
import { MapRequirement } from "shared/models/maps/beat-saver.model";
export const MAP_REQUIREMENTS: MapRequirement[] = ["chroma", "noodle", "me", "cinema"];
@@ -1,3 +0,0 @@
import { MapType } from "shared/models/maps/beat-saver.model";
export const MAP_TYPES: MapType[] = ["accuracy", "balanced", "challenge", "dancestyle", "fitness", "speed", "tech"];
+2 -2
View File
@@ -39,8 +39,8 @@ export class ModalService {
}
}
export type ModalOptions<T = unknown> = { data?: T, noStyle?: boolean }
export type ModalComponent<Return = unknown, Receive = unknown> = ({ resolver, options }: { resolver: (x: ModalResponse<Return>) => void; options?: ModalOptions<Receive> }) => JSX.Element;
export type ModalOptions<T = unknown> = { readonly data?: T, readonly noStyle?: boolean }
export type ModalComponent<Return = unknown, Receive = unknown> = ({ resolver, options }: { readonly resolver: (x: ModalResponse<Return>) => void; readonly options?: ModalOptions<Receive> }) => JSX.Element;
export type ModalObject = {modal: ModalComponent, resolver: (value: ModalResponse | PromiseLike<ModalResponse>) => void, options: ModalOptions};
export const enum ModalExitCode {
@@ -1,5 +1,5 @@
import { BehaviorSubject, Observable, Subject, Subscription, distinctUntilChanged, filter, lastValueFrom, map, shareReplay, take, takeUntil, takeWhile, tap } from "rxjs";
import { BPList, DownloadPlaylistProgressionData } from "shared/models/playlists/playlist.interface";
import { DownloadPlaylistProgressionData } from "shared/models/playlists/playlist.interface";
import { IpcService } from "./ipc.service";
import { ProgressBarService } from "./progress-bar.service";
import { Progression } from "main/helpers/fs.helpers";
@@ -7,6 +7,10 @@ import equal from "fast-deep-equal";
import { BSVersion } from "shared/bs-version.interface";
import { LocalBPListsDetails } from "shared/models/playlists/local-playlist.models";
import { removeIndex } from "shared/helpers/array.helpers";
import { ModalResponse, ModalService } from "./modale.service";
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
import { DownloadPlaylistModal } from "renderer/components/modal/modal-types/playlist/download-playlist-modal/download-playlist-modal.component";
import { IpcRequestType } from "shared/models/ipc/ipc-routes";
export class PlaylistDownloaderService {
private static instance: PlaylistDownloaderService;
@@ -21,19 +25,21 @@ export class PlaylistDownloaderService {
private readonly progress: ProgressBarService;
private readonly ipc: IpcService;
private readonly modal: ModalService;
private readonly cancel$ = new Subject<void>();
private readonly playlistQueue$ = new BehaviorSubject<PlaylistQueueInfo[]>([]);
private readonly onPlaylistDownloadedListeners = new Map<BSVersion, ((playlist: LocalBPListsDetails) => void)[]>();
private readonly downloadQueue$ = new BehaviorSubject<PlaylistQueueInfo[]>([]);
private readonly _currentDownload$ = new BehaviorSubject<PlaylistQueueInfo>(null);
private constructor() {
this.progress = ProgressBarService.getInstance();
this.ipc = IpcService.getInstance();
this.modal = ModalService.getInstance();
}
public installPlaylist(bpList: BPList, version?: BSVersion, ignoreSongsHashs?: string[]): Observable<Progression<DownloadPlaylistProgressionData>> {
public downloadPlaylist(info: IpcRequestType<"download-playlist">): Observable<Progression<DownloadPlaylistProgressionData>> {
this.playlistQueue$.next([...this.playlistQueue$.value, { version, source: bpList }]);
this.downloadQueue$.next([...this.downloadQueue$.value, { info }]);
return new Observable<Progression<DownloadPlaylistProgressionData>>(subscriber => {
@@ -41,12 +47,12 @@ export class PlaylistDownloaderService {
let canShowProgress = false;
(async () => {
const queueInfo = await lastValueFrom(this.playlistQueue$.pipe(map(queue => queue.at(0)), filter(p => equal(bpList, p.source)), take(1)));
const download$ = this.ipc.sendV2("install-playlist", { version: queueInfo.version, playlist: queueInfo.source, ignoreSongsHashs }).pipe(takeUntil(this.cancel$));
const queueInfo = await lastValueFrom(this.downloadQueue$.pipe(map(queue => queue.at(0)), filter(qInfo => equal(info.downloadSource, qInfo.info.downloadSource) && equal(info.version, qInfo.info.version)), take(1)));
const download$ = this.ipc.sendV2("download-playlist", info).pipe(takeUntil(this.cancel$));
subs.push(download$.pipe(map(progress => progress?.data?.playlist), filter(Boolean), take(1)).subscribe(playlist => {
queueInfo.downloaded = playlist;
this.onPlaylistDownloadedListeners.get(version)?.forEach(cb => cb(playlist));
this._currentDownload$.next({ ...queueInfo, downloaded: playlist });
}));
canShowProgress = !this.progress.isVisible;
@@ -59,7 +65,8 @@ export class PlaylistDownloaderService {
.then(() => subscriber.complete())
.catch(err => subscriber.error(err))
.finally(() => {
this.playlistQueue$.next(this.playlistQueue$.value.filter(p => !equal(p.version, version) || !equal(p.source, bpList)));
this._currentDownload$.next(null);
this.downloadQueue$.next(this.downloadQueue$.value.filter(qInfo => !equal(qInfo.info.version, info.version) || !equal(qInfo.info.downloadSource, info.downloadSource)));
if (canShowProgress) {
this.progress.hide(true);
}
@@ -70,47 +77,39 @@ export class PlaylistDownloaderService {
}).pipe(shareReplay(1));
}
public cancelDownload(bplist: BPList, version?: BSVersion) {
const currentDownload = this.playlistQueue$.value.at(0);
public cancelDownload(downloadSource: string, version?: BSVersion) {
const currentDownload = this.downloadQueue$.value.at(0);
if(!currentDownload) { return; }
if((equal(currentDownload.source, bplist) || equal(currentDownload.downloaded, bplist)) && equal(currentDownload.version, version)){
if(equal(currentDownload.info.downloadSource, downloadSource) && equal(currentDownload.info.version, version)){
return this.cancel$.next();
}
const indexToRemove = this.playlistQueue$.value.findIndex(p => (equal(p.source, bplist) || equal(p.downloaded, bplist)) && equal(p.version, version));
const indexToRemove = this.downloadQueue$.value.findIndex(qInfo => (equal(qInfo.info.downloadSource, downloadSource) && equal(qInfo.info.version, version)));
if(indexToRemove === -1){ return; }
const newArr = removeIndex(indexToRemove, [...this.playlistQueue$.value]);
this.playlistQueue$.next(newArr);
const newArr = removeIndex(indexToRemove, [...this.downloadQueue$.value]);
this.downloadQueue$.next(newArr);
}
public get currentDownloading$(): Observable<PlaylistQueueInfo> {
return this.playlistQueue$.pipe(map(queue => queue.at(0)), distinctUntilChanged(equal));
public get currentDownload$(): Observable<PlaylistQueueInfo> {
return this._currentDownload$.asObservable();
}
public $isPlaylistInQueue(bpList: BPList, version?: BSVersion): Observable<boolean> {
return this.playlistQueue$.pipe(map(queue => queue.some(p => p && (equal(p.source, bpList) || equal(p.downloaded, bpList)) && equal(p.version, version))), distinctUntilChanged());
public $isPlaylistInQueue(downloadSource: string, version?: BSVersion): Observable<boolean> {
return this.downloadQueue$.pipe(map(queue => queue.some(qInfo => qInfo && (equal(qInfo.info.downloadSource, downloadSource) && equal(qInfo.info.version, version))), distinctUntilChanged()));
}
public $isPlaylistDownloading(bpList: BPList, version?: BSVersion): Observable<boolean> {
return this.currentDownloading$.pipe(map(p => {
if(!p){ return false; }
if(!equal(p.source, bpList) && !equal(p.downloaded, bpList)){ return false; }
if(version && !equal(p.version, version)){ return false; }
public $isPlaylistDownloading(downloadSource: string, version?: BSVersion): Observable<boolean> {
return this.currentDownload$.pipe(map(qInfo => {
if(!qInfo){ return false; }
if(!equal(qInfo.info.downloadSource, downloadSource)){ return false; }
if(version && !equal(qInfo.info.version, version)){ return false; }
return true;
}), distinctUntilChanged());
}
public addOnPlaylistDownloadedListener(version: BSVersion, cb: (playlist: LocalBPListsDetails) => void){
this.onPlaylistDownloadedListeners.set(version, [...(this.onPlaylistDownloadedListeners.get(version) || []), cb]);
}
public removeOnPlaylistDownloadedListener(version: BSVersion, cb: (playlist: LocalBPListsDetails) => void){
this.onPlaylistDownloadedListeners.set(version, (this.onPlaylistDownloadedListeners.get(version) || []).filter(c => c !== cb));
}
public oneClickInstallPlaylist(bpListUrl: string): Observable<Progression<DownloadPlaylistProgressionData>> {
const download$ = this.ipc.sendV2("one-click-install-playlist", bpListUrl);
@@ -123,10 +122,13 @@ export class PlaylistDownloaderService {
complete: () => this.progress.hide(true)
}));
}
public openDownloadPlaylistModal(version: BSVersion, ownedPlaylists$: Observable<LocalBPListsDetails[]>, ownedMaps$: Observable<BsmLocalMap[]>): Promise<ModalResponse<void>> {
return this.modal.openModal(DownloadPlaylistModal, { data: { version, ownedPlaylists$, ownedMaps$ } })
}
}
export type PlaylistQueueInfo = {
version?: BSVersion;
source: BPList;
info: Omit<IpcRequestType<"download-playlist">, "ignoreSongsHashs">;
downloaded?: LocalBPListsDetails;
}
@@ -1,5 +1,5 @@
import { BsvMapDetail } from "shared/models/maps";
import { SearchParams } from "shared/models/maps/beat-saver.model";
import { BsvPlaylist, PlaylistSearchParams, SearchParams } from "shared/models/maps/beat-saver.model";
import { IpcService } from "../ipc.service";
import { lastValueFrom } from "rxjs";
@@ -29,4 +29,8 @@ export class BeatSaverService {
public async searchMaps(search: SearchParams): Promise<BsvMapDetail[]> {
return lastValueFrom(this.ipc.sendV2("bsv-search-map", search));
}
public async searchPlaylists(search: PlaylistSearchParams): Promise<BsvPlaylist[]> {
return lastValueFrom(this.ipc.sendV2("bsv-search-playlist", search));
}
}
@@ -0,0 +1,36 @@
import { PlaylistItemComponentProps } from "renderer/components/maps-playlists-panel/playlists/playlist-item.component";
import { BsvPlaylist } from "shared/models/maps/beat-saver.model";
import { LocalBPListsDetails } from "shared/models/playlists/local-playlist.models";
export abstract class PlaylistItemComponentPropsMapper {
static fromBsvPlaylist(playlist: BsvPlaylist): PlaylistItemComponentProps {
return {
title: playlist.name,
author: playlist.owner.name,
coverUrl: playlist.playlistImage,
nbMaps: playlist.stats?.totalMaps,
nbMappers: playlist.stats?.mapperCount,
duration: playlist.stats?.totalDuration,
maxNps: playlist.stats?.maxNps,
minNps: playlist.stats?.minNps
}
}
static fromLocalBPListDetails(playlist: LocalBPListsDetails): PlaylistItemComponentProps {
return {
title: playlist.playlistTitle,
author: playlist.playlistAuthor,
coverBase64: playlist.image,
nbMaps: playlist.nbMaps,
nbMappers: playlist.nbMappers,
duration: playlist.duration,
maxNps: playlist.maxNps,
minNps: playlist.minNps
};
}
}
+4 -3
View File
@@ -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 { SearchParams } from "../maps/beat-saver.model";
import { BsvPlaylist, 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";
@@ -22,7 +22,7 @@ import { LocalBPList, LocalBPListsDetails } from "../playlists/local-playlist.mo
export type IpcReplier<T> = (data: Observable<T>) => void;
export interface IpcChannelMapping extends Record<string, { request: unknown, response: unknown }> {
export interface IpcChannelMapping {
/* ** bs-download-ipcs ** */
"import-version": { request: ImportVersionOptions, response: Progression<BSVersion>};
@@ -41,6 +41,7 @@ export interface IpcChannelMapping extends Record<string, { request: unknown, re
"bsv-search-map": {request: SearchParams, response: BsvMapDetail[]};
"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[]};
/* ** bs-launcher-ipcs ** */
"create-launch-shortcut": { request: LaunchOption, response: boolean };
@@ -80,7 +81,7 @@ export interface IpcChannelMapping extends Record<string, { request: unknown, re
"register-playlists-deep-link": { request: void, response: boolean };
"unregister-playlists-deep-link": { request: void, response: boolean };
"is-playlists-deep-links-enabled": { request: void, response: boolean };
"install-playlist": {request: {playlist: BPList, version?: BSVersion, ignoreSongsHashs?: string[]}, response: Progression<DownloadPlaylistProgressionData>};
"download-playlist": {request: {downloadSource: string, dest?: string, version?: BSVersion, ignoreSongsHashs?: string[]}, response: Progression<DownloadPlaylistProgressionData>};
"get-version-playlists-details": {request: BSVersion, response: Progression<LocalBPListsDetails[]>};
"delete-playlist": {request: {version: BSVersion, bpList: LocalBPList, deleteMaps?: boolean}, response: Progression};
+48 -5
View File
@@ -195,8 +195,22 @@ export enum MapType {
export const MapTags = { ...MapStyle, ...MapType };
export type MapTag = MapStyle | MapType;
export type MapRequirement = "chroma" | "noodle" | "me" | "cinema";
export type MapSpecificity = "automapper" | "ranked" | "curated" | "verified" | "fullSpread";
export enum MapRequirement {
Chroma = "chroma",
Noodle = "noodle",
Me = "me",
Cinema = "cinema"
}
export enum MapSpecificity {
Automapper = "automapper",
Ranked = "ranked",
Curated = "curated",
Verified = "verified",
FullSpread = "fullSpread"
}
export interface MapFilter {
automapper?: boolean;
@@ -224,15 +238,24 @@ export interface SearchResponse {
redirect: string;
}
export interface PlaylistSearchResponse {
docs: BsvPlaylist[];
}
export interface SearchParams {
sortOrder: SearchOrder;
sortOrder: BsvSearchOrder;
filter?: MapFilter;
page?: number;
q?: string;
includeEmpty?: boolean;
}
export type SearchOrder = "Latest" | "Relevance" | "Rating" | "Curated";
export enum BsvSearchOrder {
Latest = "Latest",
Relevance = "Relevance",
Rating = "Rating",
Curated = "Curated"
}
export interface BsvMapDetailWithOrder {
map: BsvMapDetail;
@@ -251,10 +274,17 @@ export interface BsvPlaylist {
playlistId: number;
playlistImage: string;
playlistImage512: string;
public: boolean;
songsChangedAt: BsvInstant;
stats: BsvPlaylistStats;
updatedAt: BsvInstant;
type: BsvPlaylistType;
}
export enum BsvPlaylistType {
Private = "Private",
Public = "Public",
System = "System",
Search = "Search"
}
export interface BsvPlaylistStats {
@@ -275,3 +305,16 @@ export interface BsvPlaylistPage {
maps: BsvMapDetailWithOrder[];
playlist: BsvPlaylist;
}
export interface PlaylistSearchParams {
q?: string;
page?: number;
sortOrder: BsvSearchOrder;
from?: string;
to?: string;
minNps?: number;
maxNps?: number;
curated?: boolean;
verified?: boolean;
includeEmpty?: boolean;
}
@@ -1,4 +1,4 @@
import { SongDiffName } from "shared/models/maps/song-details-cache/song-details-cache.model";
import { SongDiffName } from "./song-details-cache/song-details-cache.model";
export const MAP_DIFFICULTIES_COLORS: Record<SongDiffName, string> = {
Easy: "#00FF9E",