[feature-107] Advancement on playlists feature

This commit is contained in:
MathieuG-P
2024-03-27 21:33:19 +01:00
parent 0460a0dab3
commit b98e1ccdfa
15 changed files with 210 additions and 94 deletions
+11 -3
View File
@@ -1,10 +1,11 @@
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 { mergeMap, of } from "rxjs";
import { BPList } from "shared/models/playlists/playlist.interface";
import path from "path";
import { pathToFileURL } from "url";
import { LocalMapsManagerService } from "../services/additional-content/maps/local-maps-manager.service";
import { Progression } from "main/helpers/fs.helpers";
const ipc = IpcService.getInstance();
@@ -61,5 +62,12 @@ ipc.on("get-version-playlists-details", (args, reply) => {
ipc.on("delete-playlist", (args, reply) => {
const playlists = LocalPlaylistsManagerService.getInstance();
reply(playlists.deletePlaylist(args));
const maps = LocalMapsManagerService.getInstance();
reply(playlists.deletePlaylistFile(args.bpList).pipe(mergeMap(() => {
if(args.deleteMaps){
console.log("ALALALZELALZELAZELA");
return maps.deleteMapsFromHashs(args.version, args.bpList.songs.map(s => s.hash));
}
return of({ current: 0, total: 0 } as Progression);
})));
});
@@ -20,6 +20,7 @@ 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";
import { allSettled } from "shared/helpers/promise.helpers";
export class LocalPlaylistsManagerService {
private static instance: LocalPlaylistsManagerService;
@@ -284,34 +285,8 @@ export class LocalPlaylistsManagerService {
}
public deletePlaylist(opt: {path: string, deleteMaps?: boolean}): Observable<Progression>{
return new Observable<Progression>(obs => {
(async () => {
const bpList = await this.readPlaylistFromSource(opt.path);
const progress: Progression = { current: 0, total: opt.deleteMaps ? bpList.songs.length + 1 : 1};
if(opt.deleteMaps){
const mapsHashs = bpList.songs.map(s => ({ hash: s.hash }));
await lastValueFrom(this.maps.deleteMaps(mapsHashs).pipe(
tap({
next: () => {
progress.current += 1
obs.next(progress);
},
}),
));
}
await unlinkPath(opt.path);
progress.current += 1;
obs.next(progress);
})()
.catch(err => obs.error(err))
.finally(() => obs.complete());
});
public deletePlaylistFile(bpList: LocalBPList): Observable<void>{
return from(unlinkPath(bpList.path));
}
public oneClickInstallPlaylist(bpListUrl: string): Observable<Progression<DownloadPlaylistProgressionData>> {
@@ -26,6 +26,7 @@ import { SongCacheService } from "./song-cache.service";
import { IpcService } from "../../ipc.service";
import { pathToFileURL } from "url";
import { sToMs } from "../../../../shared/helpers/time.helpers";
import { FieldRequired } from "shared/helpers/type.helpers";
export class LocalMapsManagerService {
private static instance: LocalMapsManagerService;
@@ -236,7 +237,7 @@ export class LocalMapsManagerService {
return this.linker.unlinkFolder(versionMapsPath, { keepContents: keepMaps, intermediateFolder: LocalMapsManagerService.SHARED_MAPS_FOLDER });
}
public deleteMaps(maps: Partial<BsmLocalMap>[]): Observable<DeleteMapsProgress> {
public deleteMaps(maps: FieldRequired<BsmLocalMap, "path">[]): Observable<DeleteMapsProgress> {
return new Observable<DeleteMapsProgress>(observer => {
const progress: DeleteMapsProgress = { total: maps.length, deleted: 0 };
@@ -244,17 +245,13 @@ export class LocalMapsManagerService {
for (const map of maps) {
let mapPath = map.path;
if (!mapPath) {
const mapInfo = map.hash ? this.songCache.getMapInfoFromHash(map.hash) : null;
mapPath = mapInfo?.path;
}
if (mapPath && pathExistsSync(mapPath)) {
if (pathExistsSync(mapPath)) {
await deleteFolder(mapPath);
this.songCache.deleteMapInfoFromDirname(path.basename(mapPath));
progress.deleted++;
observer.next(progress);
}
observer.next(progress);
}
})()
.catch(e => observer.error(e))
@@ -262,6 +259,53 @@ export class LocalMapsManagerService {
});
}
public deleteMapsFromHashs(version: BSVersion, hashs: string[]): Observable<Progression> {
return new Observable<Progression>(observer => {
const progress: Progression = { total: hashs.length, current: 0 };
(async () => {
const versionMapsPath = await this.getMapsFolderPath(version);
const mapsPaths = await getFoldersInFolder(versionMapsPath);
for (const mapPath of mapsPaths) {
const mapInfo = await this.loadMapInfoFromPath(mapPath);
if (hashs.includes(mapInfo.hash)) {
await deleteFolder(mapPath);
this.songCache.deleteMapInfoFromDirname(path.basename(mapPath));
progress.current++;
}
observer.next(progress);
}
})()
.catch(e => observer.error(e))
.finally(() => observer.complete());
});
}
public async getMapInfoFromHash(hash: string, version: BSVersion): Promise<BsmLocalMap> {
const versionMapsPath = await this.getMapsFolderPath(version);
const mapInfo = this.songCache.getMapInfoFromHash(hash);
const cachedMapPath = path.join(versionMapsPath, mapInfo.dirname);
if(pathExistsSync(cachedMapPath)){
return this.loadMapInfoFromPath(cachedMapPath);
}
// if not in cache, search in the folder
const mapsPaths = await getFoldersInFolder(versionMapsPath);
for (const mapPath of mapsPaths) {
const mapInfo = await this.loadMapInfoFromPath(mapPath);
if (mapInfo.hash === hash) {
return mapInfo;
}
}
return null;
}
public async downloadMap(map: BsvMapDetail, version?: BSVersion): Promise<BsmLocalMap> {
if (!map.versions.at(0).hash) {
@@ -26,9 +26,9 @@ export class SongCacheService {
return this.rawInfosCache.get(dirname);
}
public getMapInfoFromHash(hash: string): { path: string, info: CachedRawInfoWithHash } | undefined {
public getMapInfoFromHash(hash: string): { dirname: 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;
return res ? { dirname: res[0], info: res[1] } : undefined;
}
public setMapInfoFromDirname(dirname: string, info: CachedRawInfoWithHash): void {
@@ -12,7 +12,7 @@ import { noop } from "shared/helpers/function.helpers";
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";
import { ModalExitCode, ModalService } from "renderer/services/modale.service";
import { LocalPlaylistDetailsModal } from "renderer/components/modal/modal-types/playlist/local-playlist-details-modal.component";
import { InstalledMapsContext } from "../maps-playlists-panel.component";
import { IpcService } from "renderer/services/ipc.service";
@@ -21,6 +21,7 @@ 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";
import { DeletePlaylistModal } from "renderer/components/modal/modal-types/playlist/delete-playlist-modal.component";
type Props = {
version: BSVersion;
@@ -29,6 +30,8 @@ type Props = {
isActive?: boolean;
};
// TODO : Translate
export const LocalPlaylistsListPanel = forwardRef<unknown, Props>(({ version, className, isActive, linkedState }, forwardedRef) => {
const playlistService = useService(PlaylistsManagerService);
@@ -111,10 +114,14 @@ export const LocalPlaylistsListPanel = forwardRef<unknown, Props>(({ version, cl
return lastValueFrom(ipc.sendV2("view-path-in-explorer", path));
};
const deletePlaylist = (path: string) => {
const deletePlaylist = async (bpList: LocalBPList) => {
// !! Need to call the modal to confirm the deletion and to ask if the maps should be deleted too
lastValueFrom(playlistService.deletePlaylist({ path, deleteMaps: false })).then(() => {
setPlaylists(playlists.filter(p => p.path !== path));
const { exitCode, data: deleteMaps } = await modals.openModal(DeletePlaylistModal, { data: bpList });
if(exitCode !== ModalExitCode.COMPLETED){ return; }
lastValueFrom(playlistService.deletePlaylist({ version, bpList, deleteMaps })).then(() => {
setPlaylists(playlists.filter(p => p.path !== bpList.path));
})
};
@@ -153,10 +160,13 @@ export const LocalPlaylistsListPanel = forwardRef<unknown, Props>(({ version, cl
duration={p.duration}
maxNps={p.maxNps}
minNps={p.minNps}
isDownloading$={playlistDownloader.$isPlaylistDownloading(p, version)}
isInQueue$={playlistDownloader.$isPlaylistInQueue(p, version)}
onClickOpen={() => openPlaylistDetails(p.path)}
onClickDelete={() => deletePlaylist(p.path)}
onClickDelete={() => deletePlaylist(p)}
onClickSync={() => installPlaylist(p)}
onClickOpenFile={() => viewPlaylistFile(p.path)}
onClickCancelDownload={() => playlistDownloader.cancelDownload(p, version)}
/>
)}
</ul>
@@ -11,6 +11,9 @@ import { useState } from 'react';
import { SearchIcon } from 'renderer/components/svgs/icons/search-icon.component';
import { BsmButton } from 'renderer/components/shared/bsm-button.component';
import Tippy from '@tippyjs/react';
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 = {
title?: string;
@@ -23,11 +26,13 @@ type Props = {
minNps?: number;
maxNps?: number;
selected?: boolean;
path?: string;
isDownloading$?: Observable<boolean>;
isInQueue$?: Observable<boolean>;
onClickOpen?: () => void;
onClickOpenFile?: () => void;
onClickDelete?: () => void;
onClickSync?: () => void;
onClickCancelDownload?: () => void;
}
export function PlaylistItem({ title,
@@ -40,15 +45,20 @@ export function PlaylistItem({ title,
minNps,
maxNps,
selected,
isDownloading$,
isInQueue$,
onClickOpen,
onClickOpenFile,
onClickSync,
onClickDelete
onClickDelete,
onClickCancelDownload
}: Props) {
const color = useThemeColor("first-color");
const [hovered, setHovered] = useState(false);
const isDownloading = useObservable(() => isDownloading$ ?? of(), false, [isDownloading$]);
const isInQueue = useObservable(() => isInQueue$ ?? of(), false, [isInQueue$]);
const nbMapsText = nbMaps ? Intl.NumberFormat(undefined, { notation: "compact" }).format(nbMaps).trim() : null;
const nbMappersText = nbMappers ? Intl.NumberFormat(undefined, { notation: "compact" }).format(nbMappers).trim() : null;
@@ -94,21 +104,39 @@ export function PlaylistItem({ title,
</div>
</div>
<div className="absolute bg-light-main-color-3 dark:bg-main-color-3 top-0 h-full w-max left-full -translate-x-2.5 group-hover:-translate-x-full transition-transform">
<motion.div className="absolute bg-light-main-color-3 dark:bg-main-color-3 top-0 h-full w-max left-full" animate={{x: (hovered || isDownloading ? "-100%" : "-0.625rem")}} transition={{duration: .1}}>
<span className="absolute size-2.5 top-0 right-full bg-inherit translate-x-px" style={{ clipPath: 'path("M11 -1 L11 10 L10 10 A10 10 0 0 0 0 0 L0 -1 Z")' }} />
<span className="absolute size-2.5 bottom-0 right-full bg-inherit translate-x-px" style={{ clipPath: 'path("M11 11 L11 0 L10 0 A10 10 0 0 1 0 10 L 0 11 Z")' }} />
<div className="flex flex-col justify-center items-center flex-wrap gap-0.5 opacity-0 size-full px-1 group-hover:opacity-100 *:size-6 *:!bg-inherit *:p-0.5 *:rounded-md">
{onClickSync && <Tippy content="Synchronizer la playlist" placement="left" theme="default">
<BsmButton
icon="sync"
className="hover:!bg-main-color-1"
iconClassName="size-full brightness-75 dark:brightness-200"
style={{color}}
onClick={onClickSync}
withBar={false}
/>
</Tippy>}
<motion.div className="flex flex-col justify-center items-center flex-wrap gap-0.5 size-full px-1 *:size-6 *:!bg-inherit *:p-0.5 *:rounded-md" animate={{opacity: hovered || isDownloading ? 1 : 0}} transition={{duration: 0}}>
{(isDownloading || isInQueue) && onClickCancelDownload && (
<Tippy content={isDownloading ? "Arreter le téléchargement" : "Annuler le téléchargement"} placement="left" theme="default">
<BsmButton
icon="close"
className="hover:!bg-main-color-1 text-red-500 !p-0"
iconClassName="size-full"
onClick={onClickCancelDownload}
withBar={false}
/>
</Tippy>
)}
{onClickSync && (
isDownloading ? (
<BsmBasicSpinner className="hover:!bg-main-color-1" spinnerClassName="brightness-75 dark:brightness-200" style={{ color }} thikness="3px"/>
) : !isInQueue ? (
<Tippy content="Synchronizer la playlist" placement="left" theme="default">
<BsmButton
icon="sync"
className="hover:!bg-main-color-1"
iconClassName="size-full brightness-75 dark:brightness-200"
style={{color}}
onClick={onClickSync}
withBar={false}
/>
</Tippy>
) : (<></>)
)}
{onClickOpenFile && <Tippy content="Afficher le fichier" placement="left" theme="default">
<BsmButton
icon="folder"
@@ -119,7 +147,7 @@ export function PlaylistItem({ title,
withBar={false}
/>
</Tippy>}
{onClickDelete && <Tippy content="Supprimer" placement="left" theme="default">
{(onClickDelete && !isDownloading && !isInQueue) && <Tippy content="Supprimer" placement="left" theme="default">
<BsmButton
icon="trash"
className="hover:!bg-main-color-1 text-red-500"
@@ -128,9 +156,8 @@ export function PlaylistItem({ title,
withBar={false}
/>
</Tippy>}
</div>
</div>
</motion.div>
</motion.div>
</div>
</motion.li>
@@ -0,0 +1,34 @@
import { useState } from "react";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component";
import { BsmImage } from "renderer/components/shared/bsm-image.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { ModalComponent, ModalExitCode } from "renderer/services/modale.service";
import BeatConflict from "../../../../../../assets/images/apngs/beat-conflict.png";
import { BPList } from "shared/models/playlists/playlist.interface";
import Tippy from "@tippyjs/react";
export const DeletePlaylistModal: ModalComponent<boolean, BPList> = ({ resolver, options: { data }}) => {
const t = useTranslation();
const [deleteMaps, setDeleteMaps] = useState(false);
return (
<form className="text-gray-800 dark:text-gray-200">
<h1 className="text-3xl uppercase tracking-wide w-full text-center">Supprimer la playlist ?</h1>
<BsmImage className="mx-auto h-24" image={BeatConflict} />
<p className="max-w-sm w-full">{`Est-tu sûr de vouloir supprimer la playlist "${data.playlistTitle}" ?`}</p>
<div className="flex items-center relative py-2 gap-1">
<BsmCheckbox className="h-5 relative z-[1]" checked={deleteMaps} onChange={val => setDeleteMaps(() => val)} />
<Tippy placement="top" content="Si activé, toutes les maps de la playlist seront supprimées" theme="default">
<span className="italic cursor-help">Supprimer les maps</span>
</Tippy>
</div>
<div className="grid grid-flow-col grid-cols-2 gap-4 mt-2">
<BsmButton typeColor="cancel" className="rounded-md text-center transition-all" onClick={() => resolver({ exitCode: ModalExitCode.CANCELED })} withBar={false} text="misc.cancel" />
<BsmButton typeColor="primary" className="rounded-md text-center transition-all" onClick={() => resolver({ exitCode: ModalExitCode.COMPLETED, data: deleteMaps })} withBar={false} text="misc.delete" />
</div>
</form>
);
};
@@ -50,7 +50,7 @@ export function Modal() {
modal.resolver({ exitCode: ModalExitCode.CLOSED });
}}
>
<BsmIcon className="w-full h-full" icon="cross" />
<BsmIcon className="size-full" icon="cross" />
</div>
<modal.modal resolver={modal.resolver} options={modal.options} />
</div>
@@ -74,7 +74,7 @@ export const NotificationItem = forwardRef(({ resolver, notification }: { resolv
)}
</div>
<span className={`absolute w-full h-1 shadow-center left-0 top-0 ${renderNeonColors}`} />
<BsmButton icon="cross" className="absolute top-2 right-2" withBar={false} onClick={() => resolver(NotificationResult.CLOSE)} />
<BsmButton icon="cross" className="absolute top-2 right-2 size-4" withBar={false} onClick={() => resolver(NotificationResult.CLOSE)} />
</motion.li>
);
});
@@ -1,9 +1,9 @@
import { CSSProperties } from "react";
import { createSvgIcon } from "../svg-icon.type";
export function CloseIcon(props: { className?: string; style?: CSSProperties }) {
export const CloseIcon = createSvgIcon((props, ref) => {
return (
<svg className={props.className} style={props.style} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" height="40" width="40">
<path fill="currentColor" d="m20 22.208-8.417 8.417q-.458.458-1.104.458-.646 0-1.104-.458-.458-.458-.458-1.104 0-.646.458-1.104L17.792 20l-8.417-8.417q-.458-.458-.458-1.104 0-.646.458-1.104.458-.458 1.104-.458.646 0 1.104.458L20 17.792l8.417-8.417q.458-.458 1.104-.458.646 0 1.104.458.458.458.458 1.104 0 .646-.458 1.104L22.208 20l8.417 8.417q.458.458.458 1.104 0 .646-.458 1.104-.458.458-1.104.458-.646 0-1.104-.458Z" />
<svg ref={ref} {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor">
<path d="M480-416.348 287.826-224.174Q275.152-211.5 256-211.5t-31.826-12.674Q211.5-236.848 211.5-256t12.674-31.826L416.348-480 224.174-672.174Q211.5-684.848 211.5-704t12.674-31.826Q236.848-748.5 256-748.5t31.826 12.674L480-543.652l192.174-192.174Q684.848-748.5 704-748.5t31.826 12.674Q748.5-723.152 748.5-704t-12.674 31.826L543.652-480l192.174 192.174Q748.5-275.152 748.5-256t-12.674 31.826Q723.152-211.5 704-211.5t-31.826-12.674L480-416.348Z"/>
</svg>
);
}
});
@@ -1,4 +1,4 @@
import { BehaviorSubject, Observable, Subscription, distinctUntilChanged, filter, lastValueFrom, map, shareReplay, take, takeUntil, takeWhile, tap } from "rxjs";
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 { IpcService } from "./ipc.service";
import { ProgressBarService } from "./progress-bar.service";
@@ -6,6 +6,7 @@ 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";
import { removeIndex } from "shared/helpers/array.helpers";
export class PlaylistDownloaderService {
private static instance: PlaylistDownloaderService;
@@ -21,7 +22,7 @@ export class PlaylistDownloaderService {
private readonly progress: ProgressBarService;
private readonly ipc: IpcService;
private readonly canceled$ = new BehaviorSubject<boolean>(false);
private readonly cancel$ = new Subject<void>();
private readonly playlistQueue$ = new BehaviorSubject<PlaylistQueueInfo[]>([]);
private readonly onPlaylistDownloadedListeners = new Map<BSVersion, ((playlist: LocalBPListsDetails) => void)[]>();
@@ -34,40 +35,34 @@ export class PlaylistDownloaderService {
this.playlistQueue$.next([...this.playlistQueue$.value, { version, source: bpList }]);
console.log("AAAAA");
return new Observable<Progression<DownloadPlaylistProgressionData>>(subscriber => {
let subs: Subscription[] = [];
let canShowProgress = false;
(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));
const download$ = this.ipc.sendV2("install-playlist", { version: queueInfo.version, playlist: queueInfo.source, ignoreSongsHashs }).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));
}));
const canShowProgress = !this.progress.isVisible;
canShowProgress = !this.progress.isVisible;
if (canShowProgress) {
this.progress.show(download$.pipe(map(data => (data.current / data.total) * 100)), true);
this.progress.show(download$, true);
}
await lastValueFrom(download$.pipe(tap(subscriber))).finally(() => {
if (canShowProgress) {
this.progress.hide(true);
}
})
await lastValueFrom(download$.pipe(tap(subscriber)));
})()
.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.canceled$.next(false);
if (canShowProgress) {
this.progress.hide(true);
}
});
return () => subs.forEach(s => s.unsubscribe());
@@ -75,8 +70,20 @@ export class PlaylistDownloaderService {
}).pipe(shareReplay(1));
}
public cancelCurrentDownload() {
this.canceled$.next(true);
public cancelDownload(bplist: BPList, version?: BSVersion) {
const currentDownload = this.playlistQueue$.value.at(0);
if(!currentDownload) { return; }
if((equal(currentDownload.source, bplist) || equal(currentDownload.downloaded, bplist)) && equal(currentDownload.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));
if(indexToRemove === -1){ return; }
const newArr = removeIndex(indexToRemove, [...this.playlistQueue$.value]);
this.playlistQueue$.next(newArr);
}
public get currentDownloading$(): Observable<PlaylistQueueInfo> {
@@ -88,7 +95,12 @@ export class PlaylistDownloaderService {
}
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());
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; }
return true;
}), distinctUntilChanged());
}
public addOnPlaylistDownloadedListener(version: BSVersion, cb: (playlist: LocalBPListsDetails) => void){
@@ -3,7 +3,7 @@ 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 { LocalBPListsDetails } from "shared/models/playlists/local-playlist.models";
import { LocalBPList, LocalBPListsDetails } from "shared/models/playlists/local-playlist.models";
import { ModalExitCode, ModalService } from "./modale.service";
import { UnlinkPlaylistModal } from "renderer/components/modal/modal-types/unlink-playlist-modal.component";
import { LinkPlaylistModal } from "renderer/components/modal/modal-types/link-playlist-modal.component";
@@ -34,7 +34,7 @@ export class PlaylistsManagerService {
return this.ipc.sendV2("get-version-playlists-details", version);
}
public deletePlaylist(opt: {path: string, deleteMaps?: boolean}): Observable<Progression> {
public deletePlaylist(opt: {version: BSVersion, bpList: LocalBPList, deleteMaps?: boolean}): Observable<Progression> {
return this.ipc.sendV2("delete-playlist", opt);
}
+5
View File
@@ -18,3 +18,8 @@ export function popElement<T = unknown>(func: (element: T) => boolean, arr: T[])
}
return arr.splice(index, 1)[0];
}
export function removeIndex<T = unknown>(index: number, arr: T[]): T[] {
arr.splice(index, 1);
return arr;
}
+1
View File
@@ -0,0 +1 @@
export type FieldRequired<T, K extends keyof T> = Partial<T> & Required<Pick<T, K>>; // All fields of T are optional except for K
+2 -2
View File
@@ -18,7 +18,7 @@ import { FileFilter, OpenDialogReturnValue } from "electron";
import { SystemNotificationOptions } from "../notification/system-notification.model";
import { Supporter } from "../supporters";
import { AppWindow } from "../window-manager/app-window.model";
import { LocalBPListsDetails } from "../playlists/local-playlist.models";
import { LocalBPList, LocalBPListsDetails } from "../playlists/local-playlist.models";
export type IpcReplier<T> = (data: Observable<T>) => void;
@@ -82,7 +82,7 @@ export interface IpcChannelMapping extends Record<string, { request: unknown, re
"is-playlists-deep-links-enabled": { request: void, response: boolean };
"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};
"delete-playlist": {request: {version: BSVersion, bpList: LocalBPList, deleteMaps?: boolean}, response: Progression};
/* ** bs-uninstall-ipcs ** */
"bs.uninstall": { request: BSVersion, response: boolean };