mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
[feature-107] progress on playlists (lot of things, i don't remember all things i've done in this commit)
This commit is contained in:
@@ -44,3 +44,8 @@ ipc.on<BSVersion>("get-version-playlists-details", (req, reply) => {
|
||||
const playlists = LocalPlaylistsManagerService.getInstance();
|
||||
reply(playlists.getVersionPlaylistsDetails(req.args));
|
||||
});
|
||||
|
||||
ipc.on<{path: string, deleteMaps?: boolean}>("delete-playlist", (req, reply) => {
|
||||
const playlists = LocalPlaylistsManagerService.getInstance();
|
||||
reply(playlists.deletePlaylist(req.args));
|
||||
});
|
||||
|
||||
@@ -11,14 +11,18 @@ import { from, of } from "rxjs";
|
||||
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
ipcMain.on("new-window", async (event, request: IpcRequest<string>) => {
|
||||
ipcMain.on("new-window", (event, request: IpcRequest<string>) => {
|
||||
shell.openExternal(request.args);
|
||||
});
|
||||
|
||||
ipc.on<string>("choose-folder", async (req, reply) => {
|
||||
ipc.on<string>("choose-folder", (req, reply) => {
|
||||
reply(from(dialog.showOpenDialog({ properties: ["openDirectory"], defaultPath: req.args ?? "" })));
|
||||
});
|
||||
|
||||
ipc.on<string>("view-path-in-explorer", (req, reply) => {
|
||||
reply(of(shell.showItemInFolder(req.args)));
|
||||
});
|
||||
|
||||
ipcMain.on("window.progression", async (event, request: IpcRequest<number>) => {
|
||||
BrowserWindow.fromWebContents(event.sender)?.setProgressBar(request.args / 100);
|
||||
});
|
||||
|
||||
@@ -11,13 +11,12 @@ import { BPList, DownloadPlaylistProgressionData } from "shared/models/playlists
|
||||
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 { Progression, pathExist } from "../../helpers/fs.helpers";
|
||||
import { Progression, pathExist, unlinkPath } from "../../helpers/fs.helpers";
|
||||
import { FileAssociationService } from "../file-association.service";
|
||||
import { SongDetailsCacheService } from "./maps/song-details-cache.service";
|
||||
import { sToMs } from "shared/helpers/time.helpers";
|
||||
import { LocalBPList, LocalBPListsDetails } from "shared/models/playlists/local-playlist.models";
|
||||
import { SongCacheService } from "./maps/song-cache.service";
|
||||
import { pathToFileURL } from "url";
|
||||
import { InstallationLocationService } from "../installation-location.service";
|
||||
|
||||
export class LocalPlaylistsManagerService {
|
||||
@@ -229,6 +228,40 @@ export class LocalPlaylistsManagerService {
|
||||
|
||||
}
|
||||
|
||||
public deletePlaylist(opt: {path: string, deleteMaps?: boolean}): Observable<Progression>{
|
||||
|
||||
console.log("AAAAAA");
|
||||
|
||||
return new Observable<Progression>(obs => {
|
||||
(async () => {
|
||||
|
||||
const bpList = await this.readPlaylistFile(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);
|
||||
},
|
||||
error: err => obs.error(err),
|
||||
complete: () => obs.next(progress),
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
await unlinkPath(opt.path);
|
||||
progress.current += 1;
|
||||
obs.next(progress);
|
||||
})()
|
||||
.catch(err => obs.error(err))
|
||||
.finally(() => obs.complete());
|
||||
});
|
||||
}
|
||||
|
||||
public oneClickInstallPlaylist(bpListUrl: string): Observable<Progression<DownloadPlaylistProgressionData>> {
|
||||
|
||||
return new Observable<Progression<DownloadPlaylistProgressionData>>(obs => {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { InstallationLocationService } from "../../installation-location.service
|
||||
import { UtilsService } from "../../utils.service";
|
||||
import crypto from "crypto";
|
||||
import { lstatSync } from "fs";
|
||||
import { copy, createReadStream, ensureDir, pathExists, realpath, unlink } from "fs-extra";
|
||||
import { copy, createReadStream, ensureDir, pathExists, pathExistsSync, realpath, unlink } from "fs-extra";
|
||||
import StreamZip from "node-stream-zip";
|
||||
import { RequestService } from "../../request.service";
|
||||
import sanitize from "sanitize-filename";
|
||||
@@ -240,32 +240,33 @@ export class LocalMapsManagerService {
|
||||
return this.linker.unlinkFolder(versionMapsPath, { keepContents: keepMaps, intermediateFolder: LocalMapsManagerService.SHARED_MAPS_FOLDER });
|
||||
}
|
||||
|
||||
public deleteMaps(maps: BsmLocalMap[]): Observable<DeleteMapsProgress> {
|
||||
const mapsFolders = maps.map(map => map.path);
|
||||
const mapsHashsToDelete = maps.map(map => map.hash);
|
||||
|
||||
public deleteMaps(maps: Partial<BsmLocalMap>[]): Observable<DeleteMapsProgress> {
|
||||
return new Observable<DeleteMapsProgress>(observer => {
|
||||
const progress: DeleteMapsProgress = { total: maps.length, deleted: 0 };
|
||||
|
||||
(async () => {
|
||||
const progress: DeleteMapsProgress = { total: maps.length, deleted: 0 };
|
||||
try {
|
||||
for (const folder of mapsFolders) {
|
||||
const detail = await this.loadMapInfoFromPath(folder);
|
||||
if (!mapsHashsToDelete.includes(detail?.hash)) {
|
||||
continue;
|
||||
}
|
||||
await deleteFolder(folder);
|
||||
this.songCache.deleteMapInfoFromDirname(path.basename(folder));
|
||||
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)) {
|
||||
await deleteFolder(mapPath);
|
||||
this.songCache.deleteMapInfoFromDirname(path.basename(mapPath));
|
||||
progress.deleted++;
|
||||
observer.next(progress);
|
||||
}
|
||||
} catch (e) {
|
||||
observer.error(e);
|
||||
}
|
||||
observer.complete();
|
||||
})();
|
||||
})()
|
||||
.catch(e => observer.error(e))
|
||||
.finally(() => observer.complete());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public async downloadMap(map: BsvMapDetail, version?: BSVersion): Promise<BsmLocalMap> {
|
||||
if (!map.versions.at(0).hash) {
|
||||
throw new Error("Cannot download map, no hash found");
|
||||
|
||||
@@ -54,10 +54,13 @@ export class IpcService {
|
||||
complete: () => this.send(this.getCompleteChannel(channel), window)
|
||||
})
|
||||
|
||||
window.webContents.once("destroyed", () => sub.unsubscribe());
|
||||
const unsubscribeOnDestroy = () => sub.unsubscribe();
|
||||
|
||||
window.webContents.once("destroyed", unsubscribeOnDestroy);
|
||||
window.webContents.ipc.once(this.getTearDownChannel(channel), () => sub.unsubscribe());
|
||||
|
||||
sub.add(() => {
|
||||
window.webContents.removeListener("destroyed", unsubscribeOnDestroy);
|
||||
window.webContents.ipc.removeAllListeners(this.getTearDownChannel(channel));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export function AvailableVersionsSlide({ versions }: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<ol className="w-full flex items-start justify-center gap-6 shrink-0 content-start flex-wrap px-3.5 py-4 overflow-x-hidden overflow-y-scroll scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900">
|
||||
<ol className="w-full flex items-start justify-center gap-6 shrink-0 content-start flex-wrap px-3.5 py-4 overflow-x-hidden overflow-y-scroll scrollbar-default">
|
||||
{getVersions().map(version => (
|
||||
<AvailableVersionItem key={version.BSManifest} version={version} selected={equal(version, context.selectedVersion)} onClick={() => setSelectedVersion(version)}/>
|
||||
))}
|
||||
|
||||
@@ -19,13 +19,19 @@ import { LocalPlaylistsListPanel } from "./playlists/local-playlists-list-panel.
|
||||
import { PlaylistsManagerService } from "renderer/services/playlists-manager.service";
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { useConstant } from "renderer/hooks/use-constant.hook";
|
||||
import { LocalBPListsDetails } from "shared/models/playlists/local-playlist.models";
|
||||
|
||||
type Props = {
|
||||
version?: BSVersion;
|
||||
isActive?: boolean;
|
||||
};
|
||||
|
||||
export const InstalledMapsContext = createContext<{ maps$?: BehaviorSubject<BsmLocalMap[]>; setMaps: (maps: BsmLocalMap[]) => void }>(null);
|
||||
export const InstalledMapsContext = createContext<{
|
||||
maps$?: BehaviorSubject<BsmLocalMap[]>;
|
||||
setMaps: (maps: BsmLocalMap[]) => void;
|
||||
playlists$?: Observable<LocalBPListsDetails[]>;
|
||||
setPlaylists: (playlist: LocalBPListsDetails[]) => void;
|
||||
}>(null);
|
||||
|
||||
export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
|
||||
@@ -37,7 +43,8 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
const [tabIndex, setTabIndex] = useState(0);
|
||||
|
||||
const maps$ = useConstant(() => new BehaviorSubject<BsmLocalMap[]>(undefined));
|
||||
const mapsContextValue = useConstant(() => ({ maps$: maps$, setMaps: maps$.next.bind(maps$) }));
|
||||
const playlists$ = useConstant(() => new BehaviorSubject<LocalBPListsDetails[]>(undefined));
|
||||
const mapsContextValue = useConstant(() => ({ maps$: maps$, setMaps: maps$.next.bind(maps$), playlists$: playlists$, setPlaylists: playlists$.next.bind(playlists$)}));
|
||||
|
||||
const mapsRef = useRef<any>();
|
||||
const [mapFilter, setMapFilter] = useState<MapFilter>({});
|
||||
|
||||
+1
-1
@@ -402,7 +402,7 @@ export const LocalMapsListPanel = forwardRef<unknown, Props>(({ version, classNa
|
||||
|
||||
return (
|
||||
<div ref={ref} className={className}>
|
||||
<VariableSizeList className="scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900" width="100%" height={listHeight} itemSize={() => 108} itemCount={preppedMaps.length} itemData={preppedMaps} layout="vertical" style={{ scrollbarGutter: "stable both-edges" }} itemKey={(i, data) => data[i].map(map => map.hash).join()}>
|
||||
<VariableSizeList className="scrollbar-default" width="100%" height={listHeight} itemSize={() => 108} itemCount={preppedMaps.length} itemData={preppedMaps} layout="vertical" style={{ scrollbarGutter: "stable both-edges" }} itemKey={(i, data) => data[i].map(map => map.hash).join()}>
|
||||
{props => <MapsRow maps={props.data[props.index]} style={props.style} selectedMaps$={selectedMaps$} onMapSelect={onMapSelected} onMapDelete={handleDelete} />}
|
||||
</VariableSizeList>
|
||||
</div>
|
||||
|
||||
@@ -159,7 +159,7 @@ export const MapItem = memo(({ hash, title, autor, songAutor, coverUrl, songUrl,
|
||||
<GlowEffect visible={hovered || (selected && !!onSelected)} />
|
||||
<AnimatePresence>
|
||||
{(diffsPanelHovered || bottomBarHovered) && (
|
||||
<motion.ul key={hash} className="absolute top-[calc(100%-10px)] w-full h-fit max-h-[200%] pt-4 pb-2 px-2 overflow-y-scroll bg-light-main-color-3 dark:bg-main-color-3 text-main-color-1 dark:text-current brightness-125 rounded-md flex flex-col gap-3 scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900 shadow-sm shadow-black" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.15 }} onHoverStart={diffsPanelHoverStart} onHoverEnd={diffsPanelHoverEnd}>
|
||||
<motion.ul key={hash} className="absolute top-[calc(100%-10px)] w-full h-fit max-h-[200%] pt-4 pb-2 px-2 overflow-y-scroll bg-light-main-color-3 dark:bg-main-color-3 text-main-color-1 dark:text-current brightness-125 rounded-md flex flex-col gap-3 scrollbar-default shadow-sm shadow-black" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.15 }} onHoverStart={diffsPanelHoverStart} onHoverEnd={diffsPanelHoverEnd}>
|
||||
{Array.from(diffs.entries()).map(([charac, diffSet]) => (
|
||||
<ol key={crypto.randomUUID()} className="flex flex-col w-full gap-1">
|
||||
{diffSet.map(({ type, name, stars }) => (
|
||||
@@ -178,9 +178,9 @@ export const MapItem = memo(({ hash, title, autor, songAutor, coverUrl, songUrl,
|
||||
</AnimatePresence>
|
||||
<div className="h-full w-full relative pl-[100px] rounded-md overflow-hidden flex flex-row justify-end">
|
||||
<div className={`absolute top-0 left-0 h-full aspect-square cursor-pointer ${showOwned && "border-l-[5px]"}`} style={{ borderColor: showOwned && color }}>
|
||||
<BsmImage className="w-full h-full object-cover" image={coverUrl} placeholder={defaultImage} errorImage={defaultImage} />
|
||||
<BsmImage className="size-full object-cover" image={coverUrl} placeholder={defaultImage} errorImage={defaultImage} />
|
||||
<span
|
||||
className="absolute flex justify-center items-center w-full h-full pr-1 bg-transparent top-0 left-0 group-hover:bg-black group-hover:bg-opacity-40"
|
||||
className="absolute flex justify-center items-center size-full pr-1 bg-transparent top-0 left-0 group-hover:bg-black group-hover:bg-opacity-40"
|
||||
style={{ color }}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
@@ -188,12 +188,12 @@ export const MapItem = memo(({ hash, title, autor, songAutor, coverUrl, songUrl,
|
||||
toogleMusic();
|
||||
}}
|
||||
>
|
||||
<BsmIcon className="w-full h-full p-7 opacity-0 group-hover:opacity-100 text-white hover:text-current" icon={songPlaying ? "pause" : "play"} />
|
||||
<BsmIcon className="size-full p-7 opacity-0 group-hover:opacity-100 text-white hover:text-current" icon={songPlaying ? "pause" : "play"} />
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative h-full w-full z-[1] rounded-md overflow-hidden -translate-x-1" ref={ref}>
|
||||
<BsmImage className="absolute top-0 left-0 w-full h-full -z-[1] object-cover saturate-200" image={coverUrl} placeholder={defaultImage} errorImage={defaultImage} />
|
||||
<div className="pt-1 pl-2 pr-7 top-0 left-0 w-full h-full bg-neutral-600 bg-opacity-80 flex flex-col justify-between group-hover:bg-main-color-1 group-hover:bg-opacity-80">
|
||||
<BsmImage className="absolute top-0 left-0 size-full -z-[1] object-cover saturate-200" image={coverUrl} placeholder={defaultImage} errorImage={defaultImage} />
|
||||
<div className="pt-1 pl-2 pr-7 top-0 left-0 size-full bg-neutral-600 bg-opacity-80 flex flex-col justify-between group-hover:bg-main-color-1 group-hover:bg-opacity-80">
|
||||
<h1 className="font-bold whitespace-nowrap text-ellipsis overflow-hidden w-full leading-5 tracking-wide text-lg" title={title}>
|
||||
<BsmLink className="hover:underline" href={mapUrl}>
|
||||
{title}
|
||||
@@ -241,18 +241,16 @@ export const MapItem = memo(({ hash, title, autor, songAutor, coverUrl, songUrl,
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute bg-light-main-color-3 dark:bg-main-color-3 top-0 h-full z-[1] w-[30px] -right-5 group-hover:right-0 transition-all">
|
||||
<div className="absolute bg-light-main-color-3 dark:bg-main-color-3 top-0 h-full z-[1] w-[30px] -right-5 group-hover:-translate-x-5 transition-transform">
|
||||
<span className="absolute w-[10px] h-[10px] top-0 right-full bg-inherit" style={{ clipPath: 'path("M11 -1 L11 10 L10 10 A10 10 0 0 0 0 0 L0 -1 Z")' }} />
|
||||
<span className="absolute w-[10px] h-[10px] bottom-0 right-full bg-inherit" 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 gap-1 w-full h-full overflow-hidden opacity-0 group-hover:opacity-100">
|
||||
<div className="flex flex-col justify-center items-center gap-1 size-full overflow-hidden opacity-0 group-hover:opacity-100">
|
||||
{onDelete && !downloading && (
|
||||
<Tippy content={t("maps.map-item.delete")} placement="left" theme="default">
|
||||
<div>
|
||||
<BsmButton
|
||||
className="w-6 h-6 p-[2px] rounded-md !bg-inherit hover:!bg-light-main-color-2 hover:dark:!bg-main-color-2"
|
||||
iconClassName="w-full h-full brightness-75 dark:brightness-200"
|
||||
iconColor={color}
|
||||
className="w-6 h-6 p-0.5 rounded-md !bg-inherit hover:!bg-light-main-color-2 hover:dark:!bg-main-color-2 text-red-500"
|
||||
iconClassName="size-full"
|
||||
icon="trash"
|
||||
withBar={false}
|
||||
onClick={e => {
|
||||
@@ -260,15 +258,13 @@ export const MapItem = memo(({ hash, title, autor, songAutor, coverUrl, songUrl,
|
||||
onDelete(callBackParam);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Tippy>
|
||||
)}
|
||||
{onDownload && !downloading && (
|
||||
<Tippy content={t("maps.map-item.download")} placement="left" theme="default">
|
||||
<div>
|
||||
<BsmButton
|
||||
className="w-6 h-6 p-[2px] rounded-md !bg-inherit hover:!bg-light-main-color-2 hover:dark:!bg-main-color-2"
|
||||
iconClassName="w-full h-full brightness-75 dark:brightness-200"
|
||||
className="w-6 h-6 p-0.5 rounded-md !bg-inherit hover:!bg-light-main-color-2 hover:dark:!bg-main-color-2"
|
||||
iconClassName="size-full brightness-75 dark:brightness-200"
|
||||
iconColor={color}
|
||||
icon="download"
|
||||
withBar={false}
|
||||
@@ -277,15 +273,13 @@ export const MapItem = memo(({ hash, title, autor, songAutor, coverUrl, songUrl,
|
||||
onDownload(callBackParam);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Tippy>
|
||||
)}
|
||||
{onCancelDownload && !downloading && (
|
||||
<Tippy content={t("maps.map-item.cancel-download")} placement="left" theme="default">
|
||||
<div>
|
||||
<BsmButton
|
||||
className="w-6 h-6 p-1 rounded-md !bg-inherit hover:!bg-light-main-color-2 hover:dark:!bg-main-color-2"
|
||||
iconClassName="w-full h-full brightness-75 dark:brightness-200"
|
||||
iconClassName="size-full brightness-75 dark:brightness-200"
|
||||
iconColor="red"
|
||||
icon="cross"
|
||||
withBar={false}
|
||||
@@ -294,7 +288,6 @@ export const MapItem = memo(({ hash, title, autor, songAutor, coverUrl, songUrl,
|
||||
onCancelDownload(callBackParam);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Tippy>
|
||||
)}
|
||||
{downloading &&
|
||||
@@ -306,10 +299,9 @@ export const MapItem = memo(({ hash, title, autor, songAutor, coverUrl, songUrl,
|
||||
}
|
||||
{previewUrl && (
|
||||
<Tippy content={t("maps.map-item.preview")} placement="left" theme="default">
|
||||
<div>
|
||||
<BsmButton
|
||||
className="w-6 h-6 p-[2px] rounded-md !bg-inherit hover:!bg-light-main-color-2 hover:dark:!bg-main-color-2"
|
||||
iconClassName="w-full h-full brightness-75 dark:brightness-200"
|
||||
className="w-6 h-6 p-0.5 rounded-md !bg-inherit hover:!bg-light-main-color-2 hover:dark:!bg-main-color-2"
|
||||
iconClassName="size-full brightness-75 dark:brightness-200"
|
||||
iconColor={color}
|
||||
icon="eye"
|
||||
withBar={false}
|
||||
@@ -318,15 +310,13 @@ export const MapItem = memo(({ hash, title, autor, songAutor, coverUrl, songUrl,
|
||||
openPreview();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Tippy>
|
||||
)}
|
||||
{mapId && (
|
||||
<Tippy content={t("maps.map-item.bsr-code")} placement="left" theme="default">
|
||||
<div>
|
||||
<BsmButton
|
||||
className="w-6 h-6 p-1 rounded-md !bg-inherit hover:!bg-light-main-color-2 hover:dark:!bg-main-color-2"
|
||||
iconClassName="w-full h-full brightness-75 dark:brightness-200"
|
||||
iconClassName="size-full brightness-75 dark:brightness-200"
|
||||
iconColor={color}
|
||||
icon="twitch"
|
||||
withBar={false}
|
||||
@@ -335,7 +325,6 @@ export const MapItem = memo(({ hash, title, autor, songAutor, coverUrl, songUrl,
|
||||
copyBsr();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Tippy>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,7 @@ export const MapsRow = memo(({ maps, style, selectedMaps$, onMapSelect, onMapDel
|
||||
const renderMapItem = (map: BsmLocalMap) => {
|
||||
|
||||
return <MapItem
|
||||
key={map.hash}
|
||||
key={map.path}
|
||||
hash={map.hash}
|
||||
title={map.rawInfo._songName}
|
||||
coverUrl={map.coverUrl}
|
||||
|
||||
+31
-22
@@ -6,7 +6,7 @@ import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { PlaylistsManagerService } from "renderer/services/playlists-manager.service";
|
||||
import { FolderLinkState } from "renderer/services/version-folder-linker.service";
|
||||
import { BehaviorSubject, finalize, lastValueFrom, map, of, tap } from "rxjs";
|
||||
import { BehaviorSubject, combineLatest, distinctUntilChanged, filter, finalize, lastValueFrom, map, merge, mergeAll, of, pipe, 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";
|
||||
@@ -15,6 +15,9 @@ import { useStateMap } from "renderer/hooks/use-state-map.hook";
|
||||
import { 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";
|
||||
import equal from "fast-deep-equal";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
|
||||
type Props = {
|
||||
version: BSVersion;
|
||||
@@ -27,12 +30,15 @@ export const LocalPlaylistsListPanel = forwardRef<unknown, Props>(({ version, cl
|
||||
|
||||
const playlistService = useService(PlaylistsManagerService);
|
||||
const modals = useService(ModalService);
|
||||
const ipc = useService(IpcService);
|
||||
|
||||
const isActiveOnce = useChangeUntilEqual(isActive, { untilEqual: true });
|
||||
|
||||
const { maps$ } = useContext(InstalledMapsContext);
|
||||
const { maps$, playlists$, setPlaylists } = useContext(InstalledMapsContext);
|
||||
|
||||
const playlists = useObservable(() => playlists$, []);
|
||||
|
||||
const [playlistsLoading, setPlaylistsLoading] = useState(false);
|
||||
const [playlists, setPlaylists] = useState<LocalBPListsDetails[]>([]);
|
||||
const loadPercent$ = useConstant(() => new BehaviorSubject<number>(0));
|
||||
const linked = useStateMap(linkedState, (newState, precMapped) => (newState === FolderLinkState.Pending || newState === FolderLinkState.Processing) ? precMapped : newState === FolderLinkState.Linked, false);
|
||||
|
||||
@@ -52,7 +58,7 @@ export const LocalPlaylistsListPanel = forwardRef<unknown, Props>(({ version, cl
|
||||
if(!isActiveOnce){ return noop(); }
|
||||
|
||||
loadLocalPlaylistsDetails().then(loadedPlaylists => {
|
||||
setPlaylists(() => loadedPlaylists);
|
||||
setPlaylists(loadedPlaylists);
|
||||
}).catch(() => {
|
||||
setPlaylists([]);
|
||||
}).finally(() => {
|
||||
@@ -61,27 +67,27 @@ export const LocalPlaylistsListPanel = forwardRef<unknown, Props>(({ version, cl
|
||||
|
||||
}, [isActiveOnce, version, linked]);
|
||||
|
||||
console.log(maps$.value);
|
||||
const viewPlaylistFile = (path: string) => {
|
||||
return lastValueFrom(ipc.sendV2("view-path-in-explorer", { args: path }));
|
||||
};
|
||||
|
||||
const deletePlaylist = (path: string) => {
|
||||
// !! 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 openPlaylistDetails = (playlistKey: string) => {
|
||||
const playlist = playlists.find(p => p.path === playlistKey);
|
||||
|
||||
console.log(playlist.songs);
|
||||
const localPlaylist$ = playlists$.pipe(map(playlists => playlists.find(p => p.path === playlistKey)));
|
||||
const installedMaps$ = combineLatest([maps$, localPlaylist$]).pipe(
|
||||
filter(([maps, playlist]) => !!maps && !!playlist),
|
||||
map(([maps, playlist]) => maps.filter(m => playlist.songs.some(song => song.hash.toLocaleLowerCase() === m.hash.toLocaleLowerCase()))),
|
||||
distinctUntilChanged(equal)
|
||||
);
|
||||
|
||||
modals.openModal(LocalPlaylistDetailsModal, {
|
||||
data: {
|
||||
version,
|
||||
title: playlist.playlistTitle,
|
||||
image: playlist.image,
|
||||
author: playlist.playlistAuthor,
|
||||
description: playlist.playlistDescription,
|
||||
nbMaps: playlist.nbMaps,
|
||||
duration: playlist.duration,
|
||||
maxNps: playlist.maxNps,
|
||||
minNps: playlist.minNps,
|
||||
nbMappers: playlist.nbMappers,
|
||||
installedMaps$: maps$.pipe(map(maps => maps.filter(m => playlist.songs.some(song => song.hash.toLocaleLowerCase() === m.hash.toLocaleLowerCase())))),
|
||||
},
|
||||
data: { version, localPlaylist$, installedMaps$ },
|
||||
noStyle: true,
|
||||
})
|
||||
};
|
||||
@@ -93,7 +99,7 @@ export const LocalPlaylistsListPanel = forwardRef<unknown, Props>(({ version, cl
|
||||
)
|
||||
}
|
||||
|
||||
if (playlists.length){
|
||||
if (playlists?.length){
|
||||
return (
|
||||
<ul className="relative size-full flex flex-row flex-wrap justify-start content-start p-3 gap-3">
|
||||
{playlists.map(p =>
|
||||
@@ -108,6 +114,9 @@ export const LocalPlaylistsListPanel = forwardRef<unknown, Props>(({ version, cl
|
||||
maxNps={p.maxNps}
|
||||
minNps={p.minNps}
|
||||
onClickOpen={() => openPlaylistDetails(p.path)}
|
||||
onClickDelete={() => deletePlaylist(p.path)}
|
||||
onClickSync={() => console.log("sync")}
|
||||
onClickOpenFile={() => viewPlaylistFile(p.path)}
|
||||
/>
|
||||
)}
|
||||
</ul>
|
||||
|
||||
@@ -9,6 +9,8 @@ import { NpsIcon } from 'renderer/components/svgs/icons/nps-icon.component';
|
||||
import { GlowEffect } from 'renderer/components/shared/glow-effect.component';
|
||||
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';
|
||||
|
||||
type Props = {
|
||||
title?: string;
|
||||
@@ -21,10 +23,28 @@ type Props = {
|
||||
minNps?: number;
|
||||
maxNps?: number;
|
||||
selected?: boolean;
|
||||
path?: string;
|
||||
onClickOpen?: () => void;
|
||||
onClickOpenFile?: () => void;
|
||||
onClickDelete?: () => void;
|
||||
onClickSync?: () => void;
|
||||
}
|
||||
|
||||
export function PlaylistItem({ title, author, coverUrl, coverBase64, duration, nbMaps, nbMappers, minNps, maxNps, selected, onClickOpen }: Props) {
|
||||
export function PlaylistItem({ title,
|
||||
author,
|
||||
coverUrl,
|
||||
coverBase64,
|
||||
duration,
|
||||
nbMaps,
|
||||
nbMappers,
|
||||
minNps,
|
||||
maxNps,
|
||||
selected,
|
||||
onClickOpen,
|
||||
onClickOpenFile,
|
||||
onClickSync,
|
||||
onClickDelete
|
||||
}: Props) {
|
||||
|
||||
const color = useThemeColor("first-color");
|
||||
|
||||
@@ -48,18 +68,17 @@ export function PlaylistItem({ title, author, coverUrl, coverBase64, duration, n
|
||||
// TODO : Translate
|
||||
|
||||
return (
|
||||
<motion.li className='relative flex-grow basis-0 min-w-80 h-28 cursor-pointer' onHoverStart={() => setHovered(() => true)} onHoverEnd={() => setHovered(() => false)} >
|
||||
<motion.li className='relative flex-grow basis-0 min-w-80 h-28 cursor-pointer group' onHoverStart={() => setHovered(() => true)} onHoverEnd={() => setHovered(() => false)} >
|
||||
<GlowEffect visible={selected || hovered}/>
|
||||
<div className="size-full relative flex flex-row justify-start items-center overflow-hidden rounded-md">
|
||||
<div className="absolute top-0 left-0 size-full flex justify-center items-center -z-[1]">
|
||||
<BsmImage className="size-full object-cover scale-150 saturate-150 blur-lg" image={coverUrl} base64={coverBase64} />
|
||||
<div className="absolute top-0 left-0 size-full bg-black opacity-15"/>
|
||||
<div className="absolute inset-0 flex justify-center items-center -z-[1]">
|
||||
<BsmImage className="size-full object-cover saturate-150 blur-lg" image={coverUrl} base64={coverBase64} />
|
||||
<div className="absolute inset-0 bg-black opacity-20"/>
|
||||
</div>
|
||||
<div className="relative h-full aspect-square p-2.5" style={{ color }}>
|
||||
<BsmImage className="size-full flex-shrink-0 object-cover rounded-md shadow-center shadow-black bg-main-color-1" image={coverUrl} base64={coverBase64} style={{filter: hovered && "brightness(75%)"}} />
|
||||
<SearchIcon
|
||||
className="absolute size-1/2 top-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2 transition-opacity duration-150 text-white hover:text-current"
|
||||
style={{ opacity: hovered ? 1 : 0}}
|
||||
className="absolute size-full top-0 left-0 p-7 opacity-0 text-white hover:text-current group-hover:opacity-100 transition-opacity"
|
||||
onClick={onClickOpen}
|
||||
/>
|
||||
</div>
|
||||
@@ -75,6 +94,43 @@ export function PlaylistItem({ title, author, coverUrl, coverBase64, duration, n
|
||||
</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">
|
||||
<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>}
|
||||
{onClickOpenFile && <Tippy content="Afficher le fichier" placement="left" theme="default">
|
||||
<BsmButton
|
||||
icon="folder"
|
||||
className="hover:!bg-main-color-1"
|
||||
iconClassName="size-full brightness-75 dark:brightness-200"
|
||||
style={{color}}
|
||||
onClick={onClickOpenFile}
|
||||
withBar={false}
|
||||
/>
|
||||
</Tippy>}
|
||||
{onClickDelete && <Tippy content="Supprimer" placement="left" theme="default">
|
||||
<BsmButton
|
||||
icon="trash"
|
||||
className="hover:!bg-main-color-1 text-red-500"
|
||||
iconClassName="size-full"
|
||||
onClick={onClickDelete}
|
||||
withBar={false}
|
||||
/>
|
||||
</Tippy>}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.li>
|
||||
|
||||
|
||||
@@ -206,7 +206,7 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
|
||||
/>
|
||||
<BsmSelect className="bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-1 pb-0.5 text-center" options={sortOptions} onChange={sort => handleSortChange(sort)} />
|
||||
</div>
|
||||
<ul className="w-full grow flex content-start flex-wrap gap-2 pt-1.5 px-2 overflow-y-scroll overflow-x-hidden scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900 z-0">
|
||||
<ul className="w-full grow flex content-start flex-wrap gap-2 pt-1.5 px-2 overflow-y-scroll overflow-x-hidden scrollbar-default z-0">
|
||||
{maps.length === 0 ? (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center">
|
||||
<img className={`w-32 h-32 ${loading && "spin-loading"}`} src={loading ? BeatWaitingImg : BeatConflictImg} alt=" " />
|
||||
|
||||
+1
-1
@@ -217,7 +217,7 @@ export const DownloadModelsModal: ModalComponent<void, { version: BSVersion; typ
|
||||
/>
|
||||
<BsmSelect className="bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-1 pb-0.5 text-center capitalize" options={querySortsOptions} onChange={value => currentSort$.next(value)} />
|
||||
</div>
|
||||
<ul className="w-full grow flex content-start flex-wrap gap-4 pt-1.5 px-2 overflow-y-scroll overflow-x-hidden scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900 z-0">
|
||||
<ul className="w-full grow flex content-start flex-wrap gap-4 pt-1.5 px-2 overflow-y-scroll overflow-x-hidden scrollbar-default z-0">
|
||||
{msModels.length === 0 ? (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center">
|
||||
<img className={`w-32 h-32 ${isLoading && "spin-loading"}`} src={isLoading && !error ? BeatWaitingImg : BeatConflictImg} alt=" " />
|
||||
|
||||
+71
-26
@@ -8,9 +8,15 @@ import { MapItem } from "renderer/components/maps-playlists-panel/maps/map-item.
|
||||
import { extractMapDiffs } from "renderer/components/maps-playlists-panel/maps/maps-row.component";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { AudioPlayerService } from "renderer/services/audio-player.service";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { BsmImage } from "renderer/components/shared/bsm-image.component";
|
||||
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";
|
||||
|
||||
interface Props extends Omit<PlaylistDetailsTemplateProps, "children"> {
|
||||
version: BSVersion
|
||||
interface Props {
|
||||
version: BSVersion;
|
||||
localPlaylist$: Observable<LocalBPListsDetails>;
|
||||
installedMaps$: Observable<BsmLocalMap[]>;
|
||||
}
|
||||
|
||||
@@ -18,7 +24,8 @@ export const LocalPlaylistDetailsModal: ModalComponent<void, Props> = ({resolver
|
||||
|
||||
const audioPlayer = useService(AudioPlayerService);
|
||||
|
||||
const installedMaps = useObservable(() => options.data.installedMaps$, undefined);
|
||||
const localPlaylist = useObservable(() => options.data.localPlaylist$, null);
|
||||
const installedMaps = useObservable(() => options.data.installedMaps$, null);
|
||||
|
||||
const playPlaylist = () => {
|
||||
if (!installedMaps) {
|
||||
@@ -29,37 +36,75 @@ export const LocalPlaylistDetailsModal: ModalComponent<void, Props> = ({resolver
|
||||
|
||||
const renderMaps = () => {
|
||||
if (!installedMaps) {
|
||||
// loading maps
|
||||
return null;
|
||||
}
|
||||
|
||||
if(installedMaps.length === 0) {
|
||||
// no maps
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="space-y-2 overflow-y-scroll pr-2 pl-2.5 pt-2 pb-5 scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900">
|
||||
{installedMaps.map(map => (
|
||||
<MapItem
|
||||
key={map.path}
|
||||
hash={map.hash}
|
||||
title={map.rawInfo._songName}
|
||||
coverUrl={map.coverUrl}
|
||||
songUrl={map.songUrl}
|
||||
autor={map.rawInfo._levelAuthorName}
|
||||
songAutor={map.rawInfo._songAuthorName}
|
||||
bpm={map.rawInfo._beatsPerMinute}
|
||||
duration={map.songDetails?.metadata.duration}
|
||||
diffs={extractMapDiffs({ rawMapInfo: map.rawInfo, songDetails: map.songDetails })}
|
||||
mapId={map.songDetails?.id}
|
||||
ranked={map.songDetails?.ranked}
|
||||
autorId={map.songDetails?.uploader.id}
|
||||
likes={map.songDetails?.upVotes}
|
||||
createdAt={map.songDetails?.uploadedAt}
|
||||
callBackParam={null}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
<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: .2, 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} 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>
|
||||
<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
|
||||
key={map.path}
|
||||
hash={map.hash}
|
||||
title={map.rawInfo._songName}
|
||||
coverUrl={map.coverUrl}
|
||||
songUrl={map.songUrl}
|
||||
autor={map.rawInfo._levelAuthorName}
|
||||
songAutor={map.rawInfo._songAuthorName}
|
||||
bpm={map.rawInfo._beatsPerMinute}
|
||||
duration={map.songDetails?.metadata.duration}
|
||||
diffs={extractMapDiffs({ rawMapInfo: map.rawInfo, songDetails: map.songDetails })}
|
||||
mapId={map.songDetails?.id}
|
||||
ranked={map.songDetails?.ranked}
|
||||
autorId={map.songDetails?.uploader.id}
|
||||
likes={map.songDetails?.upVotes}
|
||||
createdAt={map.songDetails?.uploadedAt}
|
||||
callBackParam={null}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<PlaylistDetailsTemplate {...options.data}>
|
||||
<PlaylistDetailsTemplate
|
||||
author={localPlaylist?.playlistAuthor}
|
||||
description={localPlaylist?.playlistDescription}
|
||||
image={localPlaylist?.image}
|
||||
duration={localPlaylist?.duration}
|
||||
maxNps={localPlaylist?.maxNps}
|
||||
minNps={localPlaylist?.minNps}
|
||||
nbMaps={localPlaylist?.nbMaps}
|
||||
nbMappers={localPlaylist?.nbMappers}
|
||||
title={localPlaylist?.playlistTitle}
|
||||
>
|
||||
{renderMaps()}
|
||||
</PlaylistDetailsTemplate>
|
||||
)
|
||||
|
||||
+2
-2
@@ -41,8 +41,8 @@ export function PlaylistDetailsTemplate({title, image, author, description, nbMa
|
||||
})();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-screen max-w-2xl h-[calc(100vh-1.25rem)] translate-y-5 bg-main-color-1 rounded-t-lg overflow-hidden">
|
||||
<header className="relative flex-shrink-0 h-36 overflow-hidden flex flex-row p-3">
|
||||
<div className="flex flex-col w-screen max-w-2xl h-screen max-h-[calc(100vh-1.25rem)] translate-y-2.5 bg-main-color-1 rounded-t-lg overflow-hidden">
|
||||
<header className="shrink-0 relative h-36 overflow-hidden flex flex-row p-3">
|
||||
<BsmImage className="absolute top-0 left-0 size-full object-cover blur-xl scale-150 brightness-75 saturate-200" base64={image}/>
|
||||
<BsmImage className="h-full aspect-square object-cover z-[1] rounded-md shadow-black shadow-center" base64={image}/>
|
||||
<div className="h-full px-3 text-white z-[1]">
|
||||
|
||||
@@ -73,7 +73,7 @@ export const ShareFoldersModal: ModalComponent<void, BSVersion> = ({ options: {d
|
||||
<form className="w-full max-w-md ">
|
||||
<h1 className="text-3xl uppercase tracking-wide w-full text-center text-gray-800 dark:text-gray-200">{t("modals.shared-folders.title")}</h1>
|
||||
<p className="my-3">{t("modals.shared-folders.description")}</p>
|
||||
<ul className="flex flex-col gap-1 mb-2 h-[300px] max-h-[300px] overflow-scroll scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900 px-1">
|
||||
<ul className="flex flex-col gap-1 mb-2 h-[300px] max-h-[300px] overflow-scroll scrollbar-default px-1">
|
||||
{folders.map((folder, index) => (
|
||||
<FolderItem
|
||||
key={folder}
|
||||
|
||||
@@ -220,7 +220,7 @@ export const ModelsGrid = forwardRef<unknown, Props>(({ className, version, type
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className="flex flex-wrap shrink-0 justify-start content-start w-full h-full overflow-y-scroll overflow-x-hidden p-4 gap-4 scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900">
|
||||
<ul className="flex flex-wrap shrink-0 justify-start content-start w-full h-full overflow-y-scroll overflow-x-hidden p-4 gap-4 scrollbar-default">
|
||||
{filtredModels().map(localModel => (
|
||||
<ModelItem {...localModel?.model} key={localModel.path} hash={localModel.model?.hash ?? localModel.hash} path={localModel.path} type={localModel.type} name={localModel.model?.name ?? localModel.fileName} selected={modelsSelected.some(m => m.hash === localModel.hash)} onClick={() => handleModelClick(localModel)} onDelete={() => handleDelete(localModel)} />
|
||||
))}
|
||||
|
||||
@@ -38,7 +38,7 @@ export function NavBar() {
|
||||
return (
|
||||
<nav id="nav-bar" className="z-10 flex flex-col h-full max-h-full items-center p-1">
|
||||
<BsManagerIcon className="relative aspect-square w-16 h-16 mb-3" />
|
||||
<ol id="versions" className="w-fit max-w-[150px] relative left-[2px] grow overflow-y-hidden scrollbar-track-transparent scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900 hover:overflow-y-scroll">
|
||||
<ol id="versions" className="w-fit max-w-[150px] relative left-[2px] grow overflow-y-hidden scrollbar-track-transparent scrollbar-default hover:overflow-y-scroll">
|
||||
<SharedNavBarItem />
|
||||
<NavBarSpliter />
|
||||
{listVersions().map(version => (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BsmIcon, BsmIconType } from "../svgs/bsm-icon.component";
|
||||
import { useRef, CSSProperties, MouseEvent } from "react";
|
||||
import { useRef, CSSProperties, MouseEvent, forwardRef, useCallback, ComponentProps } from "react";
|
||||
import { BsmImage } from "./bsm-image.component";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { useClickOutside } from "renderer/hooks/use-click-outside.hook";
|
||||
@@ -20,8 +20,8 @@ type Props = {
|
||||
active?: boolean;
|
||||
withBar?: boolean;
|
||||
disabled?: boolean;
|
||||
onClickOutside?: React.ComponentProps<"div">["onClick"];
|
||||
onClick?: React.ComponentProps<"div">["onClick"];
|
||||
onClickOutside?: ComponentProps<"div">["onClick"];
|
||||
onClick?: ComponentProps<"div">["onClick"];
|
||||
typeColor?: BsmButtonType;
|
||||
color?: string;
|
||||
title?: string;
|
||||
@@ -29,10 +29,22 @@ type Props = {
|
||||
textClassName?: string;
|
||||
};
|
||||
|
||||
export function BsmButton({ className, style, imgClassName, iconClassName, icon, image, text, type, active, withBar = true, disabled, onClickOutside, onClick, typeColor, color, title, iconColor, textClassName }: Props) {
|
||||
export const BsmButton = forwardRef<unknown, Props>(({ className, style, imgClassName, iconClassName, icon, image, text, type, active, withBar = true, disabled, onClickOutside, onClick, typeColor, color, title, iconColor, textClassName }, forwardedRef) => {
|
||||
const t = useTranslation();
|
||||
const { firstColor, secondColor } = useThemeColor();
|
||||
const ref = useRef(null);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const setRef = useCallback((node: HTMLDivElement) => {
|
||||
if (ref.current) {
|
||||
ref.current = node;
|
||||
}
|
||||
|
||||
if (typeof forwardedRef === 'function') {
|
||||
forwardedRef(node);
|
||||
} else if (forwardedRef) {
|
||||
forwardedRef.current = node;
|
||||
}
|
||||
}, [forwardedRef]);
|
||||
|
||||
useClickOutside(ref, onClickOutside);
|
||||
|
||||
@@ -72,7 +84,7 @@ export function BsmButton({ className, style, imgClassName, iconClassName, icon,
|
||||
const handleClick = (e: MouseEvent<HTMLDivElement>) => !disabled && onClick?.(e);
|
||||
|
||||
return (
|
||||
<div ref={ref} onClick={handleClick} title={t(title)} className={`${className} overflow-hidden group ${!withBar && !disabled && (!!typeColor || !!color) && "hover:brightness-[1.15]"} ${disabled ? "brightness-75 cursor-not-allowed" : "cursor-pointer"} ${renderTypeColor}`} style={{ ...style, backgroundColor: primaryColor || color }}>
|
||||
<div ref={setRef} onClick={handleClick} title={t(title)} className={`${className} overflow-hidden group ${!withBar && !disabled && (!!typeColor || !!color) && "hover:brightness-[1.15]"} ${disabled ? "brightness-75 cursor-not-allowed" : "cursor-pointer"} ${renderTypeColor}`} style={{ ...style, backgroundColor: primaryColor || color }}>
|
||||
{image && <BsmImage image={image} className={imgClassName} />}
|
||||
{icon && <BsmIcon icon={icon} className={iconClassName ?? "h-full w-full text-gray-800 dark:text-white"} style={{ color: iconColor || textColor }} />}
|
||||
{text &&
|
||||
@@ -93,4 +105,4 @@ export function BsmButton({ className, style, imgClassName, iconClassName, icon,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,5 +6,9 @@ type Props = {
|
||||
};
|
||||
|
||||
export function GlowEffect({ visible, className }: Props) {
|
||||
return <AnimatePresence>{visible && <motion.div transition={{ duration: 0.1 }} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className={`${className} glow-on-hover`} />}</AnimatePresence>;
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{visible && <motion.div transition={{ duration: 0.1 }} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} className={`${className} glow-on-hover`} />}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { CSSProperties } from "react";
|
||||
import { createSvgIcon } from "../svg-icon.type";
|
||||
|
||||
export function SyncIcon(props: { className?: string; style?: CSSProperties }) {
|
||||
export const SyncIcon = createSvgIcon((props, ref) => {
|
||||
return (
|
||||
<svg className={props.className} style={props.style} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
|
||||
<svg ref={ref} {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
|
||||
<path fill="currentColor" d="M25.625 32.542q-.917.416-1.667-.084t-.75-1.625q0-.5.354-1 .355-.5.855-.75 2.666-1.25 4.187-3.75 1.521-2.5 1.521-5.458 0-1.833-.854-3.708-.854-1.875-2.313-3.375l-.791-.709v2.75q0 .667-.459 1.105-.458.437-1.083.437-.667 0-1.104-.437-.438-.438-.438-1.105V8.125q0-.792.542-1.312.542-.521 1.292-.521h6.75q.666 0 1.104.437.437.438.437 1.104 0 .667-.437 1.084-.438.416-1.104.416h-3l.375.417q2.416 2.292 3.604 4.938 1.187 2.645 1.187 5.187 0 4.208-2.229 7.583t-5.979 5.084ZM8.417 33.708q-.667 0-1.084-.437-.416-.438-.416-1.104 0-.667.416-1.084.417-.416 1.084-.416h2.958L11 30.333q-2.5-2.208-3.667-4.729-1.166-2.521-1.166-5.479 0-4.208 2.271-7.604 2.27-3.396 5.979-5.063.916-.416 1.645.105.73.52.73 1.604 0 .5-.334 1-.333.5-.833.75-2.625 1.25-4.187 3.75-1.563 2.5-1.563 5.458 0 2.25.854 4.042.854 1.791 2.396 3.208l.792.542v-2.75q0-.667.458-1.105.458-.437 1.125-.437.625 0 1.062.437.438.438.438 1.105v6.708q0 .792-.542 1.313-.541.52-1.291.52Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { CSSProperties } from "react";
|
||||
import { createSvgIcon } from "../svg-icon.type";
|
||||
|
||||
export function TrashIcon(props: { className?: string; style?: CSSProperties }) {
|
||||
export const TrashIcon = 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="M10.917 35.667Q9.417 35.667 8.333 34.583Q7.25 33.5 7.25 32V9.375H5.083V5.708H14.208V3.833H25.75V5.708H34.917V9.375H32.75V32Q32.75 33.5 31.667 34.583Q30.583 35.667 29.083 35.667ZM29.083 9.375H10.917V32Q10.917 32 10.917 32Q10.917 32 10.917 32H29.083Q29.083 32 29.083 32Q29.083 32 29.083 32ZM14.833 28.708H18.083V12.625H14.833ZM21.917 28.708H25.167V12.625H21.917ZM10.917 9.375V32Q10.917 32 10.917 32Q10.917 32 10.917 32Q10.917 32 10.917 32Q10.917 32 10.917 32Z" />
|
||||
<svg ref={ref} {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" height="40" width="40" fill="currentColor">
|
||||
<path d="M10.917 35.667Q9.417 35.667 8.333 34.583Q7.25 33.5 7.25 32V9.375H5.083V5.708H14.208V3.833H25.75V5.708H34.917V9.375H32.75V32Q32.75 33.5 31.667 34.583Q30.583 35.667 29.083 35.667ZM29.083 9.375H10.917V32Q10.917 32 10.917 32Q10.917 32 10.917 32H29.083Q29.083 32 29.083 32Q29.083 32 29.083 32ZM14.833 28.708H18.083V12.625H14.833ZM21.917 28.708H25.167V12.625H21.917ZM10.917 9.375V32Q10.917 32 10.917 32Q10.917 32 10.917 32Q10.917 32 10.917 32Q10.917 32 10.917 32Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ForwardRefExoticComponent, ForwardedRef, RefAttributes, SVGProps, forwa
|
||||
|
||||
|
||||
export type SvgIcon = ForwardRefExoticComponent<Omit<SVGProps<SVGSVGElement>, "ref"> & RefAttributes<SVGSVGElement>>;
|
||||
export type SvgRenderFunction = (props: SVGProps<SVGSVGElement>, ref: ForwardedRef<SVGSVGElement>) => JSX.Element;
|
||||
export type SvgRenderFunction = (props: SVGProps<SVGSVGElement>, ref?: ForwardedRef<SVGSVGElement>) => JSX.Element;
|
||||
|
||||
export function createSvgIcon(render: SvgRenderFunction): SvgIcon {
|
||||
return forwardRef(render);
|
||||
|
||||
@@ -5,7 +5,7 @@ export function useObservable<T>(factory: () => Observable<T>, initValue?: T, de
|
||||
const [obsValue, setObsValue] = useState<T>(initValue);
|
||||
|
||||
useEffect(() => {
|
||||
const sub = factory().subscribe(val => setObsValue(() => val));
|
||||
const sub = factory().subscribe(val => setObsValue(val));
|
||||
return () => sub.unsubscribe();
|
||||
}, deps ?? []);
|
||||
|
||||
|
||||
@@ -78,6 +78,13 @@
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.scrollbar-default {
|
||||
/* scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900 */
|
||||
@apply scrollbar-thin;
|
||||
@apply scrollbar-thumb-rounded-full;
|
||||
@apply scrollbar-thumb-neutral-900;
|
||||
}
|
||||
|
||||
.tippy-box[data-theme~='default'] {
|
||||
@apply bg-neutral-900;
|
||||
@apply text-white;
|
||||
|
||||
@@ -248,7 +248,7 @@ export function SettingsPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex justify-center overflow-y-scroll scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900 text-gray-800 dark:text-gray-200">
|
||||
<div className="w-full h-full flex justify-center overflow-y-scroll scrollbar-default text-gray-800 dark:text-gray-200">
|
||||
<div className="max-w-2xl w-full h-fit">
|
||||
<div className="inline-block sticky top-8 left-[calc(100%)] translate-x-12 grow-0 w-9 h-9">
|
||||
<BsmButton className="inline-block grow-0 bg-transparent sticky h-full w-full top-20 right-20 !m-0 rounded-full p-1" onClick={() => nav(-1)} icon="close" withBar={false} />
|
||||
|
||||
@@ -252,8 +252,6 @@ export class SteamDownloaderService extends AbstractBsDownloaderService implemen
|
||||
|
||||
})();
|
||||
|
||||
// *** TEST EXPIRATION MOBILE APP APROVAL ***
|
||||
|
||||
return downloadPromise.then(() => {}).finally(() => {
|
||||
this.downloadProgress$.next(0);
|
||||
this.progressBarService.hide(true);
|
||||
|
||||
@@ -34,6 +34,10 @@ export class PlaylistsManagerService {
|
||||
return this.ipc.sendV2("get-version-playlists-details", { args: version });
|
||||
}
|
||||
|
||||
public deletePlaylist(opt: {path: string, deleteMaps?: boolean}): Observable<Progression> {
|
||||
return this.ipc.sendV2<Progression, {path: string, deleteMaps?: boolean}>("delete-playlist", { args: opt });
|
||||
}
|
||||
|
||||
public async linkVersion(version: BSVersion): Promise<boolean> {
|
||||
const modalRes = await this.modal.openModal(LinkPlaylistModal);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user