From c02ac39184e71dcc5080baa64a775e1c6c0fc06a Mon Sep 17 00:00:00 2001 From: MathieuG-P <40181755+Zagrios@users.noreply.github.com> Date: Sun, 12 May 2024 17:15:13 +0200 Subject: [PATCH] [feature-107] we can now download playlist from beatsaver + other internal changes --- assets/jsons/bs-versions.json | 2 +- package-lock.json | 27 ++++++- package.json | 2 + src/main/ipcs/beat-saver-ipcs.ts | 6 +- src/main/ipcs/bs-playlist-ipcs.ts | 23 ++---- .../beat-saver/beat-saver-api.service.ts | 34 +++------ .../beat-saver/beat-saver.service.ts | 14 +++- .../maps-playlists-panel.component.tsx | 16 ++-- .../maps/filter-panel.component.tsx | 25 ++----- .../maps/map-item.component.tsx | 5 +- .../local-playlists-list-panel.component.tsx | 75 +++++++++---------- .../playlists/playlist-item.component.tsx | 8 +- .../download-maps-modal.component.tsx | 16 ++-- ...wnload-playlist-modal-header.component.tsx | 42 +++++++++++ .../download-playlist-modal.component.tsx | 66 ++++++++++++++++ src/renderer/helpers/css-class.helpers.ts | 11 +++ src/renderer/index.css | 15 +++- .../partials/beat-saver/sort-order.ts | 3 - .../maps/map-difficulties/map-difficulties.ts | 2 - .../maps/map-excludes/map-excludes.ts | 3 - .../maps/map-general/map-specificity.ts | 3 - .../maps/map-requirements/map-requirements.ts | 3 - .../partials/maps/map-tags/map-styles.ts | 0 .../partials/maps/map-tags/map-types.ts | 3 - src/renderer/services/modale.service.ts | 4 +- .../services/playlist-downloader.service.ts | 72 +++++++++--------- .../thrird-partys/beat-saver.service.ts | 6 +- .../playlist-item-component-props.mapper.ts | 36 +++++++++ src/shared/models/ipc/ipc-routes.ts | 7 +- src/shared/models/maps/beat-saver.model.ts | 53 +++++++++++-- .../models/maps/difficulties-colors.ts} | 2 +- 31 files changed, 391 insertions(+), 193 deletions(-) create mode 100644 src/renderer/components/modal/modal-types/playlist/download-playlist-modal/download-playlist-modal-header.component.tsx create mode 100644 src/renderer/components/modal/modal-types/playlist/download-playlist-modal/download-playlist-modal.component.tsx create mode 100644 src/renderer/helpers/css-class.helpers.ts delete mode 100644 src/renderer/partials/beat-saver/sort-order.ts delete mode 100644 src/renderer/partials/maps/map-difficulties/map-difficulties.ts delete mode 100644 src/renderer/partials/maps/map-excludes/map-excludes.ts delete mode 100644 src/renderer/partials/maps/map-general/map-specificity.ts delete mode 100644 src/renderer/partials/maps/map-requirements/map-requirements.ts delete mode 100644 src/renderer/partials/maps/map-tags/map-styles.ts delete mode 100644 src/renderer/partials/maps/map-tags/map-types.ts create mode 100644 src/shared/mappers/playlist/playlist-item-component-props.mapper.ts rename src/{renderer/partials/maps/map-difficulties/map-difficulties-colors.ts => shared/models/maps/difficulties-colors.ts} (66%) diff --git a/assets/jsons/bs-versions.json b/assets/jsons/bs-versions.json index c0a12e4d..549ff5de 100644 --- a/assets/jsons/bs-versions.json +++ b/assets/jsons/bs-versions.json @@ -718,7 +718,7 @@ "ReleaseDate": "1712681263", "year": "2024" }, - { + { "BSVersion": "1.36.2", "BSManifest": "4832237377879752064", "OculusBinaryId": "6983660338400352", diff --git a/package-lock.json b/package-lock.json index ebbdc0c7..ace59d6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index b56ab474..f2c22f30 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/main/ipcs/beat-saver-ipcs.ts b/src/main/ipcs/beat-saver-ipcs.ts index ed730d4a..8d93e1f9 100644 --- a/src/main/ipcs/beat-saver-ipcs.ts +++ b/src/main/ipcs/beat-saver-ipcs.ts @@ -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))); +}); diff --git a/src/main/ipcs/bs-playlist-ipcs.ts b/src/main/ipcs/bs-playlist-ipcs.ts index 5af61e25..26aebb03 100644 --- a/src/main/ipcs/bs-playlist-ipcs.ts +++ b/src/main/ipcs/bs-playlist-ipcs.ts @@ -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 })); }); diff --git a/src/main/services/thrid-party/beat-saver/beat-saver-api.service.ts b/src/main/services/thrid-party/beat-saver/beat-saver-api.service.ts index a8fef0a4..7ac4dafa 100644 --- a/src/main/services/thrid-party/beat-saver/beat-saver-api.service.ts +++ b/src/main/services/thrid-party/beat-saver/beat-saver-api.service.ts @@ -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): Record { + 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 = this.objectToStringRecord({...filter, tags}); return new URLSearchParams(params); } @@ -95,14 +82,15 @@ export class BeatSaverApiService { return this.request.getJSON(`${this.bsaverApiUrl}/maps/id/${id}`); } - public async searchMaps(search: SearchParams): Promise { + public searchMaps(search: SearchParams): Promise { const url = new URL(`${this.bsaverApiUrl}/search/text/${search?.page ?? 0}`); url.search = this.searchParamsToUrlParams(search).toString(); return this.request.getJSON(url.toString()); } - public async getPlaylistDetails(id: string): Promise { - const res = await this.request.getJSON(`${this.bsaverApiUrl}/playlists/id/${id}/0`); - return res.playlist; + public searchPlaylists(search: PlaylistSearchParams): Promise { + const url = new URL(`${this.bsaverApiUrl}/playlists/search/${search?.page ?? 0}`); + url.search = new URLSearchParams(this.objectToStringRecord(search)).toString(); + return this.request.getJSON(url.toString()); } } diff --git a/src/main/services/thrid-party/beat-saver/beat-saver.service.ts b/src/main/services/thrid-party/beat-saver/beat-saver.service.ts index a6ae33e4..329332b4 100644 --- a/src/main/services/thrid-party/beat-saver/beat-saver.service.ts +++ b/src/main/services/thrid-party/beat-saver/beat-saver.service.ts @@ -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 { - return this.bsaverApi.getPlaylistDetails(id); + public searchPlaylists(search: PlaylistSearchParams): Promise { + return this.bsaverApi + .searchPlaylists(search) + .then(res => res.docs) + .catch(e => { + log.error(e); + return e; + }); } } diff --git a/src/renderer/components/maps-playlists-panel/maps-playlists-panel.component.tsx b/src/renderer/components/maps-playlists-panel/maps-playlists-panel.component.tsx index d8e2be2c..2f8fc49b 100644 --- a/src/renderer/components/maps-playlists-panel/maps-playlists-panel.component.tsx +++ b/src/renderer/components/maps-playlists-panel/maps-playlists-panel.component.tsx @@ -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} />
handleSearch(e.target.value)} tabIndex={-1} /> diff --git a/src/renderer/components/maps-playlists-panel/maps/filter-panel.component.tsx b/src/renderer/components/maps-playlists-panel/maps/filter-panel.component.tsx index cfbabdef..86b09b88 100644 --- a/src/renderer/components/maps-playlists-panel/maps/filter-panel.component.tsx +++ b/src/renderer/components/maps-playlists-panel/maps/filter-panel.component.tsx @@ -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 = { [k in keyof T]: T[k] extends boolean ? k : never }[keyof T]; const handleCheckbox = (key: BooleanKeys) => { @@ -180,14 +173,14 @@ export function FilterPanel({ className, ref, playlist = false, filter, localDat

{t("maps.map-filter-panel.specificities")}

- {MAP_SPECIFICITIES.map(specificity => ( + {Object.values(MapSpecificity).map(specificity => (
handleCheckbox(specificity)}> handleCheckbox(specificity)} /> {translateMapSpecificity(specificity)}
))}

{t("maps.map-filter-panel.requirements")}

- {MAP_REQUIREMENTS.map(requirement => ( + {Object.values(MapRequirement).map(requirement => (
handleCheckbox(requirement)}> handleCheckbox(requirement)} /> {requirement} @@ -196,12 +189,10 @@ export function FilterPanel({ className, ref, playlist = false, filter, localDat { !localData && ( <>

{t("maps.map-filter-panel.exclude")}

- {MAP_EXCLUDES.map(exclude => ( -
handleCheckbox(exclude)}> - handleCheckbox(exclude)} /> - {translateMapExclude(exclude)} -
- ))} +
handleCheckbox("installed")}> + handleCheckbox("installed")} /> + {t("maps.map-excludes.installed")} +
)}
diff --git a/src/renderer/components/maps-playlists-panel/maps/map-item.component.tsx b/src/renderer/components/maps-playlists-panel/maps/map-item.component.tsx index 7859c7f5..a736a1ec 100644 --- a/src/renderer/components/maps-playlists-panel/maps/map-item.component.tsx +++ b/src/renderer/components/maps-playlists-panel/maps/map-item.component.tsx @@ -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; diff --git a/src/renderer/components/maps-playlists-panel/playlists/local-playlists-list-panel.component.tsx b/src/renderer/components/maps-playlists-panel/playlists/local-playlists-list-panel.component.tsx index 3a4fbe6a..65c21c20 100644 --- a/src/renderer/components/maps-playlists-panel/playlists/local-playlists-list-panel.component.tsx +++ b/src/renderer/components/maps-playlists-panel/playlists/local-playlists-list-panel.component.tsx @@ -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(({ 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(({ 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(({ 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(({ version, cl }) }; - const render = () => { - if(playlistsLoading){ - return ( - - ) - } - - if (playlists?.length){ - return ( -
    - {playlists.map(p => - openPlaylistDetails(p.path)} - onClickDelete={() => deletePlaylist(p)} - onClickSync={isOnline && (() => installPlaylist(p))} - onClickOpenFile={() => viewPlaylistFile(p.path)} - onClickCancelDownload={() => playlistDownloader.cancelDownload(p, version)} - /> - )} -
- ) - } - - return null; - } - return (
- {render()} + {(() => { + if(playlistsLoading){ + return ( + + ) + } + + if (playlists?.length){ + return ( +
    + {playlists.map(p => + openPlaylistDetails(p.path)} + onClickDelete={() => deletePlaylist(p)} + onClickSync={isOnline && (() => installPlaylist(p))} + onClickOpenFile={() => viewPlaylistFile(p.path)} + onClickCancelDownload={() => playlistDownloader.cancelDownload(p.customData?.syncURL ?? p.path, version)} + /> + )} +
+ ) + } + + return TODO; + })()}
) }); diff --git a/src/renderer/components/maps-playlists-panel/playlists/playlist-item.component.tsx b/src/renderer/components/maps-playlists-panel/playlists/playlist-item.component.tsx index 2ea24c4d..a5add1c1 100644 --- a/src/renderer/components/maps-playlists-panel/playlists/playlist-item.component.tsx +++ b/src/renderer/components/maps-playlists-panel/playlists/playlist-item.component.tsx @@ -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 ( setHovered(() => true)} onHoverEnd={() => setHovered(() => false)} > -
-
+
+
diff --git a/src/renderer/components/modal/modal-types/download-maps-modal.component.tsx b/src/renderer/components/modal/modal-types/download-maps-modal.component.tsx index f0af4090..189ac912 100644 --- a/src/renderer/components/modal/modal-types/download-maps-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/download-maps-modal.component.tsx @@ -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 = ({ options: {data : { ownedMaps, version }} }) => { const beatSaver = useService(BeatSaverService); @@ -35,7 +35,7 @@ export const DownloadMapsModal: ModalComponent({}); const [query, setQuery] = useState(""); const [maps, setMaps] = useState([]); - const [sortOrder, setSortOrder] = useState(BSV_SORT_ORDER.at(0)); + const [sortOrder, setSortOrder] = useState(BsvSearchOrder.Latest); const [ownedMapHashs, setOwnedMapHashs] = useState(ownedMaps?.map(map => map.hash) ?? []); const [loading, setLoading] = useState(false); const isOnline = useObservable(() => os.isOnline$); @@ -48,9 +48,9 @@ export const DownloadMapsModal: ModalComponent[] = (() => { - return BSV_SORT_ORDER.map(sort => ({ text: `beat-saver.maps-sorts.${sort}`, value: sort })); - })(); + const sortOptions: BsmSelectOption[] = 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 { + const handleSortChange = (newSort: BsvSearchOrder) => { setSortOrder(() => newSort); setMaps(() => []); setSearchParams(() => ({ ...searchParams, sortOrder: newSort })); @@ -193,7 +193,7 @@ export const DownloadMapsModal: ModalComponent filterContainerRef.current.close()} /> - setQuery(e.target.value)} /> + setQuery(e.target.value)} /> + onSubmit?: (value: Omit) => void; +} + +// TODO : Translate + +export function DownloadPlaylistModalHeader({ className, value, onSubmit }: Props) { + + const [query, setQuery] = useState(value?.q || ""); + const [order, setOrder] = useState(value?.sortOrder || BsvSearchOrder.Latest); + + const sortOptions: BsmSelectOption[] = useConstant(() => { + return Object.values(BsvSearchOrder).map(sort => ({ text: `beat-saver.maps-sorts.${sort}`, value: sort })); + }); + + const submit = (e: React.FormEvent) => { + e.preventDefault(); + if(!onSubmit){ return; } + onSubmit({ q: query, sortOrder: order }); + }; + + return ( +
+ + Filter Panel + + setQuery(e.target.value)} /> + + + + ) +} diff --git a/src/renderer/components/modal/modal-types/playlist/download-playlist-modal/download-playlist-modal.component.tsx b/src/renderer/components/modal/modal-types/playlist/download-playlist-modal/download-playlist-modal.component.tsx new file mode 100644 index 00000000..284cd323 --- /dev/null +++ b/src/renderer/components/modal/modal-types/playlist/download-playlist-modal/download-playlist-modal.component.tsx @@ -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, ownedMaps$: Observable}> = ( + { resolver, options: { data: { version, ownedPlaylists$, ownedMaps$ }} } +) => { + + const beatSaver = useService(BeatSaverService); + const playlistDownloader = useService(PlaylistDownloaderService); + + const [playlists, setPlaylists] = useState([]); + const ownedPlaylists = useObservable(() => ownedPlaylists$, []); + const ownedMaps = useObservable(() => ownedMaps$, []); + + const [loading, setLoading] = useState(false); + const [searchParams, setSearchParams] = useState({ + 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) => { + setSearchParams({ ...value, page: 0}); + }; + + const loadMorePlaylists = () => { + setSearchParams(prev => ({ ...prev, page: prev.page + 1 })); + }; + + return ( +
+ +
    + {playlists.map(playlist => ( + lastValueFrom(playlistDownloader.downloadPlaylist({ downloadSource: playlist.downloadURL, ignoreSongsHashs: ownedMaps.map(map => map.hash), version }))} + /> + ))} + +
+
+ ) +} diff --git a/src/renderer/helpers/css-class.helpers.ts b/src/renderer/helpers/css-class.helpers.ts new file mode 100644 index 00000000..f0805211 --- /dev/null +++ b/src/renderer/helpers/css-class.helpers.ts @@ -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)); +} diff --git a/src/renderer/index.css b/src/renderer/index.css index 7cdf5f2e..0c5cb0b4 100644 --- a/src/renderer/index.css +++ b/src/renderer/index.css @@ -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; diff --git a/src/renderer/partials/beat-saver/sort-order.ts b/src/renderer/partials/beat-saver/sort-order.ts deleted file mode 100644 index f2461923..00000000 --- a/src/renderer/partials/beat-saver/sort-order.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { SearchOrder } from "shared/models/maps/beat-saver.model"; - -export const BSV_SORT_ORDER: SearchOrder[] = ["Latest", "Relevance", "Rating", "Curated"]; diff --git a/src/renderer/partials/maps/map-difficulties/map-difficulties.ts b/src/renderer/partials/maps/map-difficulties/map-difficulties.ts deleted file mode 100644 index 62d2677c..00000000 --- a/src/renderer/partials/maps/map-difficulties/map-difficulties.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { SongDiffName } from "shared/models/maps/song-details-cache/song-details-cache.model"; -export const MAP_DIFFICULTIES = Object.values(SongDiffName); diff --git a/src/renderer/partials/maps/map-excludes/map-excludes.ts b/src/renderer/partials/maps/map-excludes/map-excludes.ts deleted file mode 100644 index 8244cc6a..00000000 --- a/src/renderer/partials/maps/map-excludes/map-excludes.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type MapExclude = "installed"; - -export const MAP_EXCLUDES: MapExclude[] = ["installed"]; diff --git a/src/renderer/partials/maps/map-general/map-specificity.ts b/src/renderer/partials/maps/map-general/map-specificity.ts deleted file mode 100644 index f703c627..00000000 --- a/src/renderer/partials/maps/map-general/map-specificity.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { MapSpecificity } from "shared/models/maps/beat-saver.model"; - -export const MAP_SPECIFICITIES: MapSpecificity[] = ["automapper", "ranked", "curated", "verified", "fullSpread"]; diff --git a/src/renderer/partials/maps/map-requirements/map-requirements.ts b/src/renderer/partials/maps/map-requirements/map-requirements.ts deleted file mode 100644 index 7c82e6d2..00000000 --- a/src/renderer/partials/maps/map-requirements/map-requirements.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { MapRequirement } from "shared/models/maps/beat-saver.model"; - -export const MAP_REQUIREMENTS: MapRequirement[] = ["chroma", "noodle", "me", "cinema"]; diff --git a/src/renderer/partials/maps/map-tags/map-styles.ts b/src/renderer/partials/maps/map-tags/map-styles.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/renderer/partials/maps/map-tags/map-types.ts b/src/renderer/partials/maps/map-tags/map-types.ts deleted file mode 100644 index 89387c81..00000000 --- a/src/renderer/partials/maps/map-tags/map-types.ts +++ /dev/null @@ -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"]; diff --git a/src/renderer/services/modale.service.ts b/src/renderer/services/modale.service.ts index e041def2..0115e4d8 100644 --- a/src/renderer/services/modale.service.ts +++ b/src/renderer/services/modale.service.ts @@ -39,8 +39,8 @@ export class ModalService { } } -export type ModalOptions = { data?: T, noStyle?: boolean } -export type ModalComponent = ({ resolver, options }: { resolver: (x: ModalResponse) => void; options?: ModalOptions }) => JSX.Element; +export type ModalOptions = { readonly data?: T, readonly noStyle?: boolean } +export type ModalComponent = ({ resolver, options }: { readonly resolver: (x: ModalResponse) => void; readonly options?: ModalOptions }) => JSX.Element; export type ModalObject = {modal: ModalComponent, resolver: (value: ModalResponse | PromiseLike) => void, options: ModalOptions}; export const enum ModalExitCode { diff --git a/src/renderer/services/playlist-downloader.service.ts b/src/renderer/services/playlist-downloader.service.ts index 4a051071..90af7c6d 100644 --- a/src/renderer/services/playlist-downloader.service.ts +++ b/src/renderer/services/playlist-downloader.service.ts @@ -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(); - private readonly playlistQueue$ = new BehaviorSubject([]); - private readonly onPlaylistDownloadedListeners = new Map void)[]>(); + private readonly downloadQueue$ = new BehaviorSubject([]); + private readonly _currentDownload$ = new BehaviorSubject(null); private constructor() { this.progress = ProgressBarService.getInstance(); this.ipc = IpcService.getInstance(); + this.modal = ModalService.getInstance(); } - public installPlaylist(bpList: BPList, version?: BSVersion, ignoreSongsHashs?: string[]): Observable> { + public downloadPlaylist(info: IpcRequestType<"download-playlist">): Observable> { - this.playlistQueue$.next([...this.playlistQueue$.value, { version, source: bpList }]); + this.downloadQueue$.next([...this.downloadQueue$.value, { info }]); return new Observable>(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 { - return this.playlistQueue$.pipe(map(queue => queue.at(0)), distinctUntilChanged(equal)); + public get currentDownload$(): Observable { + return this._currentDownload$.asObservable(); } - public $isPlaylistInQueue(bpList: BPList, version?: BSVersion): Observable { - 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 { + 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 { - 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 { + 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> { 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, ownedMaps$: Observable): Promise> { + return this.modal.openModal(DownloadPlaylistModal, { data: { version, ownedPlaylists$, ownedMaps$ } }) + } } export type PlaylistQueueInfo = { - version?: BSVersion; - source: BPList; + info: Omit, "ignoreSongsHashs">; downloaded?: LocalBPListsDetails; } diff --git a/src/renderer/services/thrird-partys/beat-saver.service.ts b/src/renderer/services/thrird-partys/beat-saver.service.ts index fcf4e715..0c9c0249 100644 --- a/src/renderer/services/thrird-partys/beat-saver.service.ts +++ b/src/renderer/services/thrird-partys/beat-saver.service.ts @@ -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 { return lastValueFrom(this.ipc.sendV2("bsv-search-map", search)); } + + public async searchPlaylists(search: PlaylistSearchParams): Promise { + return lastValueFrom(this.ipc.sendV2("bsv-search-playlist", search)); + } } diff --git a/src/shared/mappers/playlist/playlist-item-component-props.mapper.ts b/src/shared/mappers/playlist/playlist-item-component-props.mapper.ts new file mode 100644 index 00000000..842ffdc5 --- /dev/null +++ b/src/shared/mappers/playlist/playlist-item-component-props.mapper.ts @@ -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 + }; + } + +} diff --git a/src/shared/models/ipc/ipc-routes.ts b/src/shared/models/ipc/ipc-routes.ts index edce1c80..0ef06fc3 100644 --- a/src/shared/models/ipc/ipc-routes.ts +++ b/src/shared/models/ipc/ipc-routes.ts @@ -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 = (data: Observable) => void; -export interface IpcChannelMapping extends Record { +export interface IpcChannelMapping { /* ** bs-download-ipcs ** */ "import-version": { request: ImportVersionOptions, response: Progression}; @@ -41,6 +41,7 @@ export interface IpcChannelMapping extends Record}; + "download-playlist": {request: {downloadSource: string, dest?: string, version?: BSVersion, ignoreSongsHashs?: string[]}, response: Progression}; "get-version-playlists-details": {request: BSVersion, response: Progression}; "delete-playlist": {request: {version: BSVersion, bpList: LocalBPList, deleteMaps?: boolean}, response: Progression}; diff --git a/src/shared/models/maps/beat-saver.model.ts b/src/shared/models/maps/beat-saver.model.ts index 7e78ece8..d26adb30 100644 --- a/src/shared/models/maps/beat-saver.model.ts +++ b/src/shared/models/maps/beat-saver.model.ts @@ -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; +} diff --git a/src/renderer/partials/maps/map-difficulties/map-difficulties-colors.ts b/src/shared/models/maps/difficulties-colors.ts similarity index 66% rename from src/renderer/partials/maps/map-difficulties/map-difficulties-colors.ts rename to src/shared/models/maps/difficulties-colors.ts index ae0dcda3..8c34d6dc 100644 --- a/src/renderer/partials/maps/map-difficulties/map-difficulties-colors.ts +++ b/src/shared/models/maps/difficulties-colors.ts @@ -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 = { Easy: "#00FF9E",