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,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 {
|
||||
|
||||
+15
-5
@@ -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>
|
||||
|
||||
+45
-18
@@ -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>
|
||||
|
||||
|
||||
+34
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user