mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
[feature-107] advancement on playlists feature
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import { LocalBPList } from "shared/models/playlists/local-playlist.models";
|
||||
import { LocalPlaylistsManagerService } from "../services/additional-content/local-playlists-manager.service";
|
||||
import { IpcService } from "../services/ipc.service";
|
||||
import { of } from "rxjs";
|
||||
import { BPList } from "shared/models/playlists/playlist.interface";
|
||||
import path from "path";
|
||||
import { pathToFileURL } from "url";
|
||||
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
@@ -27,11 +31,26 @@ ipc.on("is-playlists-deep-links-enabled", (args, reply) => {
|
||||
ipc.on("install-playlist", (args, reply) => {
|
||||
const playlists = LocalPlaylistsManagerService.getInstance();
|
||||
|
||||
if(args.playlist?.customData?.syncURL){
|
||||
return reply(playlists.downloadPlaylist(args.playlist.customData.syncURL, args.version));
|
||||
const isLocalBPList = (bpList: BPList): bpList is LocalBPList => {
|
||||
return (bpList as LocalBPList).path !== undefined && path.isAbsolute((bpList as LocalBPList).path);
|
||||
}
|
||||
|
||||
reply(playlists.downloadPlaylistSongs(args.playlist.songs, args.version));
|
||||
const playListUrl = (() => {
|
||||
if (args.playlist.customData?.syncURL) {
|
||||
return args.playlist.customData.syncURL;
|
||||
}
|
||||
if (isLocalBPList(args.playlist)) {
|
||||
return args.playlist.path;
|
||||
}
|
||||
return undefined;
|
||||
})();
|
||||
|
||||
return reply(playlists.downloadPlaylist({
|
||||
bpListUrl: playListUrl,
|
||||
version: args.version,
|
||||
ignoreSongsHashs: args.ignoreSongsHashs,
|
||||
dest: (args.playlist as LocalBPList)?.path
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -41,3 +41,7 @@ ipc.on("notify-system", (args, reply) => {
|
||||
const systemNotification = NotificationService.getInstance();
|
||||
reply(of(systemNotification.notify(args)));
|
||||
});
|
||||
|
||||
ipc.on("view-path-in-explorer", (args, reply) => {
|
||||
reply(of(shell.showItemInFolder(args)));
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import path from "path";
|
||||
import { Observable, lastValueFrom, tap } from "rxjs";
|
||||
import { Observable, Subject, from, lastValueFrom, mergeMap, take, takeUntil, tap } from "rxjs";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { BSLocalVersionService } from "../bs-local-version.service";
|
||||
import { DeepLinkService } from "../deep-link.service";
|
||||
@@ -10,7 +10,7 @@ import { WindowManagerService } from "../window-manager.service";
|
||||
import { BPList, DownloadPlaylistProgressionData, PlaylistSong } from "shared/models/playlists/playlist.interface";
|
||||
import { readFileSync } from "fs";
|
||||
import { BeatSaverService } from "../thrid-party/beat-saver/beat-saver.service";
|
||||
import { copy, copyFile, ensureDir, pathExists, pathExistsSync, readdirSync, realpath } from "fs-extra";
|
||||
import { copy, copyFile, ensureDir, pathExists, pathExistsSync, readdirSync, realpath, writeFile, writeFileSync } from "fs-extra";
|
||||
import { Progression, pathExist, unlinkPath } from "../../helpers/fs.helpers";
|
||||
import { FileAssociationService } from "../file-association.service";
|
||||
import { SongDetailsCacheService } from "./maps/song-details-cache.service";
|
||||
@@ -18,6 +18,8 @@ import { sToMs } from "shared/helpers/time.helpers";
|
||||
import { LocalBPList, LocalBPListsDetails } from "shared/models/playlists/local-playlist.models";
|
||||
import { SongCacheService } from "./maps/song-cache.service";
|
||||
import { InstallationLocationService } from "../installation-location.service";
|
||||
import sanitize from "sanitize-filename";
|
||||
import { isValidUrl } from "shared/helpers/url.helpers";
|
||||
|
||||
export class LocalPlaylistsManagerService {
|
||||
private static instance: LocalPlaylistsManagerService;
|
||||
@@ -80,27 +82,37 @@ export class LocalPlaylistsManagerService {
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
private async installBPListFile(bslistSource: string, version: BSVersion): Promise<string> {
|
||||
const playlistFolder = await this.getPlaylistsFolder(version);
|
||||
const isLocalFile = await pathExists(bslistSource).catch(e => { log.error(e); return false; });
|
||||
const filename = isLocalFile ? path.basename(bslistSource) : new URL(bslistSource).pathname.split('/').pop()
|
||||
const destFile = path.join(playlistFolder, filename);
|
||||
private async installBPListFile(opt: {
|
||||
bslistSource: string,
|
||||
version?: BSVersion,
|
||||
dest?: string
|
||||
}): Promise<{path: string, localBPList: LocalBPList}> {
|
||||
const bplist = await this.readPlaylistFromSource(opt.bslistSource);
|
||||
|
||||
if (isLocalFile) {
|
||||
return copyFile(bslistSource, destFile).then(() => destFile);
|
||||
}
|
||||
return lastValueFrom(this.request.downloadFile(bslistSource, destFile)).then(res => res.data);
|
||||
const dest = await (async () => {
|
||||
if(opt.dest && path.isAbsolute(opt.dest) && path.extname(opt.dest) === ".bplist") { return opt.dest; }
|
||||
const playlistFolder = await this.getPlaylistsFolder(opt.version);
|
||||
return path.join(playlistFolder, `${sanitize(bplist.playlistTitle)}.bplist`);
|
||||
})();
|
||||
|
||||
writeFileSync(dest, JSON.stringify(bplist, null, 2));
|
||||
|
||||
const localBPList: LocalBPList = { ...bplist, path: dest };
|
||||
|
||||
return { path: dest, localBPList };
|
||||
}
|
||||
|
||||
|
||||
private async readPlaylistFile(filePath: string): Promise<BPList> {
|
||||
if (!(await pathExist(filePath))) {
|
||||
throw new Error(`bplist file not exist at ${filePath}`);
|
||||
private async readPlaylistFromSource(source: string): Promise<BPList> {
|
||||
const isLocalFile = await pathExists(source).catch(e => { log.error(e); return false; });
|
||||
|
||||
if(!isLocalFile && !isValidUrl(source)) {
|
||||
throw new Error(`Invalid source ${source}`);
|
||||
}
|
||||
|
||||
const rawContent = readFileSync(filePath).toString();
|
||||
const bpList = isLocalFile ? JSON.parse(readFileSync(source).toString()) : await this.request.getJSON<BPList>(source);
|
||||
|
||||
return JSON.parse(rawContent);
|
||||
return bpList;
|
||||
}
|
||||
|
||||
private openOneClickDownloadPlaylistWindow(downloadUrl: string): void {
|
||||
@@ -124,7 +136,7 @@ export class LocalPlaylistsManagerService {
|
||||
|
||||
for (const playlist of playlists) {
|
||||
const playlistPath = path.join(folerPath, playlist);
|
||||
const bpList = await this.readPlaylistFile(playlistPath);
|
||||
const bpList = await this.readPlaylistFromSource(playlistPath);
|
||||
|
||||
|
||||
const localBpList: LocalBPList = { ...bpList, path: playlistPath };
|
||||
@@ -139,6 +151,32 @@ export class LocalPlaylistsManagerService {
|
||||
});
|
||||
}
|
||||
|
||||
public getLocalBPListDetails(localBPList: LocalBPList): LocalBPListsDetails {
|
||||
|
||||
const tryExtractPlaylistId = (url: string) => {
|
||||
const regex = /\/id\/(\d+)\/download/;
|
||||
const match = url.match(regex);
|
||||
return match ? Number(match[1]) : undefined;
|
||||
}
|
||||
|
||||
const bpListDetails: LocalBPListsDetails = {
|
||||
...localBPList,
|
||||
nbMaps: localBPList.songs?.length ?? 0,
|
||||
id: localBPList.customData?.syncURL ? tryExtractPlaylistId(localBPList.customData.syncURL) : undefined
|
||||
}
|
||||
|
||||
const songsDetails = localBPList.songs?.map(s => this.songDetails.getSongDetails(s.hash));
|
||||
|
||||
if(songsDetails){
|
||||
bpListDetails.duration = songsDetails.reduce((acc, song) => acc + song.duration, 0);
|
||||
bpListDetails.nbMappers = new Set(songsDetails.map(s => s.uploader.id)).size;
|
||||
bpListDetails.minNps = Math.min(...songsDetails.map(s => Math.min(...s.difficulties.map(d => d.nps || 0))));
|
||||
bpListDetails.maxNps = Math.max(...songsDetails.map(s => Math.max(...s.difficulties.map(d => d.nps || 0))));
|
||||
}
|
||||
|
||||
return bpListDetails;
|
||||
}
|
||||
|
||||
public getVersionPlaylistsDetails(version: BSVersion): Observable<Progression<LocalBPListsDetails[]>> {
|
||||
|
||||
return new Observable<Progression<LocalBPListsDetails[]>>(obs => {
|
||||
@@ -151,31 +189,9 @@ export class LocalPlaylistsManagerService {
|
||||
|
||||
await this.songDetails.waitLoaded(sToMs(15));
|
||||
|
||||
const tryExtractPlaylistId = (url: string) => {
|
||||
const regex = /\/id\/(\d+)\/download/;
|
||||
const match = url.match(regex);
|
||||
return match ? Number(match[1]) : undefined;
|
||||
}
|
||||
|
||||
const bpListsDetails: LocalBPListsDetails[] = [];
|
||||
for(const bpList of localBPListsRes.data){
|
||||
|
||||
const bpListDetails: LocalBPListsDetails = {
|
||||
...bpList,
|
||||
nbMaps: bpList.songs?.length ?? 0,
|
||||
id: bpList.customData?.syncURL ? tryExtractPlaylistId(bpList.customData.syncURL) : undefined
|
||||
}
|
||||
|
||||
const songsDetails = bpList.songs?.map(s => this.songDetails.getSongDetails(s.hash));
|
||||
|
||||
if(songsDetails){
|
||||
bpListDetails.duration = songsDetails.reduce((acc, song) => acc + song.duration, 0);
|
||||
bpListDetails.nbMappers = new Set(songsDetails.map(s => s.uploader.id)).size;
|
||||
bpListDetails.minNps = Math.min(...songsDetails.map(s => Math.min(...s.difficulties.map(d => d.nps || 0))));
|
||||
bpListDetails.maxNps = Math.max(...songsDetails.map(s => Math.max(...s.difficulties.map(d => d.nps || 0))));
|
||||
}
|
||||
|
||||
bpListsDetails.push(bpListDetails);
|
||||
bpListsDetails.push(this.getLocalBPListDetails(bpList));
|
||||
}
|
||||
|
||||
obs.next({...localBPListsRes, data: bpListsDetails});
|
||||
@@ -185,23 +201,34 @@ export class LocalPlaylistsManagerService {
|
||||
});
|
||||
}
|
||||
|
||||
public downloadPlaylistSongs(bpList: PlaylistSong[], version: BSVersion): Observable<Progression<DownloadPlaylistProgressionData>> {
|
||||
public downloadPlaylistSongs(localBPList: LocalBPList, ignoreSongsHashs: string[] = [], version: BSVersion): Observable<Progression<DownloadPlaylistProgressionData>> {
|
||||
|
||||
let destroyed = false;
|
||||
|
||||
return new Observable<Progression<DownloadPlaylistProgressionData>>(obs => {
|
||||
(async () => {
|
||||
const progress: Progression<DownloadPlaylistProgressionData> = {
|
||||
total: bpList.length,
|
||||
total: localBPList.songs.length,
|
||||
current: 0,
|
||||
data: {
|
||||
downloadedMaps: [],
|
||||
currentDownload: null,
|
||||
playlistInfos: null,
|
||||
playlistPath: null,
|
||||
playlist: this.getLocalBPListDetails(localBPList),
|
||||
}
|
||||
};
|
||||
|
||||
obs.next(progress);
|
||||
|
||||
for (const song of bpList) {
|
||||
for (const song of localBPList.songs) {
|
||||
|
||||
if(destroyed) { break; }
|
||||
|
||||
if(ignoreSongsHashs.includes(song.hash)) {
|
||||
progress.current += 1;
|
||||
obs.next(progress);
|
||||
continue;
|
||||
}
|
||||
|
||||
const [ mapDetail ] = await this.bsaver.getMapDetailsFromHashs([song.hash]);
|
||||
|
||||
if(!mapDetail) {
|
||||
@@ -219,24 +246,40 @@ export class LocalPlaylistsManagerService {
|
||||
})()
|
||||
.catch(err => obs.error(err))
|
||||
.finally(() => obs.complete());
|
||||
|
||||
return () => {
|
||||
destroyed = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public downloadPlaylist(bpListUrl: string, version: BSVersion): Observable<Progression<DownloadPlaylistProgressionData>> {
|
||||
public downloadPlaylist({ bpListUrl, version, ignoreSongsHashs = [], dest }: {
|
||||
bpListUrl: string,
|
||||
version?: BSVersion
|
||||
ignoreSongsHashs?: string[]
|
||||
dest?: string
|
||||
}): Observable<Progression<DownloadPlaylistProgressionData>> {
|
||||
|
||||
const destroyed$ = new Subject<void>()
|
||||
|
||||
return new Observable<Progression<DownloadPlaylistProgressionData>>(obs => {
|
||||
(async () => {
|
||||
|
||||
const bpListFilePath = await this.installBPListFile(bpListUrl, version);
|
||||
const bpList = await this.readPlaylistFile(bpListFilePath);
|
||||
const { localBPList } = await this.installBPListFile({ bslistSource: bpListUrl, version, dest });
|
||||
|
||||
await lastValueFrom(this.downloadPlaylistSongs(bpList.songs, version).pipe(
|
||||
await lastValueFrom(this.downloadPlaylistSongs(localBPList, ignoreSongsHashs, version).pipe(
|
||||
tap({ next: p => obs.next(p) }),
|
||||
takeUntil(destroyed$),
|
||||
));
|
||||
|
||||
})()
|
||||
.catch(err => obs.error(err))
|
||||
.finally(() => obs.complete());
|
||||
|
||||
return () => {
|
||||
destroyed$.next();
|
||||
destroyed$.complete();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
@@ -246,7 +289,7 @@ export class LocalPlaylistsManagerService {
|
||||
return new Observable<Progression>(obs => {
|
||||
(async () => {
|
||||
|
||||
const bpList = await this.readPlaylistFile(opt.path);
|
||||
const bpList = await this.readPlaylistFromSource(opt.path);
|
||||
|
||||
const progress: Progression = { current: 0, total: opt.deleteMaps ? bpList.songs.length + 1 : 1};
|
||||
|
||||
@@ -277,19 +320,19 @@ export class LocalPlaylistsManagerService {
|
||||
(async () => {
|
||||
const versions = await this.versions.getInstalledVersions();
|
||||
|
||||
const download$ = this.downloadPlaylist(bpListUrl, versions.pop()).pipe(tap({
|
||||
const download$ = this.downloadPlaylist({ bpListUrl, version: versions.pop() }).pipe(tap({
|
||||
next: progress => obs.next(progress),
|
||||
error: err => obs.error(err),
|
||||
}));
|
||||
|
||||
const { data: {downloadedMaps, playlistPath} } = await lastValueFrom(download$);
|
||||
const { data: {downloadedMaps, playlist} } = await lastValueFrom(download$);
|
||||
|
||||
if(downloadedMaps?.length === 0 || !playlistPath) { return; }
|
||||
if(downloadedMaps?.length === 0 || !playlist.path) { return; }
|
||||
|
||||
const realSourceMapsFolder = await realpath(path.dirname(downloadedMaps[0].path));
|
||||
|
||||
for (const version of versions) {
|
||||
await this.installBPListFile(playlistPath, version);
|
||||
await this.installBPListFile({ bslistSource: playlist.path, version});
|
||||
|
||||
const versionMapsFolder = await this.maps.getMapsFolderPath(version);
|
||||
const realDestMapsFolder = await realpath(versionMapsFolder).catch(e => {
|
||||
|
||||
@@ -268,6 +268,8 @@ export class LocalMapsManagerService {
|
||||
throw new Error("Cannot download map, no hash found");
|
||||
}
|
||||
|
||||
log.info("Downloading map", map.name, map.id);
|
||||
|
||||
const zipUrl = map.versions.at(0).downloadURL;
|
||||
const mapFolderName = sanitize(`${map.id}-${map.name}`);
|
||||
const mapsFolder = await this.getMapsFolderPath(version);
|
||||
|
||||
@@ -29,7 +29,7 @@ type Props = {
|
||||
export const InstalledMapsContext = createContext<{
|
||||
maps$?: BehaviorSubject<BsmLocalMap[]>;
|
||||
setMaps: (maps: BsmLocalMap[]) => void;
|
||||
playlists$?: Observable<LocalBPListsDetails[]>;
|
||||
playlists$?: BehaviorSubject<LocalBPListsDetails[]>;
|
||||
setPlaylists: (playlist: LocalBPListsDetails[]) => void;
|
||||
}>(null);
|
||||
|
||||
|
||||
+44
-4
@@ -6,10 +6,10 @@ 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, combineLatest, distinctUntilChanged, filter, finalize, lastValueFrom, map, merge, mergeAll, of, pipe, tap } from "rxjs";
|
||||
import { BehaviorSubject, combineLatest, distinctUntilChanged, filter, finalize, lastValueFrom, map, tap } from "rxjs";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { noop } from "shared/helpers/function.helpers";
|
||||
import { LocalBPListsDetails } from "shared/models/playlists/local-playlist.models";
|
||||
import { LocalBPList, LocalBPListsDetails } from "shared/models/playlists/local-playlist.models";
|
||||
import { PlaylistItem } from "./playlist-item.component";
|
||||
import { useStateMap } from "renderer/hooks/use-state-map.hook";
|
||||
import { ModalService } from "renderer/services/modale.service";
|
||||
@@ -18,6 +18,9 @@ import { InstalledMapsContext } from "../maps-playlists-panel.component";
|
||||
import { IpcService } from "renderer/services/ipc.service";
|
||||
import equal from "fast-deep-equal";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { PlaylistDownloaderService } from "renderer/services/playlist-downloader.service";
|
||||
import { ProgressBarService } from "renderer/services/progress-bar.service";
|
||||
import { NotificationService } from "renderer/services/notification.service";
|
||||
|
||||
type Props = {
|
||||
version: BSVersion;
|
||||
@@ -29,8 +32,11 @@ type Props = {
|
||||
export const LocalPlaylistsListPanel = forwardRef<unknown, Props>(({ version, className, isActive, linkedState }, forwardedRef) => {
|
||||
|
||||
const playlistService = useService(PlaylistsManagerService);
|
||||
const playlistDownloader = useService(PlaylistDownloaderService);
|
||||
const modals = useService(ModalService);
|
||||
const ipc = useService(IpcService);
|
||||
const progress = useService(ProgressBarService);
|
||||
const notification = useService(NotificationService);
|
||||
|
||||
const isActiveOnce = useChangeUntilEqual(isActive, { untilEqual: true });
|
||||
|
||||
@@ -65,10 +71,44 @@ export const LocalPlaylistsListPanel = forwardRef<unknown, Props>(({ version, cl
|
||||
loadPercent$.next(0);
|
||||
});
|
||||
|
||||
const onPlaylistDownloadedCB = (downloaded: LocalBPListsDetails) => {
|
||||
const newPlaylist = (() => {
|
||||
console.log(playlists);
|
||||
const index = playlists$.value.findIndex(p => p.path === downloaded.path);
|
||||
if(index === -1){
|
||||
return [...playlists$.value, downloaded];
|
||||
}
|
||||
|
||||
const newPlaylists = [...playlists$.value];
|
||||
newPlaylists[index] = downloaded;
|
||||
return newPlaylists;
|
||||
})();
|
||||
setPlaylists(newPlaylist);
|
||||
}
|
||||
|
||||
playlistDownloader.addOnPlaylistDownloadedListener(version, onPlaylistDownloadedCB);
|
||||
|
||||
return () => {
|
||||
playlistDownloader.removeOnPlaylistDownloadedListener(version, onPlaylistDownloadedCB);
|
||||
}
|
||||
|
||||
}, [isActiveOnce, version, linked]);
|
||||
|
||||
const installPlaylist = (playlist: LocalBPList) => {
|
||||
|
||||
const ignoreSongsHashs = (maps$.value || []).map(m => m.hash.toLocaleLowerCase());
|
||||
|
||||
const obs$ = playlistDownloader.installPlaylist(playlist, version, ignoreSongsHashs);
|
||||
|
||||
return lastValueFrom(obs$).then(res => {
|
||||
if(res.current === res.total){
|
||||
notification.notifySuccess({ title: "Playlist synchronisée !", desc: "La playlist et ses maps on été téléchargées.", duration: 5000 })
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const viewPlaylistFile = (path: string) => {
|
||||
return lastValueFrom(ipc.sendV2("view-path-in-explorer", { args: path }));
|
||||
return lastValueFrom(ipc.sendV2("view-path-in-explorer", path));
|
||||
};
|
||||
|
||||
const deletePlaylist = (path: string) => {
|
||||
@@ -115,7 +155,7 @@ export const LocalPlaylistsListPanel = forwardRef<unknown, Props>(({ version, cl
|
||||
minNps={p.minNps}
|
||||
onClickOpen={() => openPlaylistDetails(p.path)}
|
||||
onClickDelete={() => deletePlaylist(p.path)}
|
||||
onClickSync={() => console.log("sync")}
|
||||
onClickSync={() => installPlaylist(p)}
|
||||
onClickOpenFile={() => viewPlaylistFile(p.path)}
|
||||
/>
|
||||
)}
|
||||
|
||||
+47
-40
@@ -1,6 +1,6 @@
|
||||
import { ModalComponent } from "renderer/services/modale.service"
|
||||
import { PlaylistDetailsTemplate, PlaylistDetailsTemplateProps } from "./playlist-details-template.component"
|
||||
import { Observable, first, lastValueFrom, map, shareReplay, switchMap, take, tap } from "rxjs"
|
||||
import { Observable, combineLatest, first, lastValueFrom, map, mergeAll, mergeMap, shareReplay, switchMap, take, tap } from "rxjs"
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
@@ -14,7 +14,10 @@ import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import BeatConflict from "../../../../../../assets/images/apngs/beat-conflict.png";
|
||||
import { LocalBPListsDetails } from "shared/models/playlists/local-playlist.models";
|
||||
import { PlaylistDownloaderService } from "renderer/services/playlist-downloader.service";
|
||||
import { ProgressBarService } from "renderer/services/progress-bar.service";
|
||||
import { PlaylistHeaderState } from "./playlist-header-state.component";
|
||||
import { useConstant } from "renderer/hooks/use-constant.hook";
|
||||
|
||||
// TODO : Translate
|
||||
|
||||
interface Props {
|
||||
version: BSVersion;
|
||||
@@ -26,11 +29,16 @@ export const LocalPlaylistDetailsModal: ModalComponent<void, Props> = ({resolver
|
||||
|
||||
const audioPlayer = useService(AudioPlayerService);
|
||||
const playlistDownloader = useService(PlaylistDownloaderService);
|
||||
const progressBar = useService(ProgressBarService);
|
||||
|
||||
const localPlaylist = useObservable(() => options.data.localPlaylist$, null);
|
||||
const installedMaps = useObservable(() => options.data.installedMaps$, null);
|
||||
|
||||
const isMissingMaps$ = useConstant(() => combineLatest([options.data.installedMaps$, options.data.localPlaylist$]).pipe(map(([maps, playlist]) => maps.length !== playlist.songs.length)));
|
||||
const isPlaylistDownloading$ = useConstant(() => options.data.localPlaylist$.pipe(switchMap(playlist => playlistDownloader.$isPlaylistDownloading(playlist, options.data.version))));
|
||||
const isPlaylistInQueue$ = useConstant(() => options.data.localPlaylist$.pipe(switchMap(playlist => playlistDownloader.$isPlaylistInQueue(playlist, options.data.version))));
|
||||
|
||||
const isInQueue = useObservable(() => isPlaylistInQueue$, false);
|
||||
|
||||
const playPlaylist = () => {
|
||||
if (!installedMaps) {
|
||||
return;
|
||||
@@ -39,55 +47,54 @@ export const LocalPlaylistDetailsModal: ModalComponent<void, Props> = ({resolver
|
||||
};
|
||||
|
||||
const installPlaylist = () => {
|
||||
const obs$ = playlistDownloader.installPlaylist(localPlaylist, options.data.version);
|
||||
|
||||
const progress$ = obs$.pipe(
|
||||
map(progress => (progress.current / progress.total) * 100),
|
||||
);
|
||||
const ignoreSongsHashs = installedMaps?.map(map => map.hash);
|
||||
|
||||
const obsWithProgress$ = obs$.pipe(
|
||||
take(1),
|
||||
tap(() => progressBar.show(progress$, true)),
|
||||
switchMap(() => obs$),
|
||||
tap({ complete: () => progressBar.hide(true) })
|
||||
);
|
||||
const obs$ = playlistDownloader.installPlaylist(localPlaylist, options.data.version, ignoreSongsHashs);
|
||||
|
||||
return lastValueFrom(obsWithProgress$);
|
||||
return lastValueFrom(obs$);
|
||||
}
|
||||
|
||||
const renderMaps = () => {
|
||||
if (!installedMaps) {
|
||||
// loading maps
|
||||
return null;
|
||||
if (!Array.isArray(installedMaps) && !isInQueue) {
|
||||
return (
|
||||
<div className="grow bg-red-400">
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if(installedMaps.length === 0) {
|
||||
// no maps
|
||||
return null;
|
||||
if(installedMaps.length === 0 && !isInQueue) {
|
||||
return (
|
||||
<div className="grow flex justify-center items-center flex-col">
|
||||
<BsmImage image={BeatConflict} className="size-28"/>
|
||||
<div className="text-white font-bold w-fit space-y-1.5 flex flex-col justify-center items-center -translate-y-5">
|
||||
<p>Aucune maps installée pour cette playlist</p>
|
||||
<BsmButton withBar={false} onClick={installPlaylist} className="rounded-md h-8 flex items-center justify-center px-4" typeColor="primary" text="Télécharger.les.maps"/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if(installedMaps.length === 0 && isInQueue) {
|
||||
return (
|
||||
<div className="grow flex justify-center items-center flex-col">
|
||||
<BsmImage image={BeatConflict} className="size-28"/>
|
||||
<div className="text-white font-bold w-fit space-y-1.5 flex flex-col justify-center items-center -translate-y-5">
|
||||
<p>La Playlist est en attente de téléchargment</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grow min-h-0 overflow-hidden flex flex-col justify-start items-center">
|
||||
<AnimatePresence>
|
||||
{/* If nb installed maps not correspond to nb maps of the playlist */}
|
||||
{installedMaps.length !== localPlaylist.nbMaps && (
|
||||
<motion.div
|
||||
initial={{ height: 0 }}
|
||||
animate={{ height: "7rem" }}
|
||||
exit={{ height: 0 }}
|
||||
transition={{delay: .25, duration: .25}}
|
||||
className="shrink-0 w-full text-center overflow-hidden flex justify-center items-center"
|
||||
>
|
||||
<div className="size-[calc(100%-1rem)] bg-main-color-2 rounded-md translate-y-1.5 flex flex-row justify-center items-center gap-3">
|
||||
<BsmImage image={BeatConflict} className="size-24"/>
|
||||
<div className="text-white font-bold w-fit space-y-1.5 flex flex-col justify-center items-center">
|
||||
<p>Certaines maps de cette playlist sont manquantes</p>
|
||||
<BsmButton withBar={false} onClick={installPlaylist} className="rounded-md h-8 flex items-center justify-center px-4" typeColor="primary" text="Télécharger.les.maps.manquantes"/>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<PlaylistHeaderState
|
||||
isMissingMaps$={isMissingMaps$}
|
||||
isPlaylistDownloading$={isPlaylistDownloading$}
|
||||
isPlaylistInQueue$={isPlaylistInQueue$}
|
||||
installPlaylist={installPlaylist}
|
||||
/>
|
||||
<ul className="min-h-0 w-full grow space-y-2 pl-2.5 pr-2 py-3 overflow-y-scroll overflow-x-hidden scrollbar-default">
|
||||
{installedMaps.map(map => (
|
||||
<MapItem
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import { BsmImage } from "renderer/components/shared/bsm-image.component";
|
||||
import BeatConflict from "../../../../../../assets/images/apngs/beat-conflict.png";
|
||||
import { Observable } from "rxjs";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
|
||||
type Props = {
|
||||
isPlaylistInQueue$: Observable<boolean>;
|
||||
isPlaylistDownloading$: Observable<boolean>;
|
||||
isMissingMaps$: Observable<boolean>;
|
||||
installPlaylist: () => void;
|
||||
}
|
||||
|
||||
export function PlaylistHeaderState({isPlaylistInQueue$, isPlaylistDownloading$, isMissingMaps$, installPlaylist}: Props) {
|
||||
|
||||
const isPlaylistInQueue = useObservable(() => isPlaylistInQueue$, false);
|
||||
const isPlaylistDownloading = useObservable(() => isPlaylistDownloading$, false);
|
||||
const isMissingMaps = useObservable(() => isMissingMaps$, false);
|
||||
|
||||
console.log(isPlaylistInQueue, isPlaylistDownloading, isMissingMaps);
|
||||
|
||||
const renderHeaderContent = () => {
|
||||
|
||||
if(isPlaylistDownloading) {
|
||||
return (
|
||||
<>
|
||||
<BsmImage image={BeatConflict} className="size-24"/>
|
||||
<div className="text-white font-bold w-fit space-y-1.5 flex flex-col justify-center items-center">
|
||||
<p>La playlist est en cours de téléchargement</p>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
if(isPlaylistInQueue) {
|
||||
return (
|
||||
<>
|
||||
<BsmImage image={BeatConflict} className="size-24"/>
|
||||
<div className="text-white font-bold w-fit space-y-1.5 flex flex-col justify-center items-center">
|
||||
<p>La Playlist est en attente de téléchargment</p>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<BsmImage image={BeatConflict} className="size-24"/>
|
||||
<div className="text-white font-bold w-fit space-y-1.5 flex flex-col justify-center items-center">
|
||||
<p>Certaines maps de cette playlist sont manquantes</p>
|
||||
<BsmButton withBar={false} onClick={installPlaylist} className="rounded-md h-8 flex items-center justify-center px-4" typeColor="primary" text="Télécharger.les.maps.manquantes"/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{(isPlaylistInQueue || isPlaylistDownloading || isMissingMaps) && <motion.div
|
||||
initial={{ height: 0 }}
|
||||
animate={{ height: "7rem" }}
|
||||
exit={{ height: 0 }}
|
||||
transition={{delay: .25, duration: .25}}
|
||||
className="shrink-0 w-full text-center overflow-hidden flex justify-center items-center"
|
||||
>
|
||||
<div className="size-[calc(100%-1rem)] bg-main-color-2 rounded-md translate-y-1.5 flex flex-row justify-center items-center gap-3">
|
||||
{renderHeaderContent()}
|
||||
</div>
|
||||
</motion.div>}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
@@ -162,7 +162,7 @@ export class BSVersionManagerService {
|
||||
shareReplay({ bufferSize: 1, refCount: true })
|
||||
);
|
||||
|
||||
this.progressBar.show(obs$.pipe(map(progress => (progress.current / progress.total) * 100)), true);
|
||||
this.progressBar.show(obs$, true);
|
||||
|
||||
return obs$;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { BehaviorSubject, Observable, filter, lastValueFrom, map, shareReplay, take, tap } from "rxjs";
|
||||
import { BehaviorSubject, Observable, Subscription, distinctUntilChanged, filter, lastValueFrom, map, shareReplay, take, takeUntil, takeWhile, tap } from "rxjs";
|
||||
import { BPList, 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";
|
||||
import equal from "fast-deep-equal";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { LocalBPListsDetails } from "shared/models/playlists/local-playlist.models";
|
||||
|
||||
export class PlaylistDownloaderService {
|
||||
private static instance: PlaylistDownloaderService;
|
||||
@@ -20,32 +21,82 @@ export class PlaylistDownloaderService {
|
||||
private readonly progress: ProgressBarService;
|
||||
private readonly ipc: IpcService;
|
||||
|
||||
private readonly playlistQueue$ = new BehaviorSubject<BPList[]>([]);
|
||||
private readonly canceled$ = new BehaviorSubject<boolean>(false);
|
||||
private readonly playlistQueue$ = new BehaviorSubject<PlaylistQueueInfo[]>([]);
|
||||
private readonly onPlaylistDownloadedListeners = new Map<BSVersion, ((playlist: LocalBPListsDetails) => void)[]>();
|
||||
|
||||
private constructor() {
|
||||
this.progress = ProgressBarService.getInstance();
|
||||
this.ipc = IpcService.getInstance();
|
||||
}
|
||||
|
||||
public installPlaylist(bpList: BPList, version?: BSVersion): Observable<Progression<DownloadPlaylistProgressionData>> {
|
||||
this.playlistQueue$.next([...this.playlistQueue$.value, bpList]);
|
||||
public installPlaylist(bpList: BPList, version?: BSVersion, ignoreSongsHashs?: string[]): Observable<Progression<DownloadPlaylistProgressionData>> {
|
||||
|
||||
const clear = () => {
|
||||
this.playlistQueue$.next(this.playlistQueue$.value.filter(p => p !== bpList));
|
||||
}
|
||||
this.playlistQueue$.next([...this.playlistQueue$.value, { version, source: bpList }]);
|
||||
|
||||
console.log("AAAAA");
|
||||
|
||||
return new Observable<Progression<DownloadPlaylistProgressionData>>(subscriber => {
|
||||
(async () => {
|
||||
const playlist = await lastValueFrom(this.playlistQueue$.pipe(map(queue => queue.at(0)), filter(p => equal(bpList, p)), take(1)));
|
||||
const download$ = this.ipc.sendV2("install-playlist", { version, playlist });
|
||||
|
||||
await lastValueFrom(download$.pipe(tap(subscriber)));
|
||||
let subs: Subscription[] = [];
|
||||
|
||||
(async () => {
|
||||
const queueInfo = await lastValueFrom(this.playlistQueue$.pipe(map(queue => queue.at(0)), filter(p => equal(bpList, p.source)), take(1)));
|
||||
|
||||
console.log(queueInfo);
|
||||
|
||||
const download$ = this.ipc.sendV2("install-playlist", { version: queueInfo.version, playlist: queueInfo.source, ignoreSongsHashs }).pipe(takeWhile(() => !this.canceled$.value));
|
||||
|
||||
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));
|
||||
}));
|
||||
|
||||
const canShowProgress = !this.progress.isVisible;
|
||||
if (canShowProgress) {
|
||||
this.progress.show(download$.pipe(map(data => (data.current / data.total) * 100)), true);
|
||||
}
|
||||
|
||||
await lastValueFrom(download$.pipe(tap(subscriber))).finally(() => {
|
||||
if (canShowProgress) {
|
||||
this.progress.hide(true);
|
||||
}
|
||||
})
|
||||
})()
|
||||
.then(() => subscriber.complete())
|
||||
.catch(err => subscriber.error(err))
|
||||
.finally(clear);
|
||||
.finally(() => {
|
||||
this.playlistQueue$.next(this.playlistQueue$.value.filter(p => !equal(p.version, version) || !equal(p.source, bpList)));
|
||||
this.canceled$.next(false);
|
||||
});
|
||||
|
||||
}).pipe(shareReplay(1))
|
||||
return () => subs.forEach(s => s.unsubscribe());
|
||||
|
||||
}).pipe(shareReplay(1));
|
||||
}
|
||||
|
||||
public cancelCurrentDownload() {
|
||||
this.canceled$.next(true);
|
||||
}
|
||||
|
||||
public get currentDownloading$(): Observable<PlaylistQueueInfo> {
|
||||
return this.playlistQueue$.pipe(map(queue => queue.at(0)), distinctUntilChanged(equal));
|
||||
}
|
||||
|
||||
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 $isPlaylistDownloading(bpList: BPList, version?: BSVersion): Observable<boolean> {
|
||||
return this.currentDownloading$.pipe(map(p => p && (equal(p.source, bpList) || equal(p.downloaded, bpList)) && equal(p.version, version)), 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>> {
|
||||
@@ -61,3 +112,9 @@ export class PlaylistDownloaderService {
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export type PlaylistQueueInfo = {
|
||||
version?: BSVersion;
|
||||
source: BPList;
|
||||
downloaded?: LocalBPListsDetails;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { IpcService } from "./ipc.service";
|
||||
import { NotificationService } from "./notification.service";
|
||||
import { CSSProperties } from "react";
|
||||
import { ProgressionInterface } from "shared/models/progress-bar";
|
||||
import { Progression } from "main/helpers/fs.helpers";
|
||||
import { satisfies } from "semver";
|
||||
|
||||
export class ProgressBarService {
|
||||
private static instance: ProgressBarService;
|
||||
@@ -39,7 +41,7 @@ export class ProgressBarService {
|
||||
lastValueFrom(this.ipcService.sendV2("window.progression", progression));
|
||||
}
|
||||
|
||||
public subscribreTo(obs: Observable<ProgressionInterface | number>) {
|
||||
public subscribreTo(obs: Observable<ProgressionInterface | number | Progression>) {
|
||||
if (this.subscription) {
|
||||
this.unsubscribe();
|
||||
}
|
||||
@@ -47,6 +49,16 @@ export class ProgressBarService {
|
||||
if (typeof value === "number") {
|
||||
return this._progression$.next({ progression: value });
|
||||
}
|
||||
|
||||
const isProgression = (progression: unknown): progression is Progression => {
|
||||
return typeof (progression as Progression).current === "number" && typeof (progression as Progression).total === "number";
|
||||
}
|
||||
|
||||
if (isProgression(value)) {
|
||||
const progress = (value as Progression).current / (value as Progression).total * 100;
|
||||
return this._progression$.next({ progression: Math.floor(progress)});
|
||||
}
|
||||
|
||||
this._progression$.next(value);
|
||||
});
|
||||
}
|
||||
@@ -57,7 +69,7 @@ export class ProgressBarService {
|
||||
this.subscription = null;
|
||||
}
|
||||
|
||||
public show(obs?: Observable<ProgressionInterface | number>, unsubscribe?: boolean, style?: CSSProperties) {
|
||||
public show(obs?: Observable<ProgressionInterface | number | Progression>, unsubscribe?: boolean, style?: CSSProperties) {
|
||||
if (unsubscribe) {
|
||||
this.unsubscribe();
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function OneClickDownloadPlaylist() {
|
||||
const mapsContainer = useRef<HTMLDivElement>(null);
|
||||
const { playlistUrl } = useWindowArgs("playlistUrl");
|
||||
const download$ = useConstant(() => playlistDownloader.oneClickInstallPlaylist(playlistUrl));
|
||||
const playlistInfos = useObservable<BPList>(() => download$.pipe(filter(progress => !!progress.data?.playlistInfos), map(progress => progress.data.playlistInfos), take(1)));
|
||||
const playlistInfos = useObservable(() => download$.pipe(filter(progress => !!progress.data?.playlist), map(progress => progress.data.playlist), take(1)));
|
||||
const downloadedMaps = useObservable(() => download$.pipe(filter(progress => !!progress.data?.downloadedMaps), map(progress => progress.data.downloadedMaps)));
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -80,7 +80,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}, response: Progression<DownloadPlaylistProgressionData>};
|
||||
"install-playlist": {request: {playlist: BPList, version?: BSVersion, ignoreSongsHashs?: string[]}, response: Progression<DownloadPlaylistProgressionData>};
|
||||
"get-version-playlists-details": {request: BSVersion, response: Progression<LocalBPListsDetails[]>};
|
||||
"delete-playlist": {request: {path: string, deleteMaps?: boolean}, response: Progression};
|
||||
|
||||
@@ -117,6 +117,7 @@ export interface IpcChannelMapping extends Record<string, { request: unknown, re
|
||||
"current-version": { request: void, response: string };
|
||||
"open-logs": { request: void, response: string };
|
||||
"notify-system": { request: SystemNotificationOptions, response: void };
|
||||
"view-path-in-explorer": { request: string, response: void };
|
||||
|
||||
/* ** supporters-ipcs ** */
|
||||
"get-supporters": { request: void, response: Supporter[] };
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BsvMapDetail, SongDetails } from "../maps";
|
||||
import { BsmLocalMap } from "../maps/bsm-local-map.interface";
|
||||
import { LocalBPListsDetails } from "./local-playlist.models";
|
||||
|
||||
export interface BPList<SongType = PlaylistSong> {
|
||||
playlistTitle: string;
|
||||
@@ -21,8 +22,7 @@ export interface PlaylistSong {
|
||||
export interface DownloadPlaylistProgressionData {
|
||||
downloadedMaps: BsmLocalMap[];
|
||||
currentDownload: BsvMapDetail;
|
||||
playlistInfos: BPList;
|
||||
playlistPath: string;
|
||||
playlist: LocalBPListsDetails;
|
||||
}
|
||||
|
||||
export interface CustomDataBPList {
|
||||
|
||||
Reference in New Issue
Block a user