mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
[feature-107] we can now download playlist from beatsaver + other internal changes
This commit is contained in:
@@ -718,7 +718,7 @@
|
||||
"ReleaseDate": "1712681263",
|
||||
"year": "2024"
|
||||
},
|
||||
{
|
||||
{
|
||||
"BSVersion": "1.36.2",
|
||||
"BSManifest": "4832237377879752064",
|
||||
"OculusBinaryId": "6983660338400352",
|
||||
|
||||
Generated
+25
-2
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)));
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
+34
-41
@@ -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"
|
||||
|
||||
+42
@@ -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>
|
||||
)
|
||||
}
|
||||
+66
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
@@ -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"];
|
||||
@@ -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,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};
|
||||
|
||||
|
||||
@@ -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
-1
@@ -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",
|
||||
Reference in New Issue
Block a user