diff --git a/src/main/ipcs/bs-playlist-ipcs.ts b/src/main/ipcs/bs-playlist-ipcs.ts index c95fdaa7..b3284481 100644 --- a/src/main/ipcs/bs-playlist-ipcs.ts +++ b/src/main/ipcs/bs-playlist-ipcs.ts @@ -1,3 +1,4 @@ +import { BSVersion } from "shared/bs-version.interface"; import { LocalPlaylistsManagerService } from "../services/additional-content/local-playlists-manager.service"; import { IpcService } from "../services/ipc.service"; import { of, throwError } from "rxjs"; @@ -39,7 +40,7 @@ ipc.on("is-playlists-deep-links-enabled", (_, reply) => { } }); -ipc.on("get-version-playlists", (req, reply) => { +ipc.on("get-version-playlists", (req, reply) => { const playlists = LocalPlaylistsManagerService.getInstance(); reply(playlists.getVersionPlaylists(req.args)); }); diff --git a/src/main/models/json-cache.class.ts b/src/main/models/json-cache.class.ts index f3a76a9f..8ce64836 100644 --- a/src/main/models/json-cache.class.ts +++ b/src/main/models/json-cache.class.ts @@ -5,7 +5,7 @@ import { Subject, debounceTime } from "rxjs"; export class JsonCache { - private cache: Record = {}; + private _cache: Record = {}; private readonly setEvent: Subject = new Subject(); public constructor( @@ -22,40 +22,43 @@ export class JsonCache { private load(): void { try { - this.cache = require(this.jsonPath); + this._cache = require(this.jsonPath); } catch (error) { log.warn("Failed to load cache or file cache not exist yet", this.jsonPath, error); } finally { - this.cache ??= {}; + this._cache ??= {}; } } public save(): void { - const res = tryit(() => writeFileSync(this.jsonPath, JSON.stringify(this.cache), { flush: true })); + const res = tryit(() => writeFileSync(this.jsonPath, JSON.stringify(this._cache), { flush: true })); if(res.error){ log.error("Failed to save cache", this.jsonPath, res.error); } } public get(key: string): T { - return this.cache[key]; + return this._cache[key]; } public set(key: string, value: T): void { - this.cache[key] = value; + this._cache[key] = value; this.setEvent?.next(); } public delete(key: string): void { - delete this.cache[key]; + delete this._cache[key]; this.setEvent?.next(); } public clear(): void { - this.cache = {}; + this._cache = {}; this.setEvent?.next(); } + public get cache(): Record { + return this._cache; + } } export type JsonCacheOptions = { diff --git a/src/main/services/additional-content/local-playlists-manager.service.ts b/src/main/services/additional-content/local-playlists-manager.service.ts index f6932075..cc72ef0d 100644 --- a/src/main/services/additional-content/local-playlists-manager.service.ts +++ b/src/main/services/additional-content/local-playlists-manager.service.ts @@ -13,6 +13,11 @@ import { BeatSaverService } from "../thrid-party/beat-saver/beat-saver.service"; import { copy, copyFile, pathExists, pathExistsSync, readdirSync, realpath } from "fs-extra"; import { Progression, ensureFolderExist, pathExist } from "../../helpers/fs.helpers"; import { FileAssociationService } from "../file-association.service"; +import { SongDetailsCacheService } from "./maps/song-details-cache.service"; +import { sToMs } from "shared/helpers/time.helpers"; +import { LocalBPList, LocalBpListSong } from "shared/models/playlists/local-playlist.models"; +import { SongCacheService } from "./maps/song-cache.service"; +import { pathToFileURL } from "url"; export class LocalPlaylistsManagerService { private static instance: LocalPlaylistsManagerService; @@ -36,6 +41,8 @@ export class LocalPlaylistsManagerService { private readonly fileAssociation: FileAssociationService; private readonly windows: WindowManagerService; private readonly bsaver: BeatSaverService; + private readonly songDetails: SongDetailsCacheService; + private readonly songCache: SongCacheService; private constructor() { this.maps = LocalMapsManagerService.getInstance(); @@ -45,6 +52,9 @@ export class LocalPlaylistsManagerService { this.fileAssociation = FileAssociationService.getInstance(); this.windows = WindowManagerService.getInstance(); this.bsaver = BeatSaverService.getInstance(); + this.songDetails = SongDetailsCacheService.getInstance(); + this.songCache = SongCacheService.getInstance(); + this.deepLink.addLinkOpenedListener(this.DEEP_LINKS.BeatSaver, link => { log.info("DEEP-LINK RECEIVED FROM", this.DEEP_LINKS.BeatSaver, link); @@ -100,34 +110,61 @@ export class LocalPlaylistsManagerService { this.windows.openWindow(`oneclick-download-playlist.html?playlistUrl=${downloadUrl}`); } - private getReadBPListOfFolder(folerPath: string): Observable> { - return new Observable>(obs => { + private readLocalBPListsOfFolder(folerPath: string, version?: BSVersion): Observable> { + return new Observable>(obs => { - const progress: Progression = { current: 0, total: 0, data: [] }; + const progress: Progression = { current: 0, total: 0, data: [] }; + const bpLists: LocalBPList[] = []; (async () => { + + await this.songDetails.waitLoaded(sToMs(15)); + if(!pathExistsSync(folerPath)) { throw new Error(`Playlists folder not found ${folerPath}`); } + const mapsFolder = version ? await this.maps.getMapsFolderPath(version) : null; const playlists = readdirSync(folerPath).filter(file => path.extname(file) === ".bplist"); progress.total = playlists.length; for (const playlist of playlists) { const playlistPath = path.join(folerPath, playlist); - const playlistContent = await this.readPlaylistFile(playlistPath); - progress.data.push(playlistContent); + const bpList = await this.readPlaylistFile(playlistPath); + + const localBpListSongs = bpList.songs.map(song => { + const localInfos = mapsFolder ? this.songCache.getMapInfoFromHash(song.hash) : null; + const coverPath = localInfos ? path.join(mapsFolder, localInfos.path, localInfos.info.rawInfo._coverImageFilename) : null; + const songFilePath = localInfos ? path.join(mapsFolder, localInfos.path, localInfos.info.rawInfo._songFilename) : null; + return ({ + song, + songDetails: this.songDetails.getSongDetails(song.hash), + coverUrl: (coverPath && pathExistsSync(coverPath)) ? pathToFileURL(coverPath).href : null, + songUrl: (songFilePath && pathExistsSync(songFilePath)) ? pathToFileURL(songFilePath).href : null, + } as LocalBpListSong) + }); + const localBpList: LocalBPList = { ...bpList, path: playlistPath, songs: localBpListSongs }; + bpLists.push(localBpList); progress.current += 1; obs.next(progress); } + + progress.data = bpLists; + obs.next(progress); })().catch(err => obs.error(err)).finally(() => obs.complete()); }); } - public getVersionPlaylists(version: BSVersion): Observable> { - return new Observable>(obs => { + public getVersionPlaylists(version: BSVersion): Observable> { + return new Observable>(obs => { this.getPlaylistsFolder(version) - .then(folder => this.getReadBPListOfFolder(folder).subscribe(obs)) + .then(folder => this.readLocalBPListsOfFolder(folder, version)) + .then(progress$ => lastValueFrom(progress$.pipe( + tap(progress => obs.next({...progress, data: []})), + ))) + .then(res => obs.next(res)) + .catch(err => obs.error(err)) + .finally(() => obs.complete()); }); } diff --git a/src/main/services/additional-content/maps/local-maps-manager.service.ts b/src/main/services/additional-content/maps/local-maps-manager.service.ts index d3172b6e..21bf9fca 100644 --- a/src/main/services/additional-content/maps/local-maps-manager.service.ts +++ b/src/main/services/additional-content/maps/local-maps-manager.service.ts @@ -25,6 +25,7 @@ import { IpcService } from "../../ipc.service"; import { SongDetailsCacheService } from "./song-details-cache.service"; import { sToMs } from "shared/helpers/time.helpers"; import { SongCacheService } from "./song-cache.service"; +import { pathToFileURL } from "url"; export class LocalMapsManagerService { private static instance: LocalMapsManagerService; @@ -38,6 +39,7 @@ export class LocalMapsManagerService { public static readonly LEVELS_ROOT_FOLDER = "Beat Saber_Data"; public static readonly CUSTOM_LEVELS_FOLDER = "CustomLevels"; + public static readonly RELATIVE_MAPS_FOLDER = path.join(LocalMapsManagerService.LEVELS_ROOT_FOLDER, LocalMapsManagerService.CUSTOM_LEVELS_FOLDER); public static readonly SHARED_MAPS_FOLDER = "SharedMaps"; private readonly DEEP_LINKS = { @@ -81,7 +83,7 @@ export class LocalMapsManagerService { public async getMapsFolderPath(version?: BSVersion): Promise { if (version) { - return path.join(await this.localVersion.getVersionPath(version), LocalMapsManagerService.LEVELS_ROOT_FOLDER, LocalMapsManagerService.CUSTOM_LEVELS_FOLDER); + return path.join(await this.localVersion.getVersionPath(version), LocalMapsManagerService.RELATIVE_MAPS_FOLDER); } const sharedMapsPath = path.join(await this.installLocation.sharedContentPath(), LocalMapsManagerService.SHARED_MAPS_FOLDER, LocalMapsManagerService.CUSTOM_LEVELS_FOLDER); if (!(await pathExist(sharedMapsPath))) { @@ -117,8 +119,8 @@ export class LocalMapsManagerService { private async loadMapInfoFromPath(mapPath: string): Promise { const getUrlsAndReturn = (rawInfo: RawMapInfoData, hash: string, mapPath: string) => { - const coverUrl = new URL(`file:///${path.join(mapPath, rawInfo._coverImageFilename)}`).href; - const songUrl = new URL(`file:///${path.join(mapPath, rawInfo._songFilename)}`).href; + const coverUrl = pathToFileURL(path.join(mapPath, rawInfo._coverImageFilename)).href; + const songUrl = pathToFileURL(path.join(mapPath, rawInfo._songFilename)).href; return { rawInfo, coverUrl, songUrl, hash, path: mapPath, songDetails: this.songDetailsCache.getSongDetails(hash) } as BsmLocalMap; }; diff --git a/src/main/services/additional-content/maps/song-cache.service.ts b/src/main/services/additional-content/maps/song-cache.service.ts index 48619351..fffac6fc 100644 --- a/src/main/services/additional-content/maps/song-cache.service.ts +++ b/src/main/services/additional-content/maps/song-cache.service.ts @@ -26,6 +26,11 @@ export class SongCacheService { return this.rawInfosCache.get(dirname); } + public getMapInfoFromHash(hash: string): { path: string, info: CachedRawInfoWithHash } | undefined { + const res = Object.entries(this.rawInfosCache.cache).find(([, info]) => info.hash === hash); + return res ? { path: res[0], info: res[1] } : undefined; + } + public setMapInfoFromDirname(dirname: string, info: CachedRawInfoWithHash): void { this.rawInfosCache.set(dirname, info); } diff --git a/src/main/services/additional-content/maps/song-details-cache.service.ts b/src/main/services/additional-content/maps/song-details-cache.service.ts index ceed522e..f353cf19 100644 --- a/src/main/services/additional-content/maps/song-details-cache.service.ts +++ b/src/main/services/additional-content/maps/song-details-cache.service.ts @@ -160,7 +160,7 @@ export class SongDetailsCacheService { })))); } - public getSongDetails(hash: string): any { + public getSongDetails(hash: string): SongDetails { return this.songDetailsCache[hash.toLocaleLowerCase()]; } diff --git a/src/main/services/window-manager.service.ts b/src/main/services/window-manager.service.ts index 0450eb40..50495cb2 100644 --- a/src/main/services/window-manager.service.ts +++ b/src/main/services/window-manager.service.ts @@ -47,7 +47,7 @@ export class WindowManagerService { if(isValidUrl(url)){ shell.openExternal(url); } - + return { action: "deny"} }); diff --git a/src/main/util.ts b/src/main/util.ts index c9dac068..f7857f7b 100644 --- a/src/main/util.ts +++ b/src/main/util.ts @@ -1,5 +1,5 @@ /* eslint import/prefer-default-export: off, import/no-mutable-exports: off */ -import { URL } from "url"; +import { URL, pathToFileURL } from "url"; import path from "path"; export let resolveHtmlPath: (htmlFileName: string) => string; @@ -12,6 +12,6 @@ if (process.env.NODE_ENV === "development") { }; } else { resolveHtmlPath = (htmlFileName: string) => { - return `file://${path.resolve(__dirname, "../renderer/", htmlFileName)}`; + return pathToFileURL(path.resolve(__dirname, "../renderer/", htmlFileName)).href; }; } 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 afb367c6..01f14efc 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 @@ -123,8 +123,8 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) { ]} > <> - - + + diff --git a/src/renderer/components/maps-playlists-panel/maps/local-maps-list-panel.component.tsx b/src/renderer/components/maps-playlists-panel/maps/local-maps-list-panel.component.tsx index 0b9d12a2..981b169a 100644 --- a/src/renderer/components/maps-playlists-panel/maps/local-maps-list-panel.component.tsx +++ b/src/renderer/components/maps-playlists-panel/maps/local-maps-list-panel.component.tsx @@ -9,15 +9,15 @@ import { VariableSizeList } from "react-window"; import { MapsRow } from "./maps-row.component"; import { debounceTime, last, tap } from "rxjs/operators"; import { useTranslation } from "renderer/hooks/use-translation.hook"; -import BeatWaitingImg from "../../../../../assets/images/apngs/beat-waiting.png"; import BeatConflict from "../../../../../assets/images/apngs/beat-conflict.png"; import { BsmImage } from "../../shared/bsm-image.component"; import { BsmButton } from "../../shared/bsm-button.component"; -import TextProgressBar from "../../progress-bar/text-progress-bar.component"; import { useChangeUntilEqual } from "renderer/hooks/use-change-until-equal.hook"; import { useService } from "renderer/hooks/use-service.hook"; import { FolderLinkState } from "renderer/services/version-folder-linker.service"; import { useOnUpdate } from "renderer/hooks/use-on-update.hook"; +import { useConstant } from "renderer/hooks/use-constant.hook"; +import { BsContentLoader } from "renderer/components/shared/bs-content-loader.component"; type Props = { version: BSVersion; @@ -28,7 +28,7 @@ type Props = { isActive?: boolean; }; -export const LocalMapsListPanel = forwardRef(({ version, className, filter, search, linkedState, isActive }: Props, forwardRef) => { +export const LocalMapsListPanel = forwardRef(({ version, className, filter, search, linkedState, isActive }, forwardRef) => { const mapsManager = useService(MapsManagerService); const mapsDownloader = useService(MapsDownloaderService); @@ -42,7 +42,7 @@ export const LocalMapsListPanel = forwardRef(({ version, className, filter, sear const isActiveOnce = useChangeUntilEqual(isActive, { untilEqual: true }); const [linked, setLinked] = useState(false); - const [loadPercent$] = useState(new BehaviorSubject(0)); + const loadPercent$ = useConstant(() => new BehaviorSubject(0)); useImperativeHandle( forwardRef, @@ -372,11 +372,7 @@ export const LocalMapsListPanel = forwardRef(({ version, className, filter, sear if (!maps) { return (
-
- - {t("modals.download-maps.loading-maps")} - -
+
); } 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 4afd2595..b94c1d09 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 @@ -76,7 +76,7 @@ export const MapItem = memo(({ hash, title, autor, songAutor, coverUrl, songUrl, const previewUrl = mapId ? `https://allpoland.github.io/ArcViewer/?id=${mapId}` : null; const mapUrl = mapId ? `https://beatsaver.com/maps/${mapId}` : null; const authorUrl = autorId ? `https://beatsaver.com/profile/${autorId}` : null; - const createdDate = createdAt ? dateFormat(createdAt, "d mmm yyyy") : null; + const createdDate = createdAt ? dateFormat(createdAt * 1000, "d mmm yyyy") : null; const likesText = likes ? Intl.NumberFormat(undefined, { notation: "compact" }).format(likes).split(" ").join("") : null; const durationText = (() => { 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 63dd7fcd..32df4cc8 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 @@ -1,8 +1,16 @@ +import { forwardRef, useState } from "react"; +import { BsContentLoader } from "renderer/components/shared/bs-content-loader.component"; import { useChangeUntilEqual } from "renderer/hooks/use-change-until-equal.hook"; +import { useConstant } from "renderer/hooks/use-constant.hook"; import { useOnUpdate } from "renderer/hooks/use-on-update.hook"; +import { useService } from "renderer/hooks/use-service.hook"; +import { PlaylistsManagerService } from "renderer/services/playlists-manager.service"; import { FolderLinkState } from "renderer/services/version-folder-linker.service"; +import { BehaviorSubject, finalize, lastValueFrom, map, tap } from "rxjs"; import { BSVersion } from "shared/bs-version.interface"; import { noop } from "shared/helpers/function.helpers"; +import { LocalBPList } from "shared/models/playlists/local-playlist.models"; +import { PlaylistItem } from "./playlist-item.component"; type Props = { version: BSVersion; @@ -11,19 +19,116 @@ type Props = { isActive?: boolean; }; -export function LocalPlaylistsListPanel({ version, className, isActive, linkedState }: Props) { +export const LocalPlaylistsListPanel = forwardRef(({ version, className, isActive, linkedState }, forwardedRef) => { + + const playlistService = useService(PlaylistsManagerService); const isActiveOnce = useChangeUntilEqual(isActive, { untilEqual: true }); + const [playlistsLoading, setPlaylistsLoading] = useState(false); + const [playlists, setPlaylists] = useState([]); + const loadPercent$ = useConstant(() => new BehaviorSubject(0)); + + const loadPlaylists = (): Promise => { + setPlaylistsLoading(true); + const obs = playlistService.getVersionPlaylists(version).pipe( + tap({ next: load => loadPercent$.next((load.current / load.total) * 100)}), + tap({ next: console.log}), + map(load => load.data), + finalize(() => setPlaylistsLoading(false)) + ); + + return lastValueFrom(obs); + } + + const getNbMappersOfPlaylist = (playlist: LocalBPList) => { + return new Set(playlist.songs.map(s => s.songDetails?.uploader?.id ?? s.song.hash)).size; + }; + + const getDurationOfPlaylist = (playlist: LocalBPList) => { + return playlist.songs.reduce((acc, s) => acc + (s.songDetails?.duration ?? 0), 0); + } + + const getMinNpsOfPlaylist = (playlist: LocalBPList) => { + let min = Infinity; + playlist.songs.forEach(s => { + s.songDetails?.difficulties.forEach(d => { + if(d.nps < min){ + min = d.nps; + } + }) + }); + return min === Infinity ? null : min; + } + + const getMaxNpsOfPlaylist = (playlist: LocalBPList) => { + let max = -Infinity; + playlist.songs.forEach(s => { + s.songDetails?.difficulties.forEach(d => { + if(d.nps > max){ + max = d.nps; + } + }) + }); + return max === -Infinity ? null : max; + } + + const getSongsOfPlaylist = (playlist: LocalBPList) => { + return playlist.songs.map(s => ({ url: s.songUrl ?? `https://cdn.beatsaver.com/${s.song.hash}.mp3`, bpm: s.songDetails?.bpm ?? 0})); + } + useOnUpdate(() => { if(!isActiveOnce){ return noop(); } - // load playlists + loadPlaylists().then(loadedPlaylists => { + setPlaylists(() => loadedPlaylists); + }).catch(() => { + setPlaylists([]) + }).finally(() => { + loadPercent$.next(0); + }); - }, [isActiveOnce]); + }, [isActiveOnce, version]); + + const render = () => { + if(playlistsLoading){ + return ( + + ) + } + + if (playlists.length){ + return ( + <> +
    + {playlists.map(p => + + )} +
+
+ + ) + } + + return null; + } return ( -
local-playlists-list-panel.component
+
+ {render()} +
) -} +}); 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 new file mode 100644 index 00000000..75d95f77 --- /dev/null +++ b/src/renderer/components/maps-playlists-panel/playlists/playlist-item.component.tsx @@ -0,0 +1,79 @@ +import { motion } from 'framer-motion'; +import { BsmImage } from 'renderer/components/shared/bsm-image.component'; +import { ClockIcon } from 'renderer/components/svgs/icons/clock-icon.component'; +import { MapIcon } from 'renderer/components/svgs/icons/map-icon.component'; +import { PersonIcon } from 'renderer/components/svgs/icons/person-icon.component'; +import { useThemeColor } from 'renderer/hooks/use-theme-color.hook'; +import dateFormat from 'dateformat'; +import { NpsIcon } from 'renderer/components/svgs/icons/nps-icon.component'; +import { useService } from 'renderer/hooks/use-service.hook'; +import { AudioPlayerService } from 'renderer/services/audio-player.service'; +import { filter, lastValueFrom, skip, take } from 'rxjs'; + +type Props = { + id: string; + className?: string; + title?: string; + author?: string; + coverBase64?: string; + coverUrl?: string; + nbMaps?: number; + nbMappers?: number; + duration?: number; + minNps?: number; + maxNps?: number; + songs?: { url: string, bpm: number }[]; +} + +export function PlaylistItem({ id, className, title, author, coverUrl, coverBase64, duration, nbMaps, nbMappers, minNps, maxNps, songs }: Props) { + + const player = useService(AudioPlayerService); + + const firstColor = useThemeColor("first-color"); + + const nbMapsText = nbMaps ? Intl.NumberFormat(undefined, { notation: "compact" }).format(nbMaps).trim() : null; + const nbMappersText = nbMappers ? Intl.NumberFormat(undefined, { notation: "compact" }).format(nbMappers).trim() : null; + const minNpsText = minNps ? Math.round(minNps * 10) / 10 : null; + const maxNpsText = maxNps ? Math.round(maxNps * 10) / 10 : null; + + const durationText = (() => { + if (!duration) { + return null; + } + const date = new Date(0); + date.setSeconds(duration); + return duration > 3600 ? dateFormat(date, "h:MM:ss") : dateFormat(date, "MM:ss"); + })(); + + const startPlay = async () => { + if (!songs || songs.length === 0) { + return; + } + const playlist = songs.map(s => ({ src: s.url, bpm: s.bpm })); + player.playlist(playlist, 0); + } + + return ( + startPlay()}> +
+ +
+
+
+ +
+
+

{title}

+

Créé par {author}

+ +
+ { nbMapsText &&
{nbMapsText}
} + { nbMappersText &&
{nbMappersText}
} + { durationText &&
{durationText}
} + { (minNps && maxNps) &&
{`${minNpsText} - ${maxNpsText}`}
} +
+ +
+ + ) +} diff --git a/src/renderer/components/models-management/models-grid.component.tsx b/src/renderer/components/models-management/models-grid.component.tsx index 024e4938..1703bad0 100644 --- a/src/renderer/components/models-management/models-grid.component.tsx +++ b/src/renderer/components/models-management/models-grid.component.tsx @@ -1,5 +1,5 @@ import { BSVersion } from "shared/bs-version.interface"; -import { useRef, forwardRef, useImperativeHandle } from "react"; +import { forwardRef, useImperativeHandle } from "react"; import { MSModelType } from "shared/models/models/model-saber.model"; import { useOnUpdate } from "renderer/hooks/use-on-update.hook"; import { useConstant } from "renderer/hooks/use-constant.hook"; @@ -9,9 +9,7 @@ import { BsmLocalModel } from "shared/models/models/bsm-local-model.interface"; import { ModelItem } from "./model-item.component"; import { useBehaviorSubject } from "renderer/hooks/use-behavior-subject.hook"; import { BsmImage } from "../shared/bsm-image.component"; -import BeatWaitingImg from "../../../../assets/images/apngs/beat-waiting.png"; import BeatConflict from "../../../../assets/images/apngs/beat-conflict.png"; -import TextProgressBar from "../progress-bar/text-progress-bar.component"; import { BehaviorSubject, distinctUntilChanged, map, startWith } from "rxjs"; import { BsmButton } from "../shared/bsm-button.component"; import equal from "fast-deep-equal"; @@ -20,6 +18,7 @@ import { MODEL_TYPE_FOLDERS } from "shared/models/models/constants"; import { useService } from "renderer/hooks/use-service.hook"; import { ModelsDownloaderService } from "renderer/services/models-management/models-downloader.service"; import { useTranslation } from "renderer/hooks/use-translation.hook"; +import { BsContentLoader } from "../shared/bs-content-loader.component"; type Props = { className?: string; @@ -30,14 +29,12 @@ type Props = { downloadModels?: () => void; }; -export const ModelsGrid = forwardRef(({ className, version, type, search, active, downloadModels }: Props, forwardRef) => { +export const ModelsGrid = forwardRef(({ className, version, type, search, active, downloadModels }, forwardRef) => { const modelsManager = useService(ModelsManagerService); const modelsDownloader = useService(ModelsDownloaderService); const t = useTranslation(); - const ref = useRef(); - const [models, setModelsLoadObservable, , setModels] = useSwitchableObservable(); const progress$ = useConstant(() => new BehaviorSubject(0)); const [modelsSelected, modelsSelected$] = useBehaviorSubject([]); @@ -199,11 +196,7 @@ export const ModelsGrid = forwardRef(({ className, version, type, search, active const renderContent = () => { if (isLoading) { return ( -
- - {t("models.panel.grid.loading")} - -
+ ); } @@ -236,7 +229,7 @@ export const ModelsGrid = forwardRef(({ className, version, type, search, active }; return ( -
+
{renderContent()}
); diff --git a/src/renderer/components/nav-bar/bsmanager-icon.component.tsx b/src/renderer/components/nav-bar/bsmanager-icon.component.tsx index b4e1355d..1d6ce561 100644 --- a/src/renderer/components/nav-bar/bsmanager-icon.component.tsx +++ b/src/renderer/components/nav-bar/bsmanager-icon.component.tsx @@ -8,7 +8,7 @@ import { useService } from "renderer/hooks/use-service.hook"; // Thanks to cheddZy for the icon : https://github.com/cheddZy export const BsManagerIcon = memo(({ className }: { className?: string }) => { - + const audioPlayer = useService(AudioPlayerService); const { firstColor, secondColor } = useThemeColor(); @@ -27,6 +27,8 @@ export const BsManagerIcon = memo(({ className }: { className?: string }) => { const clickAction = () => { if (playing) { audioPlayer.pause(); + } else { + audioPlayer.resume(); } }; diff --git a/src/renderer/components/shared/bs-content-loader.component.tsx b/src/renderer/components/shared/bs-content-loader.component.tsx new file mode 100644 index 00000000..00148cd6 --- /dev/null +++ b/src/renderer/components/shared/bs-content-loader.component.tsx @@ -0,0 +1,24 @@ +import { useTranslation } from "renderer/hooks/use-translation.hook"; +import TextProgressBar from "../progress-bar/text-progress-bar.component"; +import { BsmImage } from "./bsm-image.component"; +import BeatWaitingImg from "../../../../assets/images/apngs/beat-waiting.png"; +import { Observable } from "rxjs"; + +type Props = { + className?: string; + value$: Observable; + text: string; +} + +export function BsContentLoader({className, value$, text}: Props) { + + const t = useTranslation(); + + return ( +
+ + {t(text)} + +
+ ) +} diff --git a/src/renderer/components/shared/bsm-image.component.tsx b/src/renderer/components/shared/bsm-image.component.tsx index bc245949..bddb0914 100644 --- a/src/renderer/components/shared/bsm-image.component.tsx +++ b/src/renderer/components/shared/bsm-image.component.tsx @@ -1,20 +1,26 @@ -import { CSSProperties, forwardRef, SyntheticEvent, useState } from "react"; +import { ComponentProps, CSSProperties, forwardRef, SyntheticEvent, useState } from "react"; type Props = { className?: string; - image: string; + image?: string; + base64?: string; errorImage?: string; placeholder?: string; loading?: "lazy" | "eager"; style?: CSSProperties; title?: string; - onClick?: (e: MouseEvent) => void; + onClick?: ComponentProps<"img">["onClick"] }; -export const BsmImage = forwardRef(({ className, image, errorImage, placeholder, loading, style, title, onClick }: Props, ref) => { +export const BsmImage = forwardRef(({ className, image, base64, errorImage, placeholder, loading, style, title, onClick }, ref) => { const [isLoaded, setIsLoaded] = useState(false); - image = image || placeholder || errorImage; + const getBase64Url = () => { + if(base64?.startsWith("data:image")){ return base64; } + return base64 ? `data:image/png;base64,${base64}` : undefined; + }; + + const imageSrc = image || getBase64Url() || placeholder || errorImage; const styles: CSSProperties = (() => { return { @@ -34,5 +40,5 @@ export const BsmImage = forwardRef(({ className, image, errorImage, placeholder, setIsLoaded(() => true); }; - return onClick?.(e)} alt=" " decoding="async" />; + return onClick?.(e)} alt=" " decoding="async" />; }); diff --git a/src/renderer/components/svgs/icons/clock-icon.component.tsx b/src/renderer/components/svgs/icons/clock-icon.component.tsx new file mode 100644 index 00000000..8751da3f --- /dev/null +++ b/src/renderer/components/svgs/icons/clock-icon.component.tsx @@ -0,0 +1,9 @@ +import { createSvgIcon } from "../svg-icon.type" + +export const ClockIcon = createSvgIcon((props, ref) => { + return ( + + + + ) +}); diff --git a/src/renderer/components/svgs/icons/nps-icon.component.tsx b/src/renderer/components/svgs/icons/nps-icon.component.tsx new file mode 100644 index 00000000..c93f240d --- /dev/null +++ b/src/renderer/components/svgs/icons/nps-icon.component.tsx @@ -0,0 +1,10 @@ +import { createSvgIcon } from "../svg-icon.type"; + +export const NpsIcon = createSvgIcon((props, ref) => { + return ( + + + + + ) +}); diff --git a/src/renderer/components/svgs/icons/person-icon.component.tsx b/src/renderer/components/svgs/icons/person-icon.component.tsx new file mode 100644 index 00000000..0cc82c42 --- /dev/null +++ b/src/renderer/components/svgs/icons/person-icon.component.tsx @@ -0,0 +1,9 @@ +import { createSvgIcon } from '../svg-icon.type'; + +export const PersonIcon = createSvgIcon((props, ref) => { + return ( + + + + ) +}); diff --git a/src/renderer/components/svgs/icons/timer-fill.component.tsx b/src/renderer/components/svgs/icons/timer-fill.component.tsx index 37593bd9..8504be58 100644 --- a/src/renderer/components/svgs/icons/timer-fill.component.tsx +++ b/src/renderer/components/svgs/icons/timer-fill.component.tsx @@ -2,7 +2,7 @@ import { CSSProperties } from "react"; export function TimerFillIcon(props: { className?: string; style?: CSSProperties }) { return ( - + ); diff --git a/src/renderer/hooks/use-change-until-equal.hook.ts b/src/renderer/hooks/use-change-until-equal.hook.ts index d20d64a8..b44bd9e9 100644 --- a/src/renderer/hooks/use-change-until-equal.hook.ts +++ b/src/renderer/hooks/use-change-until-equal.hook.ts @@ -19,10 +19,7 @@ export function useChangeUntilEqual(variableValue: T, { untilEqual const isEqual = equal(variableValue, untilEqualRef.current); - console.log("isEqual", isEqual, variableValue, untilEqualRef.current); - if(!isEqual){ - console.log("setValue", variableValue); return setValue(variableValue); } diff --git a/src/renderer/services/audio-player.service.ts b/src/renderer/services/audio-player.service.ts index ee63aca0..2d7fec72 100644 --- a/src/renderer/services/audio-player.service.ts +++ b/src/renderer/services/audio-player.service.ts @@ -50,6 +50,15 @@ export class AudioPlayerService { return this.player.play(); } + public playlist(songs: {src: string, bpm: number}[], index: number): void { + this.play(songs[index].src, songs[index].bpm); + this.player.onended = () => { + if (index < songs.length - 1) { + this.playlist(songs, index + 1); + } + }; + } + public pause(): void { this._playing$.next(false); this.player.pause(); @@ -83,7 +92,10 @@ export class AudioPlayerService { } public toggleMute(): void { - this.muted ? this.unmute() : this.mute(); + if(this.muted){ + return this.unmute(); + } + this.mute(); } public get src$(): Observable { @@ -114,6 +126,9 @@ export class AudioPlayerService { public get muted(): boolean { return this.player.muted; } + public get paused(): boolean { + return this.player.paused; + } } interface PlayerVolume { diff --git a/src/renderer/services/playlists-manager.service.ts b/src/renderer/services/playlists-manager.service.ts index c974cffe..f45ebee3 100644 --- a/src/renderer/services/playlists-manager.service.ts +++ b/src/renderer/services/playlists-manager.service.ts @@ -2,6 +2,8 @@ import { BSVersion } from "shared/bs-version.interface"; import { IpcService } from "./ipc.service"; import { Observable, lastValueFrom } from "rxjs"; import { FolderLinkState, VersionFolderLinkerService } from "./version-folder-linker.service"; +import { Progression } from "main/helpers/fs.helpers"; +import { LocalBPList } from "shared/models/playlists/local-playlist.models"; export class PlaylistsManagerService { private static instance: PlaylistsManagerService; @@ -23,8 +25,8 @@ export class PlaylistsManagerService { this.linker = VersionFolderLinkerService.getInstance(); } - public getVersionPlaylists(version: BSVersion): Promise { - return this.ipc.sendV2("get-version-playlists", version); + public getVersionPlaylists(version: BSVersion): Observable> { + return this.ipc.sendV2("get-version-playlists", { args: version }); } public isDeepLinksEnabled(): Promise { diff --git a/src/shared/models/playlists/local-playlist.models.ts b/src/shared/models/playlists/local-playlist.models.ts new file mode 100644 index 00000000..f69d22ea --- /dev/null +++ b/src/shared/models/playlists/local-playlist.models.ts @@ -0,0 +1,13 @@ +import { SongDetails } from "../maps"; +import { BPList, PlaylistSong } from "./playlist.interface"; + +export interface LocalBPList extends BPList { + path: string; +} + +export interface LocalBpListSong { + song: PlaylistSong; + songDetails?: SongDetails; + songUrl?: string; + coverUrl?: string; +} diff --git a/src/shared/models/playlists/playlist.interface.ts b/src/shared/models/playlists/playlist.interface.ts index c483eb96..5fde662d 100644 --- a/src/shared/models/playlists/playlist.interface.ts +++ b/src/shared/models/playlists/playlist.interface.ts @@ -1,13 +1,13 @@ -import { BsvMapDetail } from "../maps"; +import { BsvMapDetail, SongDetails } from "../maps"; import { BsmLocalMap } from "../maps/bsm-local-map.interface"; -export interface BPList { +export interface BPList { playlistTitle: string; playlistAuthor: string; playlistDescription?: string; image: string; customData: unknown; - songs: PlaylistSong[]; + songs: SongType[]; } export interface PlaylistSong { @@ -15,6 +15,7 @@ export interface PlaylistSong { hash: string; songName: string; uploader?: string; + songDetails?: SongDetails; } export interface DownloadPlaylistProgressionData {