mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
[feature-107] can now list maps of playlist + lot of other things
This commit is contained in:
@@ -40,7 +40,7 @@ ipc.on("is-playlists-deep-links-enabled", (_, reply) => {
|
||||
}
|
||||
});
|
||||
|
||||
ipc.on<BSVersion>("get-version-playlists", (req, reply) => {
|
||||
ipc.on<BSVersion>("get-version-playlists-details", (req, reply) => {
|
||||
const playlists = LocalPlaylistsManagerService.getInstance();
|
||||
reply(playlists.getVersionPlaylists(req.args));
|
||||
reply(playlists.getVersionPlaylistsDetails(req.args));
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Progression, pathExist } from "../../helpers/fs.helpers";
|
||||
import { FileAssociationService } from "../file-association.service";
|
||||
import { SongDetailsCacheService } from "./maps/song-details-cache.service";
|
||||
import { sToMs } from "shared/helpers/time.helpers";
|
||||
import { LocalBPList, LocalBpListSong } from "shared/models/playlists/local-playlist.models";
|
||||
import { 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";
|
||||
@@ -108,7 +108,7 @@ export class LocalPlaylistsManagerService {
|
||||
this.windows.openWindow(`oneclick-download-playlist.html?playlistUrl=${downloadUrl}`);
|
||||
}
|
||||
|
||||
private readLocalBPListsOfFolder(folerPath: string, version?: BSVersion): Observable<Progression<LocalBPList[]>> {
|
||||
private getLocalBPListsOfFolder(folerPath: string): Observable<Progression<LocalBPList[]>> {
|
||||
return new Observable<Progression<LocalBPList[]>>(obs => {
|
||||
|
||||
const progress: Progression<LocalBPList[]> = { current: 0, total: 0, data: [] };
|
||||
@@ -116,13 +116,10 @@ export class LocalPlaylistsManagerService {
|
||||
|
||||
(async () => {
|
||||
|
||||
await this.songDetails.waitLoaded(sToMs(15));
|
||||
|
||||
if(!pathExistsSync(folerPath)) {
|
||||
throw new Error(`Playlists folder not found ${folerPath}`);
|
||||
}
|
||||
|
||||
const mapsFolder = version ? await this.maps.getMapsFolderPath(version) : null;
|
||||
const playlists = readdirSync(folerPath).filter(file => path.extname(file) === ".bplist");
|
||||
progress.total = playlists.length;
|
||||
|
||||
@@ -130,18 +127,8 @@ export class LocalPlaylistsManagerService {
|
||||
const playlistPath = path.join(folerPath, playlist);
|
||||
const bpList = await this.readPlaylistFile(playlistPath);
|
||||
|
||||
const localBpListSongs = bpList.songs.map(song => {
|
||||
const localInfos = mapsFolder ? this.songCache.getMapInfoFromHash(song.hash) : null;
|
||||
const coverPath = localInfos ? path.join(mapsFolder, localInfos.path, localInfos.info.rawInfo._coverImageFilename) : null;
|
||||
const songFilePath = localInfos ? path.join(mapsFolder, localInfos.path, localInfos.info.rawInfo._songFilename) : null;
|
||||
return ({
|
||||
song,
|
||||
songDetails: this.songDetails.getSongDetails(song.hash),
|
||||
coverUrl: (coverPath && pathExistsSync(coverPath)) ? pathToFileURL(coverPath).href : null,
|
||||
songUrl: (songFilePath && pathExistsSync(songFilePath)) ? pathToFileURL(songFilePath).href : null,
|
||||
} as LocalBpListSong)
|
||||
});
|
||||
const localBpList: LocalBPList = { ...bpList, path: playlistPath, songs: localBpListSongs };
|
||||
|
||||
const localBpList: LocalBPList = { ...bpList, path: playlistPath };
|
||||
bpLists.push(localBpList);
|
||||
progress.current += 1;
|
||||
obs.next(progress);
|
||||
@@ -153,18 +140,48 @@ export class LocalPlaylistsManagerService {
|
||||
});
|
||||
}
|
||||
|
||||
public getVersionPlaylists(version: BSVersion): Observable<Progression<LocalBPList[]>> {
|
||||
public getVersionPlaylistsDetails(version: BSVersion): Observable<Progression<LocalBPListsDetails[]>> {
|
||||
|
||||
console.log("getVersionPlaylists", version);
|
||||
return new Observable<Progression<LocalBPListsDetails[]>>(obs => {
|
||||
(async () => {
|
||||
|
||||
return new Observable<Progression<LocalBPList[]>>(obs => {
|
||||
this.getPlaylistsFolder(version)
|
||||
.then(folder => this.readLocalBPListsOfFolder(folder, version))
|
||||
.then(progress$ => lastValueFrom(progress$.pipe(
|
||||
tap(progress => obs.next({...progress, data: []})),
|
||||
)))
|
||||
.then(res => obs.next(res))
|
||||
.catch(err => obs.error(err))
|
||||
const folder = await this.getPlaylistsFolder(version);
|
||||
const progress$ = this.getLocalBPListsOfFolder(folder);
|
||||
|
||||
const localBPListsRes = (await lastValueFrom(progress$.pipe(tap({ next: progress => obs.next({...progress, data: []}) }))));
|
||||
|
||||
await this.songDetails.waitLoaded(sToMs(15));
|
||||
|
||||
const tryExtractPlaylistId = (url: string) => {
|
||||
const regex = /\/id\/(\d+)\/download/;
|
||||
const match = url.match(regex);
|
||||
return match ? Number(match[1]) : undefined;
|
||||
}
|
||||
|
||||
const bpListsDetails: LocalBPListsDetails[] = [];
|
||||
for(const bpList of localBPListsRes.data){
|
||||
|
||||
const bpListDetails: LocalBPListsDetails = {
|
||||
...bpList,
|
||||
nbMaps: bpList.songs?.length ?? 0,
|
||||
id: bpList.customData?.syncURL ? tryExtractPlaylistId(bpList.customData.syncURL) : undefined
|
||||
}
|
||||
|
||||
const songsDetails = bpList.songs?.map(s => this.songDetails.getSongDetails(s.hash));
|
||||
|
||||
if(songsDetails){
|
||||
bpListDetails.duration = songsDetails.reduce((acc, song) => acc + song.duration, 0);
|
||||
bpListDetails.nbMappers = new Set(songsDetails.map(s => s.uploader.id)).size;
|
||||
bpListDetails.minNps = Math.min(...songsDetails.map(s => Math.min(...s.difficulties.map(d => d.nps || 0))));
|
||||
bpListDetails.maxNps = Math.max(...songsDetails.map(s => Math.max(...s.difficulties.map(d => d.nps || 0))));
|
||||
}
|
||||
|
||||
bpListsDetails.push(bpListDetails);
|
||||
}
|
||||
|
||||
obs.next({...localBPListsRes, data: bpListsDetails});
|
||||
|
||||
})().catch(err => obs.error(err))
|
||||
.finally(() => obs.complete());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef, useState } from "react";
|
||||
import { Dispatch, SetStateAction, createContext, useMemo, useRef, useState } from "react";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { LocalMapsListPanel } from "./maps/local-maps-list-panel.component";
|
||||
import { BsmDropdownButton, DropDownItem } from "../shared/bsm-dropdown-button.component";
|
||||
@@ -14,15 +14,19 @@ import { BsmButton } from "../shared/bsm-button.component";
|
||||
import { MapIcon } from "../svgs/icons/map-icon.component";
|
||||
import { PlaylistIcon } from "../svgs/icons/playlist-icon.component";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { of } from "rxjs";
|
||||
import { BehaviorSubject, Observable, of } from "rxjs";
|
||||
import { LocalPlaylistsListPanel } from "./playlists/local-playlists-list-panel.component";
|
||||
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";
|
||||
|
||||
type Props = {
|
||||
version?: BSVersion;
|
||||
isActive?: boolean;
|
||||
};
|
||||
|
||||
export const InstalledMapsContext = createContext<{ maps$?: BehaviorSubject<BsmLocalMap[]>; setMaps: (maps: BsmLocalMap[]) => void }>(null);
|
||||
|
||||
export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
|
||||
const mapsService = useService(MapsManagerService);
|
||||
@@ -32,6 +36,9 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
const t = useTranslation();
|
||||
const [tabIndex, setTabIndex] = useState(0);
|
||||
|
||||
const maps$ = useConstant(() => new BehaviorSubject<BsmLocalMap[]>(undefined));
|
||||
const mapsContextValue = useConstant(() => ({ maps$: maps$, setMaps: maps$.next.bind(maps$) }));
|
||||
|
||||
const mapsRef = useRef<any>();
|
||||
const [mapFilter, setMapFilter] = useState<MapFilter>({});
|
||||
const [mapSearch, setMapSearch] = useState("");
|
||||
@@ -55,7 +62,7 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
};
|
||||
|
||||
const handleMapsAddClick = () => {
|
||||
mapsDownloader.openDownloadMapModal(version, mapsRef.current.getMaps?.());
|
||||
mapsDownloader.openDownloadMapModal(version, maps$.value);
|
||||
};
|
||||
|
||||
const handleMapsLinkClick = () => {
|
||||
@@ -132,10 +139,10 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
|
||||
},
|
||||
]}
|
||||
>
|
||||
<>
|
||||
<InstalledMapsContext.Provider value={mapsContextValue}>
|
||||
<LocalMapsListPanel className="w-full h-full shrink-0" isActive={isActive && tabIndex === 0} ref={mapsRef} version={version} filter={mapFilter} search={mapSearch} linkedState={mapsLinkedState} />
|
||||
<LocalPlaylistsListPanel className="w-full h-full shrink-0" isActive={isActive && tabIndex === 1} version={version} linkedState={playlistLinkedState}/>
|
||||
</>
|
||||
</InstalledMapsContext.Provider>
|
||||
</BsContentTabPanel>
|
||||
</div>
|
||||
);
|
||||
|
||||
+13
-11
@@ -1,6 +1,6 @@
|
||||
import { MapsManagerService } from "renderer/services/maps-manager.service";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react";
|
||||
import { forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useRef, useState } from "react";
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { Subscription, BehaviorSubject } from "rxjs";
|
||||
import { MapFilter, MapTag } from "shared/models/maps/beat-saver.model";
|
||||
@@ -18,6 +18,8 @@ import { FolderLinkState } from "renderer/services/version-folder-linker.service
|
||||
import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
|
||||
import { useConstant } from "renderer/hooks/use-constant.hook";
|
||||
import { BsContentLoader } from "renderer/components/shared/bs-content-loader.component";
|
||||
import { InstalledMapsContext } from "../maps-playlists-panel.component";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
|
||||
type Props = {
|
||||
version: BSVersion;
|
||||
@@ -33,8 +35,11 @@ export const LocalMapsListPanel = forwardRef<unknown, Props>(({ version, classNa
|
||||
const mapsDownloader = useService(MapsDownloaderService);
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
const ref = useRef(null);
|
||||
const [maps, setMaps] = useState<BsmLocalMap[]>(null);
|
||||
|
||||
const {maps$, setMaps} = useContext(InstalledMapsContext);
|
||||
const maps = useObservable(() => maps$, undefined);
|
||||
const [subs] = useState<Subscription[]>([]);
|
||||
const [selectedMaps$] = useState(new BehaviorSubject<BsmLocalMap[]>([]));
|
||||
const [itemPerRow, setItemPerRow] = useState(2);
|
||||
@@ -59,10 +64,7 @@ export const LocalMapsListPanel = forwardRef<unknown, Props>(({ version, classNa
|
||||
},
|
||||
exportMaps() {
|
||||
mapsManager.exportMaps(version, selectedMaps$.value);
|
||||
},
|
||||
getMaps() {
|
||||
return maps;
|
||||
},
|
||||
}
|
||||
}),
|
||||
[selectedMaps$.value, maps, version]
|
||||
);
|
||||
@@ -79,12 +81,12 @@ export const LocalMapsListPanel = forwardRef<unknown, Props>(({ version, classNa
|
||||
if (targerVersion !== version) {
|
||||
return;
|
||||
}
|
||||
setMaps(maps => (maps ? [map, ...maps] : [map]));
|
||||
setMaps((maps ? [map, ...maps] : [map]));
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
setMaps(() => null);
|
||||
setMaps(null);
|
||||
loadPercent$.next(0);
|
||||
subs.forEach(s => s.unsubscribe());
|
||||
mapsDownloader.removeOnMapDownloadedListener(loadMaps);
|
||||
@@ -122,7 +124,7 @@ export const LocalMapsListPanel = forwardRef<unknown, Props>(({ version, classNa
|
||||
}, [isActiveOnce, itemPerRow]);
|
||||
|
||||
const loadMaps = () => {
|
||||
setMaps(() => null);
|
||||
setMaps(null);
|
||||
loadPercent$.next(0);
|
||||
|
||||
const loadMapsObs$ = mapsManager.getMaps(version);
|
||||
@@ -134,7 +136,7 @@ export const LocalMapsListPanel = forwardRef<unknown, Props>(({ version, classNa
|
||||
last()
|
||||
)
|
||||
.subscribe({
|
||||
next: progress => setMaps(() => progress.maps),
|
||||
next: progress => setMaps(progress.maps),
|
||||
complete: () => loadPercent$.next(0),
|
||||
})
|
||||
);
|
||||
@@ -142,7 +144,7 @@ export const LocalMapsListPanel = forwardRef<unknown, Props>(({ version, classNa
|
||||
|
||||
const removeMapsFromList = (mapsToRemove: BsmLocalMap[]) => {
|
||||
const filtredMaps = maps.filter(map => !mapsToRemove.some(toDeleteMaps => map.hash === toDeleteMaps.hash));
|
||||
setMaps(() => filtredMaps);
|
||||
setMaps(filtredMaps);
|
||||
|
||||
const filtredSelectedMaps = selectedMaps$.value.filter(map => !mapsToRemove.some(toDeleteMaps => map.hash === toDeleteMaps.hash));
|
||||
selectedMaps$.next(filtredSelectedMaps);
|
||||
|
||||
@@ -155,7 +155,7 @@ export const MapItem = memo(({ hash, title, autor, songAutor, coverUrl, songUrl,
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.li className="relative h-[100px] w-[400px] min-w-[370px] shrink-0 grow basis-0 text-white group cursor-pointer" onHoverStart={() => setHovered(true)} onHoverEnd={() => setHovered(false)} style={{ zIndex: hovered && 5, transform: "translateZ(0) scale(1.0, 1.0)", backfaceVisibility: "hidden" }}>
|
||||
<motion.li className="relative h-[100px] min-w-[370px] shrink-0 grow basis-0 text-white group cursor-pointer" onHoverStart={() => setHovered(true)} onHoverEnd={() => setHovered(false)} style={{ zIndex: hovered && 5, transform: "translateZ(0) scale(1.0, 1.0)", backfaceVisibility: "hidden" }}>
|
||||
<GlowEffect visible={hovered || (selected && !!onSelected)} />
|
||||
<AnimatePresence>
|
||||
{(diffsPanelHovered || bottomBarHovered) && (
|
||||
|
||||
@@ -5,7 +5,8 @@ import { BehaviorSubject } from "rxjs";
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { ParsedMapDiff, MapItem } from "./map-item.component";
|
||||
import equal from "fast-deep-equal/es6";
|
||||
import { SongDetailDiffCharactertistic } from "shared/models/maps/song-details-cache.model";
|
||||
import { SongDetailDiffCharactertistic, SongDetails } from "shared/models/maps/song-details-cache.model";
|
||||
import { BsvMapDetail, RawMapInfoData } from "shared/models/maps";
|
||||
|
||||
type Props = {
|
||||
maps: BsmLocalMap[];
|
||||
@@ -21,29 +22,6 @@ export const MapsRow = memo(({ maps, style, selectedMaps$, onMapSelect, onMapDel
|
||||
distinctUntilChanged(equal),
|
||||
), []);
|
||||
|
||||
const extractMapDiffs = (map: BsmLocalMap): Map<SongDetailDiffCharactertistic, ParsedMapDiff[]> => {
|
||||
const res = new Map<SongDetailDiffCharactertistic, ParsedMapDiff[]>();
|
||||
if (map.songDetails?.difficulties) {
|
||||
map.songDetails?.difficulties.forEach(diff => {
|
||||
const arr = res.get(diff.characteristic) || [];
|
||||
const diffName = map.rawInfo._difficultyBeatmapSets.find(set => set._beatmapCharacteristicName === diff.characteristic)._difficultyBeatmaps.find(rawDiff => rawDiff._difficulty === diff.difficulty)?._customData?._difficultyLabel || diff.difficulty;
|
||||
arr.push({ name: diffName, type: diff.difficulty, stars: diff.stars });
|
||||
res.set(diff.characteristic, arr);
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
map.rawInfo._difficultyBeatmapSets.forEach(set => {
|
||||
set._difficultyBeatmaps.forEach(diff => {
|
||||
const arr = res.get(set._beatmapCharacteristicName) || [];
|
||||
arr.push({ name: diff._customData?._difficultyLabel || diff._difficulty, type: diff._difficulty, stars: null });
|
||||
res.set(set._beatmapCharacteristicName, arr);
|
||||
});
|
||||
});
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
const renderMapItem = (map: BsmLocalMap) => {
|
||||
|
||||
return <MapItem
|
||||
@@ -57,7 +35,7 @@ export const MapsRow = memo(({ maps, style, selectedMaps$, onMapSelect, onMapDel
|
||||
bpm={map.rawInfo._beatsPerMinute}
|
||||
duration={map.songDetails?.metadata.duration}
|
||||
selected={selectedMaps.some(selected => selected.hash === map.hash)}
|
||||
diffs={extractMapDiffs(map)}
|
||||
diffs={extractMapDiffs({ rawMapInfo: map.rawInfo, songDetails: map.songDetails })}
|
||||
mapId={map.songDetails?.id}
|
||||
ranked={map.songDetails?.ranked}
|
||||
autorId={map.songDetails?.uploader.id}
|
||||
@@ -75,3 +53,35 @@ export const MapsRow = memo(({ maps, style, selectedMaps$, onMapSelect, onMapDel
|
||||
</ul>
|
||||
);
|
||||
}, equal);
|
||||
|
||||
export function extractMapDiffs({rawMapInfo, songDetails, bsvMap}: {rawMapInfo?: RawMapInfoData, songDetails?: SongDetails, bsvMap?: BsvMapDetail}): Map<SongDetailDiffCharactertistic, ParsedMapDiff[]> {
|
||||
const res = new Map<SongDetailDiffCharactertistic, ParsedMapDiff[]>();
|
||||
if (bsvMap && bsvMap.versions?.at(0)?.diffs) {
|
||||
bsvMap.versions.at(0).diffs.forEach(diff => {
|
||||
const arr = res.get(diff.characteristic) || [];
|
||||
arr.push({ name: diff.difficulty, type: diff.difficulty, stars: diff.stars });
|
||||
res.set(diff.characteristic, arr);
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
if (songDetails?.difficulties) {
|
||||
songDetails?.difficulties.forEach(diff => {
|
||||
const arr = res.get(diff.characteristic) || [];
|
||||
const diffName = rawMapInfo._difficultyBeatmapSets.find(set => set._beatmapCharacteristicName === diff.characteristic)._difficultyBeatmaps.find(rawDiff => rawDiff._difficulty === diff.difficulty)?._customData?._difficultyLabel || diff.difficulty;
|
||||
arr.push({ name: diffName, type: diff.difficulty, stars: diff.stars });
|
||||
res.set(diff.characteristic, arr);
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
rawMapInfo._difficultyBeatmapSets.forEach(set => {
|
||||
set._difficultyBeatmaps.forEach(diff => {
|
||||
const arr = res.get(set._beatmapCharacteristicName) || [];
|
||||
arr.push({ name: diff._customData?._difficultyLabel || diff._difficulty, type: diff._difficulty, stars: null });
|
||||
res.set(set._beatmapCharacteristicName, arr);
|
||||
});
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
+54
-59
@@ -1,4 +1,4 @@
|
||||
import { forwardRef, useState } from "react";
|
||||
import { forwardRef, useContext, useState } from "react";
|
||||
import { BsContentLoader } from "renderer/components/shared/bs-content-loader.component";
|
||||
import { useChangeUntilEqual } from "renderer/hooks/use-change-until-equal.hook";
|
||||
import { useConstant } from "renderer/hooks/use-constant.hook";
|
||||
@@ -6,12 +6,15 @@ import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { PlaylistsManagerService } from "renderer/services/playlists-manager.service";
|
||||
import { FolderLinkState } from "renderer/services/version-folder-linker.service";
|
||||
import { BehaviorSubject, finalize, lastValueFrom, map, tap } from "rxjs";
|
||||
import { BehaviorSubject, finalize, lastValueFrom, map, of, tap } from "rxjs";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { noop } from "shared/helpers/function.helpers";
|
||||
import { LocalBPList } from "shared/models/playlists/local-playlist.models";
|
||||
import { 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 { LocalPlaylistDetailsModal } from "renderer/components/modal/modal-types/playlist/local-playlist-details-modal.component";
|
||||
import { InstalledMapsContext } from "../maps-playlists-panel.component";
|
||||
|
||||
type Props = {
|
||||
version: BSVersion;
|
||||
@@ -23,17 +26,19 @@ type Props = {
|
||||
export const LocalPlaylistsListPanel = forwardRef<unknown, Props>(({ version, className, isActive, linkedState }, forwardedRef) => {
|
||||
|
||||
const playlistService = useService(PlaylistsManagerService);
|
||||
const modals = useService(ModalService);
|
||||
|
||||
const isActiveOnce = useChangeUntilEqual(isActive, { untilEqual: true });
|
||||
|
||||
const { maps$ } = useContext(InstalledMapsContext);
|
||||
const [playlistsLoading, setPlaylistsLoading] = useState(false);
|
||||
const [playlists, setPlaylists] = useState<LocalBPList[]>([]);
|
||||
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);
|
||||
|
||||
const loadPlaylists = (): Promise<LocalBPList[]> => {
|
||||
const loadLocalPlaylistsDetails = (): Promise<LocalBPListsDetails[]> => {
|
||||
setPlaylistsLoading(true);
|
||||
const obs = playlistService.getVersionPlaylists(version).pipe(
|
||||
const obs = playlistService.getVersionPlaylistsDetails(version).pipe(
|
||||
tap({ next: load => loadPercent$.next((load.current / load.total) * 100)}),
|
||||
map(load => load.data),
|
||||
finalize(() => setPlaylistsLoading(false))
|
||||
@@ -42,52 +47,45 @@ export const LocalPlaylistsListPanel = forwardRef<unknown, Props>(({ version, cl
|
||||
return lastValueFrom(obs);
|
||||
}
|
||||
|
||||
const getNbMappersOfPlaylist = (playlist: LocalBPList) => {
|
||||
return new Set(playlist.songs.map(s => s.songDetails?.uploader?.id ?? s.song.hash)).size;
|
||||
};
|
||||
|
||||
const getDurationOfPlaylist = (playlist: LocalBPList) => {
|
||||
return playlist.songs.reduce((acc, s) => acc + (s.songDetails?.duration ?? 0), 0);
|
||||
}
|
||||
|
||||
const getMinNpsOfPlaylist = (playlist: LocalBPList) => {
|
||||
let min = Infinity;
|
||||
playlist.songs.forEach(s => {
|
||||
s.songDetails?.difficulties.forEach(d => {
|
||||
if(d.nps < min){
|
||||
min = d.nps;
|
||||
}
|
||||
})
|
||||
});
|
||||
return min === Infinity ? null : min;
|
||||
}
|
||||
|
||||
const getMaxNpsOfPlaylist = (playlist: LocalBPList) => {
|
||||
let max = -Infinity;
|
||||
playlist.songs.forEach(s => {
|
||||
s.songDetails?.difficulties.forEach(d => {
|
||||
if(d.nps > max){
|
||||
max = d.nps;
|
||||
}
|
||||
})
|
||||
});
|
||||
return max === -Infinity ? null : max;
|
||||
}
|
||||
|
||||
useOnUpdate(() => {
|
||||
|
||||
if(!isActiveOnce){ return noop(); }
|
||||
|
||||
loadPlaylists().then(loadedPlaylists => {
|
||||
loadLocalPlaylistsDetails().then(loadedPlaylists => {
|
||||
setPlaylists(() => loadedPlaylists);
|
||||
}).catch(() => {
|
||||
setPlaylists([])
|
||||
setPlaylists([]);
|
||||
}).finally(() => {
|
||||
loadPercent$.next(0);
|
||||
});
|
||||
|
||||
}, [isActiveOnce, version, linked]);
|
||||
|
||||
console.log(maps$.value);
|
||||
|
||||
const openPlaylistDetails = (playlistKey: string) => {
|
||||
const playlist = playlists.find(p => p.path === playlistKey);
|
||||
|
||||
console.log(playlist.songs);
|
||||
|
||||
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())))),
|
||||
},
|
||||
noStyle: true,
|
||||
})
|
||||
};
|
||||
|
||||
const render = () => {
|
||||
if(playlistsLoading){
|
||||
return (
|
||||
@@ -97,25 +95,22 @@ export const LocalPlaylistsListPanel = forwardRef<unknown, Props>(({ version, cl
|
||||
|
||||
if (playlists.length){
|
||||
return (
|
||||
<>
|
||||
<ul className="size-full flex flex-row flex-wrap justify-start content-start p-3 gap-3 overflow-y-scroll">
|
||||
{playlists.map(p =>
|
||||
<PlaylistItem
|
||||
key={p.path}
|
||||
id={p.path}
|
||||
title={p.playlistTitle}
|
||||
author={p.playlistAuthor}
|
||||
coverBase64={p.image}
|
||||
nbMaps={p.songs.length}
|
||||
nbMappers={getNbMappersOfPlaylist(p)}
|
||||
duration={getDurationOfPlaylist(p)}
|
||||
maxNps={getMaxNpsOfPlaylist(p)}
|
||||
minNps={getMinNpsOfPlaylist(p)}
|
||||
/>
|
||||
)}
|
||||
</ul>
|
||||
<br/>
|
||||
</>
|
||||
<ul className="relative size-full flex flex-row flex-wrap justify-start content-start p-3 gap-3">
|
||||
{playlists.map(p =>
|
||||
<PlaylistItem
|
||||
key={p.path}
|
||||
title={p.playlistTitle}
|
||||
author={p.playlistAuthor}
|
||||
coverBase64={p.image}
|
||||
nbMaps={p.nbMaps}
|
||||
nbMappers={p.nbMappers}
|
||||
duration={p.duration}
|
||||
maxNps={p.maxNps}
|
||||
minNps={p.minNps}
|
||||
onClickOpen={() => openPlaylistDetails(p.path)}
|
||||
/>
|
||||
)}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+18
-12
@@ -8,11 +8,9 @@ import dateFormat from 'dateformat';
|
||||
import { NpsIcon } from 'renderer/components/svgs/icons/nps-icon.component';
|
||||
import { GlowEffect } from 'renderer/components/shared/glow-effect.component';
|
||||
import { useState } from 'react';
|
||||
import { FolderOpenIcon } from 'renderer/components/svgs/icons/folder-open-icon.component';
|
||||
import { SearchIcon } from 'renderer/components/svgs/icons/search-icon.component';
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
className?: string;
|
||||
title?: string;
|
||||
author?: string;
|
||||
coverBase64?: string;
|
||||
@@ -23,9 +21,10 @@ type Props = {
|
||||
minNps?: number;
|
||||
maxNps?: number;
|
||||
selected?: boolean;
|
||||
onClickOpen?: () => void;
|
||||
}
|
||||
|
||||
export function PlaylistItem({ id, className, title, author, coverUrl, coverBase64, duration, nbMaps, nbMappers, minNps, maxNps, selected }: Props) {
|
||||
export function PlaylistItem({ title, author, coverUrl, coverBase64, duration, nbMaps, nbMappers, minNps, maxNps, selected, onClickOpen }: Props) {
|
||||
|
||||
const color = useThemeColor("first-color");
|
||||
|
||||
@@ -33,8 +32,9 @@ export function PlaylistItem({ id, className, title, author, coverUrl, coverBase
|
||||
|
||||
const nbMapsText = nbMaps ? Intl.NumberFormat(undefined, { notation: "compact" }).format(nbMaps).trim() : null;
|
||||
const nbMappersText = nbMappers ? Intl.NumberFormat(undefined, { notation: "compact" }).format(nbMappers).trim() : null;
|
||||
const minNpsText = minNps ? Math.round(minNps * 10) / 10 : null;
|
||||
const maxNpsText = maxNps ? Math.round(maxNps * 10) / 10 : null;
|
||||
const minNpsText = minNps ? Math.round(minNps * 10) / 10 : 0;
|
||||
const maxNpsText = maxNps ? Math.round(maxNps * 10) / 10 : 0;
|
||||
const showNps = minNps !== undefined && maxNps !== undefined;
|
||||
|
||||
const durationText = (() => {
|
||||
if (!duration) {
|
||||
@@ -45,27 +45,33 @@ export function PlaylistItem({ id, className, title, author, coverUrl, coverBase
|
||||
return duration > 3600 ? dateFormat(date, "h:MM:ss") : dateFormat(date, "MM:ss");
|
||||
})();
|
||||
|
||||
// TODO : Translate
|
||||
|
||||
return (
|
||||
<motion.li id={id} layoutId={id} 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' 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 ${className}`}>
|
||||
<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-[2] saturate-150 blur-xl" image={coverUrl} base64={coverBase64} />
|
||||
<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>
|
||||
<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%)"}} />
|
||||
<FolderOpenIcon 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}}/>
|
||||
<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}}
|
||||
onClick={onClickOpen}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-full py-2.5 text-white">
|
||||
<h1 className="font-bold text-lg tracking-wide line-clamp-1 hover:underline w-fit">{title}</h1>
|
||||
<h1 className="font-bold text-xl tracking-wide line-clamp-1 w-fit">{title}</h1>
|
||||
<p className="text-xs font-bold">Créé par <span className="brightness-200" style={{color}}>{author}</span></p>
|
||||
|
||||
<div className="flex flex-row flex-wrap w-full gap-2 mt-1">
|
||||
{ nbMapsText && <div className="flex items-center text-sm h-5 gap-0.5"> <MapIcon className='h-full aspect-square'/> <span className="mb-0.5">{nbMapsText}</span> </div> }
|
||||
{ nbMappersText && <div className="flex items-center text-sm h-5 gap-0.5"> <PersonIcon className='h-full aspect-square'/> <span className="mb-0.5">{nbMappersText}</span> </div> }
|
||||
{ durationText && <div className="flex items-center text-sm h-5 gap-0.5"> <ClockIcon className='h-full aspect-square'/> <span className="mb-0.5">{durationText}</span> </div> }
|
||||
{ (minNps && maxNps) && <div className="flex items-center text-sm h-5 gap-0.5"> <NpsIcon className='h-full aspect-square scale-95'/> <span className="mb-0.5">{`${minNpsText} - ${maxNpsText}`}</span> </div> }
|
||||
{ showNps && <div className="flex items-center text-sm h-5 gap-0.5"> <NpsIcon className='h-full aspect-square scale-95'/> <span className="mb-0.5">{`${minNpsText} - ${maxNpsText}`}</span> </div> }
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
+5
-5
@@ -13,8 +13,8 @@ import { BsmBasicSpinner } from "renderer/components/shared/bsm-basic-spinner/bs
|
||||
export const LoginToSteamModal: ModalComponent<
|
||||
{ username: string; password: string; stay: boolean, method: "form"|"qr" },
|
||||
{ qrCode$: Observable<string>, logged$: Observable<string> }
|
||||
> = ({ resolver, data }) => {
|
||||
|
||||
> = ({ resolver, options: {data} }) => {
|
||||
|
||||
const modal = useService(ModalService);
|
||||
|
||||
const [username, setUsername] = useState("");
|
||||
@@ -82,10 +82,10 @@ export const LoginToSteamModal: ModalComponent<
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col max-w-[13rem]">
|
||||
<span className="block font-bold cursor-pointer tracking-wide ">{t("modals.steam-login.inputs.qr.label")}</span>
|
||||
<span className="block font-bold cursor-pointer tracking-wide ">{t("modals.steam-login.inputs.qr.label")}</span>
|
||||
<div className="w-52 h-52 p-3 bg-light-main-color-1 dark:bg-white rounded-md max-w-xs mb-1 flex items-center justify-center">
|
||||
{(qrCodeUrl ?
|
||||
<QRCodeSVG className="w-full h-full text-light-main-color-1 dark:text-white" value={qrCodeUrl} bgColor="currentColor" level="H"/> :
|
||||
{(qrCodeUrl ?
|
||||
<QRCodeSVG className="w-full h-full text-light-main-color-1 dark:text-white" value={qrCodeUrl} bgColor="currentColor" level="H"/> :
|
||||
<BsmBasicSpinner className="w-full h-full p-11 text-neutral-300" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
+3
-3
@@ -6,8 +6,8 @@ import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { Observable } from "rxjs";
|
||||
import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
|
||||
|
||||
export const SteamMobileApproveModal: ModalComponent<void, { logged$: Observable<unknown> }> = ({ resolver, data }) => {
|
||||
|
||||
export const SteamMobileApproveModal: ModalComponent<void, { logged$: Observable<unknown> }> = ({ resolver, options: {data} }) => {
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
useOnUpdate(() => {
|
||||
@@ -36,4 +36,4 @@ export const SteamMobileApproveModal: ModalComponent<void, { logged$: Observable
|
||||
<a className="underline text-sm mt-2.5" href="https://help.steampowered.com/wizard/HelpWithLoginInfo?lost=8&issueid=402" target="_blank">{t("modals.steam-auth-approve.not-access-to-steam-app")}</a>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import DOMPurify from 'dompurify';
|
||||
import './changelog-modal.component.css';
|
||||
import Tippy from "@tippyjs/react";
|
||||
|
||||
export const ChangelogModal: ModalComponent<void, ChangelogVersion> = ({ resolver, data: changelog }) => {
|
||||
export const ChangelogModal: ModalComponent<void, ChangelogVersion> = ({ resolver, options: {data: changelog} }) => {
|
||||
|
||||
const linkOpener = useService(LinkOpenerService);
|
||||
const openGithub = () => linkOpener.open("https://github.com/Zagrios/bs-manager");
|
||||
|
||||
+5
-5
@@ -12,7 +12,7 @@ import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import { ChevronTopIcon } from "renderer/components/svgs/icons/chevron-top-icon.component";
|
||||
import Tippy from "@tippyjs/react";
|
||||
|
||||
export const CreateLaunchShortcutModal: ModalComponent<LaunchOption, BSVersion> = ({resolver, data}) => {
|
||||
export const CreateLaunchShortcutModal: ModalComponent<LaunchOption, BSVersion> = ({resolver, options: {data}}) => {
|
||||
|
||||
const bsLauncher = useService(BSLauncherService);
|
||||
|
||||
@@ -76,10 +76,10 @@ export const CreateLaunchShortcutModal: ModalComponent<LaunchOption, BSVersion>
|
||||
<div className={`grid grid-rows-[0fr] transition-[grid-template-rows] ${advanced ? "!grid-rows-[1fr]" : ""}`}>
|
||||
<div className="overflow-hidden">
|
||||
<div className="p-2">
|
||||
<input
|
||||
type="text"
|
||||
className="w-full rounded-md text-center outline-none bg-light-main-color-3 dark:bg-main-color-3"
|
||||
placeholder={t("pages.version-viewer.launch-mods.advanced-launch.placeholder")}
|
||||
<input
|
||||
type="text"
|
||||
className="w-full rounded-md text-center outline-none bg-light-main-color-3 dark:bg-main-color-3"
|
||||
placeholder={t("pages.version-viewer.launch-mods.advanced-launch.placeholder")}
|
||||
value={additionalArgsString}
|
||||
onChange={e => setAdditionalArgsString(e.target.value)}
|
||||
/>
|
||||
|
||||
@@ -10,8 +10,8 @@ import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import BeatConflict from "../../../../../assets/images/apngs/beat-conflict.png";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
|
||||
export const DeleteMapsModal: ModalComponent<void, { linked: boolean; maps: BsmLocalMap[] }> = ({ resolver, data: { linked, maps } }) => {
|
||||
|
||||
export const DeleteMapsModal: ModalComponent<void, { linked: boolean; maps: BsmLocalMap[] }> = ({ resolver, options: {data : { linked, maps }}}) => {
|
||||
|
||||
const config = useService(ConfigurationService);
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
@@ -20,8 +20,9 @@ import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { OsDiagnosticService } from "renderer/services/os-diagnostic.service";
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { extractMapDiffs } from "renderer/components/maps-playlists-panel/maps/maps-row.component";
|
||||
|
||||
export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; ownedMaps: BsmLocalMap[] }> = ({ data: { ownedMaps, version } }) => {
|
||||
export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; ownedMaps: BsmLocalMap[] }> = ({ options: {data : { ownedMaps, version }} }) => {
|
||||
const beatSaver = useService(BeatSaverService);
|
||||
const mapsDownloader = useService(MapsDownloaderService);
|
||||
const progressBar = useService(ProgressBarService);
|
||||
@@ -84,24 +85,32 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
|
||||
.finally(() => setLoading(() => false));
|
||||
};
|
||||
|
||||
const extractMapDiffs = (map: BsvMapDetail): Map<BsvMapCharacteristic, ParsedMapDiff[]> => {
|
||||
const res = new Map<BsvMapCharacteristic, ParsedMapDiff[]>();
|
||||
if (map.versions.at(0).diffs) {
|
||||
map.versions.at(0).diffs.forEach(diff => {
|
||||
const arr = res.get(diff.characteristic) || [];
|
||||
arr.push({ name: diff.difficulty, type: diff.difficulty, stars: diff.stars });
|
||||
res.set(diff.characteristic, arr);
|
||||
});
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
const renderMap = (map: BsvMapDetail) => {
|
||||
const isMapOwned = map.versions.some(version => ownedMapHashs.includes(version.hash));
|
||||
const isDownloading = map.id === currentDownload?.map?.id;
|
||||
const inQueue = mapsInQueue.some(toDownload => equal(toDownload.version, version) && toDownload.map.id === map.id);
|
||||
|
||||
return <MapItem autor={map.metadata.levelAuthorName} autorId={map.uploader.id} bpm={map.metadata.bpm} coverUrl={map.versions.at(0).coverURL} createdAt={map.createdAt} duration={map.metadata.duration} hash={map.versions.at(0).hash} likes={map.stats.upvotes} mapId={map.id} ranked={map.ranked} title={map.name} songAutor={map.metadata.songAuthorName} diffs={extractMapDiffs(map)} songUrl={map.versions.at(0).previewURL} key={map.id} onDownload={!isMapOwned && !inQueue && handleDownloadMap} onDoubleClick={!isMapOwned && !inQueue && handleDownloadMap} onCancelDownload={inQueue && !isDownloading && handleCancelDownload} downloading={isDownloading} showOwned={isMapOwned} callBackParam={map} />;
|
||||
return <MapItem
|
||||
autor={map.metadata.levelAuthorName}
|
||||
autorId={map.uploader.id}
|
||||
bpm={map.metadata.bpm}
|
||||
coverUrl={map.versions.at(0).coverURL}
|
||||
createdAt={map.createdAt}
|
||||
duration={map.metadata.duration}
|
||||
hash={map.versions.at(0).hash}
|
||||
likes={map.stats.upvotes}
|
||||
mapId={map.id} ranked={map.ranked}
|
||||
title={map.name}
|
||||
songAutor={map.metadata.songAuthorName}
|
||||
diffs={extractMapDiffs({ bsvMap: map })}
|
||||
songUrl={map.versions.at(0).previewURL}
|
||||
key={map.id}
|
||||
onDownload={!isMapOwned && !inQueue && handleDownloadMap}
|
||||
onDoubleClick={!isMapOwned && !inQueue && handleDownloadMap}
|
||||
onCancelDownload={inQueue && !isDownloading && handleCancelDownload}
|
||||
downloading={isDownloading}
|
||||
showOwned={isMapOwned}
|
||||
callBackParam={map} />;
|
||||
};
|
||||
|
||||
const handleDownloadMap = useCallback((map: BsvMapDetail) => {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ConfigurationService } from "renderer/services/configuration.service";
|
||||
import { ModalComponent, ModalExitCode } from "renderer/services/modale.service";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
|
||||
export const EditVersionModal: ModalComponent<{ name: string; color: string }, { version: BSVersion; clone?: boolean }> = ({ resolver, data }) => {
|
||||
export const EditVersionModal: ModalComponent<{ name: string; color: string }, { version: BSVersion; clone?: boolean }> = ({ resolver, options: {data} }) => {
|
||||
const { version, clone } = data;
|
||||
|
||||
const configService = useService(ConfigurationService);
|
||||
|
||||
@@ -12,7 +12,7 @@ import { ConfigurationService } from "renderer/services/configuration.service";
|
||||
import { ModelsManagerService } from "renderer/services/models-management/models-manager.service";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
|
||||
export const DeleteModelsModal: ModalComponent<void, { models: BsmLocalModel[]; linked: boolean }> = ({ resolver, data }) => {
|
||||
export const DeleteModelsModal: ModalComponent<void, { models: BsmLocalModel[]; linked: boolean }> = ({ resolver, options: {data} }) => {
|
||||
const config = useService(ConfigurationService);
|
||||
const t = useTranslation();
|
||||
const [remember, setRemember] = useState(config.get<boolean>(ModelsManagerService.REMEMBER_CHOICE_DELETE_MODEL_KEY) || false);
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ import { catchError, of } from "rxjs";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import { BsmLocalModel } from "shared/models/models/bsm-local-model.interface";
|
||||
|
||||
export const DownloadModelsModal: ModalComponent<void, { version: BSVersion; type: MSModelType; owned: BsmLocalModel[] }> = ({ data: { version, type, owned } }) => {
|
||||
export const DownloadModelsModal: ModalComponent<void, { version: BSVersion; type: MSModelType; owned: BsmLocalModel[] }> = ({ options: {data: { version, type, owned }} }) => {
|
||||
const modelsDownloader = useService(ModelsDownloaderService);
|
||||
const modelSaber = useService(ModelSaberService);
|
||||
const os = useService(OsDiagnosticService);
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ModalComponent, ModalExitCode } from "renderer/services/modale.service"
|
||||
import { MSModelType } from "shared/models/models/model-saber.model";
|
||||
import BeatRunning from "../../../../../../assets/images/apngs/beat-running.png";
|
||||
|
||||
export const LinkModelsModal: ModalComponent<boolean, MSModelType> = ({ resolver, data }) => {
|
||||
export const LinkModelsModal: ModalComponent<boolean, MSModelType> = ({ resolver, options: {data} }) => {
|
||||
const t = useTranslation();
|
||||
const [keepMaps, setKeepMaps] = useState(true);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { ModalComponent, ModalExitCode } from "renderer/services/modale.service"
|
||||
import BeatConflict from "../../../../../../assets/images/apngs/beat-conflict.png";
|
||||
import { MSModelType } from "shared/models/models/model-saber.model";
|
||||
|
||||
export const UnlinkModelsModal: ModalComponent<boolean, MSModelType> = ({ resolver, data }) => {
|
||||
export const UnlinkModelsModal: ModalComponent<boolean, MSModelType> = ({ resolver, options: {data} }) => {
|
||||
const t = useTranslation();
|
||||
const [keepMaps, setKeepMaps] = useState(true);
|
||||
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import { ModalComponent } from "renderer/services/modale.service"
|
||||
import { PlaylistDetailsTemplate, PlaylistDetailsTemplateProps } from "./playlist-details-template.component"
|
||||
import { Observable } from "rxjs"
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { MapItem } from "renderer/components/maps-playlists-panel/maps/map-item.component";
|
||||
import { extractMapDiffs } from "renderer/components/maps-playlists-panel/maps/maps-row.component";
|
||||
|
||||
interface Props extends Omit<PlaylistDetailsTemplateProps, "children"> {
|
||||
version: BSVersion
|
||||
installedMaps$: Observable<BsmLocalMap[]>;
|
||||
}
|
||||
|
||||
export const LocalPlaylistDetailsModal: ModalComponent<void, Props> = ({resolver, options}) => {
|
||||
|
||||
const installedMaps = useObservable(() => options.data.installedMaps$, undefined);
|
||||
|
||||
console.log(installedMaps);
|
||||
|
||||
const renderMaps = () => {
|
||||
if (!installedMaps) {
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<PlaylistDetailsTemplate {...options.data}>
|
||||
{renderMaps()}
|
||||
</PlaylistDetailsTemplate>
|
||||
)
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import { ReactNode } from "react"
|
||||
import { BsmImage } from "renderer/components/shared/bsm-image.component";
|
||||
import dateFormat from "dateformat";
|
||||
import { MapIcon } from "renderer/components/svgs/icons/map-icon.component";
|
||||
import { PersonIcon } from "renderer/components/svgs/icons/person-icon.component";
|
||||
import { ClockIcon } from "renderer/components/svgs/icons/clock-icon.component";
|
||||
import { NpsIcon } from "renderer/components/svgs/icons/nps-icon.component";
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import { ThemeColorGradientSpliter } from "renderer/components/shared/theme-color-gradient-spliter.component";
|
||||
|
||||
export type PlaylistDetailsTemplateProps = {
|
||||
title: string;
|
||||
image: string;
|
||||
author: string;
|
||||
description: string;
|
||||
nbMaps: number;
|
||||
duration: number;
|
||||
nbMappers: number;
|
||||
minNps: number;
|
||||
maxNps: number;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export function PlaylistDetailsTemplate({title, image, author, description, nbMaps, duration, nbMappers, minNps, maxNps, children}: PlaylistDetailsTemplateProps) {
|
||||
|
||||
const color = useThemeColor("first-color");
|
||||
|
||||
const nbMapsText = nbMaps ? Intl.NumberFormat(undefined, { notation: "compact" }).format(nbMaps).trim() : null;
|
||||
const nbMappersText = nbMappers ? Intl.NumberFormat(undefined, { notation: "compact" }).format(nbMappers).trim() : null;
|
||||
const minNpsText = minNps ? Math.round(minNps * 10) / 10 : 0;
|
||||
const maxNpsText = maxNps ? Math.round(maxNps * 10) / 10 : 0;
|
||||
const showNps = minNps !== undefined && maxNps !== undefined;
|
||||
|
||||
const durationText = (() => {
|
||||
if (!duration) {
|
||||
return null;
|
||||
}
|
||||
const date = new Date(0);
|
||||
date.setSeconds(duration);
|
||||
return duration > 3600 ? dateFormat(date, "h:MM:ss") : dateFormat(date, "MM:ss");
|
||||
})();
|
||||
|
||||
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">
|
||||
<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]">
|
||||
<h1 className="font-bold text-xl tracking-wide line-clamp-1 w-fit">{title}</h1>
|
||||
<p>{description}</p>
|
||||
<p className="text-xs font-bold">Créé par <span className="brightness-200" style={{color}}>{author}</span></p>
|
||||
|
||||
<div className="flex flex-row flex-wrap w-full gap-2 mt-1">
|
||||
{ nbMapsText && <div className="flex items-center text-sm h-5 gap-0.5"> <MapIcon className='h-full aspect-square'/> <span className="mb-0.5">{nbMapsText}</span> </div> }
|
||||
{ nbMappersText && <div className="flex items-center text-sm h-5 gap-0.5"> <PersonIcon className='h-full aspect-square'/> <span className="mb-0.5">{nbMappersText}</span> </div> }
|
||||
{ durationText && <div className="flex items-center text-sm h-5 gap-0.5"> <ClockIcon className='h-full aspect-square'/> <span className="mb-0.5">{durationText}</span> </div> }
|
||||
{ showNps && <div className="flex items-center text-sm h-5 gap-0.5"> <NpsIcon className='h-full aspect-square scale-95'/> <span className="mb-0.5">{`${minNpsText} - ${maxNpsText}`}</span> </div> }
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</header>
|
||||
<ThemeColorGradientSpliter className="shrink-0 w-full h-0.5"/>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import { ModalComponent } from "renderer/services/modale.service";
|
||||
import { FolderLinkState, VersionFolderLinkerService, VersionLinkerActionType } from "renderer/services/version-folder-linker.service";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
|
||||
export const ShareFoldersModal: ModalComponent<void, BSVersion> = ({ data }) => {
|
||||
export const ShareFoldersModal: ModalComponent<void, BSVersion> = ({ options: {data} }) => {
|
||||
const SHARED_FOLDERS_KEY = "default-shared-folders";
|
||||
|
||||
const config = useService(ConfigurationService);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { BsmImage } from "renderer/components/shared/bsm-image.component";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
|
||||
export const UninstallAllModsModal: ModalComponent<void, BSVersion> = ({ resolver, data }) => {
|
||||
export const UninstallAllModsModal: ModalComponent<void, BSVersion> = ({ resolver, options: {data} }) => {
|
||||
const version = data;
|
||||
const t = useTranslation();
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { BsmImage } from "renderer/components/shared/bsm-image.component";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { Mod } from "shared/models/mods/mod.interface";
|
||||
|
||||
export const UninstallModModal: ModalComponent<void, Mod> = ({ resolver, data }) => {
|
||||
export const UninstallModModal: ModalComponent<void, Mod> = ({ resolver, options: {data} }) => {
|
||||
const mod = data;
|
||||
const t = useTranslation();
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import { BsmImage } from "renderer/components/shared/bsm-image.component";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
|
||||
export const UninstallModal: ModalComponent<void, BSVersion> = ({ resolver, data }) => {
|
||||
export const UninstallModal: ModalComponent<void, BSVersion> = ({ resolver, options: {data} }) => {
|
||||
const version = data;
|
||||
const t = useTranslation();
|
||||
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { ModalExitCode, ModalService } from "renderer/services/modale.service";
|
||||
import { ModalExitCode, ModalObject, ModalService } from "renderer/services/modale.service";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import { useEffect } from "react";
|
||||
import { BsmIcon } from "../svgs/bsm-icon.component";
|
||||
import { ThemeColorGradientSpliter } from "../shared/theme-color-gradient-spliter.component";
|
||||
|
||||
export function Modal() {
|
||||
const modalSevice = ModalService.getInstance();
|
||||
|
||||
const ModalComponent = useObservable(() => modalSevice.getModalToShow());
|
||||
|
||||
const modalData = modalSevice.getModalData();
|
||||
const resolver = modalSevice.getResolver();
|
||||
const modals = useObservable(() => modalSevice.getModalToShow());
|
||||
const currentModal = modals?.at(-1);
|
||||
|
||||
const { firstColor, secondColor } = useThemeColor();
|
||||
|
||||
@@ -20,10 +19,10 @@ export function Modal() {
|
||||
if (e.key !== "Escape") {
|
||||
return;
|
||||
}
|
||||
resolver?.(ModalExitCode.NO_CHOICE);
|
||||
currentModal.resolver({ exitCode: ModalExitCode.CLOSED });
|
||||
};
|
||||
|
||||
if (ModalComponent) {
|
||||
if (currentModal) {
|
||||
window.addEventListener("keyup", onEscape);
|
||||
} else {
|
||||
window.removeEventListener("keyup", onEscape);
|
||||
@@ -32,32 +31,40 @@ export function Modal() {
|
||||
return () => {
|
||||
window.removeEventListener("keyup", onEscape);
|
||||
};
|
||||
}, [ModalComponent]);
|
||||
}, [currentModal]);
|
||||
|
||||
const renderModal = (modal: ModalObject) => {
|
||||
if (!modal?.modal) { return null; }
|
||||
|
||||
if(modal.options?.noStyle){
|
||||
return <modal.modal resolver={modal.resolver} options={modal.options} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative p-4 text-gray-800 dark:text-gray-200 rounded-md shadow-lg shadow-black bg-gradient-to-br from-light-main-color-3 to-light-main-color-2 dark:from-main-color-3 dark:to-main-color-2">
|
||||
<ThemeColorGradientSpliter className="absolute top-0 w-full left-0 h-1 rounded-t-md overflow-hidden"/>
|
||||
<div
|
||||
className="w-2.5 h-2.5 absolute top-2.5 right-1.5 cursor-pointer"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
modal.resolver({ exitCode: ModalExitCode.CLOSED });
|
||||
}}
|
||||
>
|
||||
<BsmIcon className="w-full h-full" icon="cross" />
|
||||
</div>
|
||||
<modal.modal resolver={modal.resolver} options={modal.options} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{ModalComponent && resolver && (
|
||||
<div className="top-0 absolute w-screen h-screen flex content-center items-center justify-center z-[90]">
|
||||
<motion.span key="modal-overlay" onClick={() => modalSevice.resolve({ exitCode: ModalExitCode.NO_CHOICE })} className="absolute top-0 bottom-0 right-0 left-0 bg-black" initial={{ opacity: 0 }} animate={{ opacity: ModalComponent && 0.6 }} exit={{ opacity: 0 }} transition={{ duration: 0.2 }} />
|
||||
<motion.div key="modal" initial={{ y: "100vh" }} animate={{ y: 0 }} exit={{ y: "100vh" }}>
|
||||
<div className="relative p-4 text-gray-800 dark:text-gray-200 rounded-md shadow-lg shadow-black bg-gradient-to-br from-light-main-color-3 to-light-main-color-2 dark:from-main-color-3 dark:to-main-color-2">
|
||||
<div className="absolute top-0 w-full left-0 h-1 rounded-t-md overflow-hidden">
|
||||
<span className="block w-full h-full" style={{ backgroundImage: `linear-gradient(to right, ${firstColor}, ${secondColor})` }} />
|
||||
</div>
|
||||
<div
|
||||
className="w-2.5 h-2.5 absolute top-2.5 right-1.5 cursor-pointer"
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
resolver(ModalExitCode.CLOSED);
|
||||
}}
|
||||
>
|
||||
<BsmIcon className="w-full h-full" icon="cross" />
|
||||
</div>
|
||||
<ModalComponent resolver={resolver} data={modalData} />
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
{currentModal && <motion.span key="modal-overlay" onClick={() => currentModal.resolver({ exitCode: ModalExitCode.NO_CHOICE })} className="fixed size-full bg-black z-[90]" initial={{ opacity: 0 }} animate={{ opacity: currentModal && 0.6 }} exit={{ opacity: 0 }} transition={{ duration: 0.2 }} />}
|
||||
{modals?.map((modal, index) => (
|
||||
<motion.div className="fixed z-[90] left-1/2 top-1/2" key={index} initial={{ y: "100vh", x: "-50%" }} animate={{y: "-50%", scale: modal === currentModal ? 1 : 0, opacity: modal === currentModal ? 1 : 0}} exit={{ y: "100vh" }}>
|
||||
{renderModal(modal)}
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
|
||||
+4
-4
@@ -18,11 +18,11 @@ export function BsContentTabPanel({className, tabIndex, tabs, onTabChange, child
|
||||
<div className={className ?? "w-full h-full flex flex-row bg-light-main-color-1 dark:bg-main-color-1 rounded-md shadow-black shadow-md overflow-hidden"}>
|
||||
<nav className="h-full grid grid-flow-row !rounded-none shadow-sm flex-shrink-0">
|
||||
{tabs.map((tab, i) => (
|
||||
<BsContentTabItem
|
||||
key={`${tab.text}${tab.icon}`}
|
||||
<BsContentTabItem
|
||||
key={`${tab.text}${tab.icon}`}
|
||||
{...tab}
|
||||
active={tab.active ?? tabIndex === i}
|
||||
onClick={value => {onTabChange(i, tab); tab.onClick(value);}}
|
||||
active={tab.active ?? tabIndex === i}
|
||||
onClick={value => {onTabChange(i, tab); tab.onClick(value);}}
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ComponentProps } from "react";
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
|
||||
export function ThemeColorGradientSpliter(props: ComponentProps<"div">) {
|
||||
|
||||
const colors = useThemeColor();
|
||||
|
||||
return (
|
||||
<div {...props} style={{ backgroundImage: `linear-gradient(to right, ${colors.firstColor}, ${colors.secondColor})`}}/>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { CSSProperties } from "react";
|
||||
import { createSvgIcon } from "../svg-icon.type";
|
||||
|
||||
export function SearchIcon(props: { className?: string; style?: CSSProperties }) {
|
||||
export const SearchIcon = 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">
|
||||
<svg ref={ref} {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" height="40" width="40">
|
||||
<path fill="currentColor" d="m31.833 34.167-9.708-9.75q-1.25 1.041-2.896 1.625-1.646.583-3.521.583-4.583 0-7.77-3.208-3.188-3.209-3.188-7.709 0-4.541 3.188-7.729 3.187-3.187 7.729-3.187 4.541 0 7.729 3.187 3.187 3.188 3.187 7.729 0 1.792-.583 3.438-.583 1.646-1.625 3.021l9.792 9.75q.458.458.458 1.104 0 .646-.5 1.146-.458.458-1.146.458-.687 0-1.146-.458ZM15.667 23.458q3.25 0 5.5-2.27 2.25-2.271 2.25-5.48 0-3.25-2.25-5.5t-5.5-2.25q-3.209 0-5.479 2.25-2.271 2.25-2.271 5.5 0 3.209 2.25 5.48 2.25 2.27 5.5 2.27Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -46,7 +46,7 @@ export function VersionViewer() {
|
||||
const verifyFiles = () => bsDownloader.verifyBsVersion(state);
|
||||
|
||||
const uninstall = async () => {
|
||||
const modalCompleted = await modalService.openModal(UninstallModal, state);
|
||||
const modalCompleted = await modalService.openModal(UninstallModal, { data: state });
|
||||
if (modalCompleted.exitCode === ModalExitCode.COMPLETED) {
|
||||
bsUninstallerService.uninstall(state).then(() => {
|
||||
bsVersionManagerService.askInstalledVersions().then(versions => {
|
||||
@@ -79,11 +79,11 @@ export function VersionViewer() {
|
||||
};
|
||||
|
||||
const openShareFolderModal = () => {
|
||||
modalService.openModal(ShareFoldersModal, state);
|
||||
modalService.openModal(ShareFoldersModal, {data: state});
|
||||
};
|
||||
|
||||
const createLaunchShortcut = async () => {
|
||||
const { exitCode, data } = await modalService.openModal(CreateLaunchShortcutModal, state);
|
||||
const { exitCode, data } = await modalService.openModal(CreateLaunchShortcutModal, {data: state});
|
||||
if(exitCode !== ModalExitCode.COMPLETED){ return; }
|
||||
|
||||
lastValueFrom(bsLauncher.createLaunchShortcut(data)).then(() => {
|
||||
|
||||
@@ -109,6 +109,6 @@ export class AutoUpdaterService {
|
||||
public async showChangelog(version:string): Promise<void>{
|
||||
const changelog = await this.getChangelogVersion(version);
|
||||
|
||||
this.modal.openModal(ChangelogModal, changelog);
|
||||
this.modal.openModal(ChangelogModal, {data: changelog});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ export class BsModsManagerService {
|
||||
return;
|
||||
}
|
||||
|
||||
const modalRes = await this.modals.openModal(UninstallModModal, mod);
|
||||
const modalRes = await this.modals.openModal(UninstallModModal, {data: mod});
|
||||
|
||||
if (modalRes.exitCode !== ModalExitCode.COMPLETED) {
|
||||
return;
|
||||
@@ -117,7 +117,7 @@ export class BsModsManagerService {
|
||||
return;
|
||||
}
|
||||
|
||||
const modalRes = await this.modals.openModal(UninstallAllModsModal, version);
|
||||
const modalRes = await this.modals.openModal(UninstallAllModsModal, {data: version});
|
||||
|
||||
if (modalRes.exitCode !== ModalExitCode.COMPLETED) {
|
||||
return;
|
||||
|
||||
@@ -15,7 +15,7 @@ import { DownloaderServiceInterface } from "./bs-store-downloader.interface";
|
||||
import { AbstractBsDownloaderService } from "./abstract-bs-downloader.service";
|
||||
|
||||
export class SteamDownloaderService extends AbstractBsDownloaderService implements DownloaderServiceInterface{
|
||||
|
||||
|
||||
private static instance: SteamDownloaderService;
|
||||
|
||||
public static getInstance(): SteamDownloaderService {
|
||||
@@ -102,7 +102,7 @@ export class SteamDownloaderService extends AbstractBsDownloaderService implemen
|
||||
take(1),
|
||||
).subscribe(async () => {
|
||||
const logged$ = events$.pipe(filter(event => event.subType === DepotDownloaderInfoEvent.SteamID), take(1));
|
||||
const res = await this.modalService.openModal(SteamMobileApproveModal, { logged$ });
|
||||
const res = await this.modalService.openModal(SteamMobileApproveModal, {data: { logged$ }});
|
||||
if(res.exitCode !== ModalExitCode.COMPLETED){
|
||||
return this.stopDownload();
|
||||
}
|
||||
@@ -128,7 +128,7 @@ export class SteamDownloaderService extends AbstractBsDownloaderService implemen
|
||||
return this.notificationService.notifySuccess({title: "notifications.bs-download.success.titles.verification-finished"});
|
||||
}
|
||||
return this.notificationService.notifySuccess({title: "notifications.bs-download.success.titles.download-success"});
|
||||
}));
|
||||
}));
|
||||
|
||||
return subs;
|
||||
}
|
||||
@@ -182,7 +182,7 @@ export class SteamDownloaderService extends AbstractBsDownloaderService implemen
|
||||
}),
|
||||
share({connector: () => new ReplaySubject(1)})
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
|
||||
private tryAutoDownloadBsVersion(downloadInfo: DownloadSteamInfo){
|
||||
@@ -217,7 +217,7 @@ export class SteamDownloaderService extends AbstractBsDownloaderService implemen
|
||||
}
|
||||
|
||||
this.progressBarService.show(this.downloadProgress$, true);
|
||||
|
||||
|
||||
const downloadPromise = (async () => {
|
||||
|
||||
const haveDotNet = await this.isDotNet6Installed().catch(() => false);
|
||||
@@ -227,16 +227,16 @@ export class SteamDownloaderService extends AbstractBsDownloaderService implemen
|
||||
}
|
||||
|
||||
const downloadInfo: DownloadSteamInfo = {bsVersion, isVerification}
|
||||
|
||||
|
||||
const autoDownload = await lastValueFrom(this.tryAutoDownloadBsVersion(downloadInfo)).then(() => true).catch(() => false);
|
||||
|
||||
|
||||
if(autoDownload){ return Promise.resolve(); }
|
||||
|
||||
const qrCodeDownload$ = this.startQrCodeDownload(downloadInfo);
|
||||
const qrCode$ = qrCodeDownload$.pipe(filter(event => event.type === DepotDownloaderEventType.Info && event.subType === DepotDownloaderInfoEvent.QRCode), map(event => event.data as string));
|
||||
const logged$ = qrCodeDownload$.pipe(filter(event => event.type === DepotDownloaderEventType.Info && event.subType === DepotDownloaderInfoEvent.SteamID), map(event => event.data as string), take(1));
|
||||
|
||||
const loginRes = await this.modalService.openModal(LoginToSteamModal, { qrCode$, logged$ });
|
||||
const loginRes = await this.modalService.openModal(LoginToSteamModal, {data: { qrCode$, logged$ }});
|
||||
|
||||
if(loginRes.exitCode !== ModalExitCode.COMPLETED){
|
||||
return Promise.resolve();
|
||||
|
||||
@@ -66,7 +66,7 @@ export class BSVersionManagerService {
|
||||
}
|
||||
|
||||
public async editVersion(version: BSVersion): Promise<BSVersion> {
|
||||
const modalRes = await this.modalService.openModal(EditVersionModal, { version, clone: false });
|
||||
const modalRes = await this.modalService.openModal(EditVersionModal, {data: { version, clone: false }});
|
||||
if (modalRes.exitCode !== ModalExitCode.COMPLETED) {
|
||||
return null;
|
||||
}
|
||||
@@ -90,7 +90,7 @@ export class BSVersionManagerService {
|
||||
if (!this.progressBar.require()) {
|
||||
return null;
|
||||
}
|
||||
const modalRes = await this.modalService.openModal(EditVersionModal, { version, clone: true });
|
||||
const modalRes = await this.modalService.openModal(EditVersionModal, {data: { version, clone: true }});
|
||||
if (modalRes.exitCode !== ModalExitCode.COMPLETED) {
|
||||
return null;
|
||||
}
|
||||
@@ -128,7 +128,7 @@ export class BSVersionManagerService {
|
||||
if(resModal.exitCode !== ModalExitCode.COMPLETED){
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const store = resModal.data;
|
||||
|
||||
const folderRes = await lastValueFrom(this.ipcService.sendV2<{ canceled: boolean; filePaths: string[] }>("choose-folder"));
|
||||
|
||||
@@ -78,7 +78,7 @@ export class MapsDownloaderService {
|
||||
}
|
||||
|
||||
public async openDownloadMapModal(version?: BSVersion, ownedMaps: BsmLocalMap[] = []): Promise<ModalResponse<void>> {
|
||||
const res = await this.modals.openModal(DownloadMapsModal, { version, ownedMaps });
|
||||
const res = await this.modals.openModal(DownloadMapsModal, {data: { version, ownedMaps }});
|
||||
this.progressBar.setStyle(null);
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ export class MapsManagerService {
|
||||
const askModal = maps.length > 1 || !this.config.get<boolean>(MapsManagerService.REMEMBER_CHOICE_DELETE_MAP_KEY);
|
||||
|
||||
if (askModal) {
|
||||
const modalRes = await this.modal.openModal(DeleteMapsModal, { linked: versionLinked, maps });
|
||||
const modalRes = await this.modal.openModal(DeleteMapsModal, {data: { linked: versionLinked, maps }});
|
||||
if (modalRes.exitCode !== ModalExitCode.COMPLETED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { Observable, BehaviorSubject } from "rxjs";
|
||||
import { timeout } from "rxjs/operators";
|
||||
|
||||
export class ModalService {
|
||||
private static instance: ModalService;
|
||||
|
||||
private _modalToShow$: BehaviorSubject<ModalComponent> = new BehaviorSubject(null);
|
||||
private _modalToShow$: BehaviorSubject<ModalObject[]> = new BehaviorSubject([]);
|
||||
private modalData: unknown = null;
|
||||
private resolver: (value: ModalResponse | PromiseLike<ModalResponse>) => void = null;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
@@ -17,47 +15,33 @@ export class ModalService {
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
private close() {
|
||||
if (this.resolver) {
|
||||
this.resolve({ exitCode: ModalExitCode.NO_CHOICE });
|
||||
}
|
||||
this._modalToShow$.next(null);
|
||||
}
|
||||
|
||||
public getModalData<Type>(): Type {
|
||||
return this.modalData as Type;
|
||||
}
|
||||
|
||||
public getResolver(): any {
|
||||
return this.resolver;
|
||||
}
|
||||
|
||||
public resolve(data: ModalResponse<unknown>): void {
|
||||
this.resolver(data);
|
||||
}
|
||||
|
||||
public async openModal<T, K>(modal: ModalComponent<T, K>, data?: K): Promise<ModalResponse<T>> {
|
||||
this.close();
|
||||
await timeout(100); // Must wait resolve
|
||||
public async openModal<T, K>(modal: ModalComponent<T, K>, options?: ModalOptions<K>): Promise<ModalResponse<T>> {
|
||||
let resolver: (value: ModalResponse | PromiseLike<ModalResponse>) => void;
|
||||
const promise = new Promise<ModalResponse<T>>(resolve => {
|
||||
this.resolver = resolve as (value: ModalResponse | PromiseLike<ModalResponse>) => void;
|
||||
resolver = resolve as (value: ModalResponse | PromiseLike<ModalResponse>) => void;
|
||||
});
|
||||
this._modalToShow$.next(modal as ModalComponent);
|
||||
promise.then(() => this.close());
|
||||
if (data) {
|
||||
this.modalData = data;
|
||||
} else {
|
||||
this.modalData = null;
|
||||
}
|
||||
const modalObj = {modal: modal as ModalComponent, resolver: resolver, options};
|
||||
this._modalToShow$.next([...this._modalToShow$.getValue(), modalObj]);
|
||||
|
||||
promise.then(() => {
|
||||
this._modalToShow$.next(this._modalToShow$.getValue().filter(m => m !== modalObj));
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
public getModalToShow(): Observable<ModalComponent> {
|
||||
public getModalToShow(): Observable<ModalObject[]> {
|
||||
return this._modalToShow$.asObservable();
|
||||
}
|
||||
}
|
||||
|
||||
export type ModalComponent<Return = unknown, Receive = unknown> = ({ resolver, data }: { resolver: (x: ModalResponse<Return>) => void; data?: Receive }) => JSX.Element;
|
||||
export type ModalOptions<T = unknown> = { data?: T, noStyle?: boolean }
|
||||
export type ModalComponent<Return = unknown, Receive = unknown> = ({ resolver, options }: { resolver: (x: ModalResponse<Return>) => void; options?: ModalOptions<Receive> }) => JSX.Element;
|
||||
export type ModalObject = {modal: ModalComponent, resolver: (value: ModalResponse | PromiseLike<ModalResponse>) => void, options: ModalOptions};
|
||||
|
||||
export const enum ModalExitCode {
|
||||
NO_CHOICE = -1,
|
||||
|
||||
@@ -118,7 +118,7 @@ export class ModelsDownloaderService {
|
||||
}
|
||||
|
||||
public openDownloadModelsModal(version: BSVersion, type?: MSModelType, owned?: BsmLocalModel[]): Promise<ModalResponse<void>> {
|
||||
return this.modal.openModal(DownloadModelsModal, { version, type, owned });
|
||||
return this.modal.openModal(DownloadModelsModal, {data: { version, type, owned }});
|
||||
}
|
||||
|
||||
public async oneClickInstallModel(model: MSModel): Promise<boolean> {
|
||||
|
||||
@@ -73,7 +73,7 @@ export class ModelsManagerService {
|
||||
}
|
||||
|
||||
public async linkModels(type: MSModelType, version?: BSVersion): Promise<boolean> {
|
||||
const res = await this.modalService.openModal(LinkModelsModal, type);
|
||||
const res = await this.modalService.openModal(LinkModelsModal, {data: type});
|
||||
|
||||
if (res.exitCode !== ModalExitCode.COMPLETED) {
|
||||
return null;
|
||||
@@ -82,13 +82,12 @@ export class ModelsManagerService {
|
||||
return this.versionFolderLinked.linkVersionFolder({
|
||||
version,
|
||||
relativeFolder: MODEL_TYPE_FOLDERS[type],
|
||||
type: VersionLinkerActionType.Link,
|
||||
options: { keepContents: res.data !== false },
|
||||
});
|
||||
}
|
||||
|
||||
public async unlinkModels(type: MSModelType, version?: BSVersion): Promise<boolean> {
|
||||
const res = await this.modalService.openModal(UnlinkModelsModal, type);
|
||||
const res = await this.modalService.openModal(UnlinkModelsModal, {data: type});
|
||||
|
||||
if (res.exitCode !== ModalExitCode.COMPLETED) {
|
||||
return null;
|
||||
@@ -97,7 +96,6 @@ export class ModelsManagerService {
|
||||
return this.versionFolderLinked.unlinkVersionFolder({
|
||||
version,
|
||||
relativeFolder: MODEL_TYPE_FOLDERS[type],
|
||||
type: VersionLinkerActionType.Unlink,
|
||||
options: { keepContents: res.data !== false },
|
||||
});
|
||||
}
|
||||
@@ -165,7 +163,7 @@ export class ModelsManagerService {
|
||||
return false;
|
||||
})();
|
||||
|
||||
const res = await this.modalService.openModal(DeleteModelsModal, { models, linked });
|
||||
const res = await this.modalService.openModal(DeleteModelsModal, {data: { models, linked }});
|
||||
if (res.exitCode !== ModalExitCode.COMPLETED) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
@@ -3,9 +3,8 @@ import { IpcService } from "./ipc.service";
|
||||
import { Observable, lastValueFrom } from "rxjs";
|
||||
import { FolderLinkState, VersionFolderLinkerService } from "./version-folder-linker.service";
|
||||
import { Progression } from "main/helpers/fs.helpers";
|
||||
import { LocalBPList } from "shared/models/playlists/local-playlist.models";
|
||||
import { LocalBPListsDetails } from "shared/models/playlists/local-playlist.models";
|
||||
import { ModalExitCode, ModalService } from "./modale.service";
|
||||
import { LinkMapsModal } from "renderer/components/modal/modal-types/link-maps-modal.component";
|
||||
import { UnlinkPlaylistModal } from "renderer/components/modal/modal-types/unlink-playlist-modal.component";
|
||||
import { LinkPlaylistModal } from "renderer/components/modal/modal-types/link-playlist-modal.component";
|
||||
|
||||
@@ -31,8 +30,8 @@ export class PlaylistsManagerService {
|
||||
this.modal = ModalService.getInstance();
|
||||
}
|
||||
|
||||
public getVersionPlaylists(version: BSVersion): Observable<Progression<LocalBPList[]>> {
|
||||
return this.ipc.sendV2("get-version-playlists", { args: version });
|
||||
public getVersionPlaylistsDetails(version: BSVersion): Observable<Progression<LocalBPListsDetails[]>> {
|
||||
return this.ipc.sendV2("get-version-playlists-details", { args: version });
|
||||
}
|
||||
|
||||
public async linkVersion(version: BSVersion): Promise<boolean> {
|
||||
|
||||
@@ -24,6 +24,8 @@ import { OsDiagnosticService } from "renderer/services/os-diagnostic.service";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { AutoUpdaterService } from "renderer/services/auto-updater.service";
|
||||
import { gt } from "semver"
|
||||
import { ModalService } from "renderer/services/modale.service";
|
||||
import { ChooseStore } from "renderer/components/modal/modal-types/bs-downgrade/choose-store-modal.component";
|
||||
|
||||
export default function App() {
|
||||
useService(OsDiagnosticService);
|
||||
@@ -34,6 +36,7 @@ export default function App() {
|
||||
const notification = useService(NotificationService);
|
||||
const config = useService(ConfigurationService);
|
||||
const autoUpdater = useService(AutoUpdaterService);
|
||||
const modals = useService(ModalService);
|
||||
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
@@ -55,6 +58,7 @@ export default function App() {
|
||||
};
|
||||
|
||||
const checkOneClicks = async () => {
|
||||
|
||||
if (config.get("not-remind-oneclick") === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { SongDetailDiffCharactertistic, SongDiffName } from "./song-details-cache.model";
|
||||
|
||||
export interface BsvMapDetail {
|
||||
automapper: boolean;
|
||||
createdAt: string;
|
||||
@@ -115,10 +117,10 @@ export interface BsvMapTestplay {
|
||||
|
||||
export interface BsvMapDifficulty {
|
||||
bombs: number;
|
||||
characteristic: BsvMapCharacteristic;
|
||||
characteristic: SongDetailDiffCharactertistic;
|
||||
chroma: boolean;
|
||||
cinema: boolean;
|
||||
difficulty: BsvMapDifficultyType;
|
||||
difficulty: SongDiffName;
|
||||
events: number;
|
||||
length: number;
|
||||
maxScore: number;
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { SongDetails } from "../maps";
|
||||
import { BPList, PlaylistSong } from "./playlist.interface";
|
||||
import { BPList } from "./playlist.interface";
|
||||
|
||||
export interface LocalBPList extends BPList<LocalBpListSong> {
|
||||
export interface LocalBPList extends BPList {
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface LocalBpListSong {
|
||||
song: PlaylistSong;
|
||||
songDetails?: SongDetails;
|
||||
songUrl?: string;
|
||||
coverUrl?: string;
|
||||
export interface LocalBPListsDetails extends LocalBPList {
|
||||
nbMaps: number;
|
||||
id?: number;
|
||||
nbMappers?: number;
|
||||
duration?: number;
|
||||
minNps?: number;
|
||||
maxNps?: number;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ export interface BPList<SongType = PlaylistSong> {
|
||||
playlistAuthor: string;
|
||||
playlistDescription?: string;
|
||||
image: string;
|
||||
customData: unknown;
|
||||
customData?: CustomDataBPList;
|
||||
songs: SongType[];
|
||||
}
|
||||
|
||||
@@ -24,3 +24,7 @@ export interface DownloadPlaylistProgressionData {
|
||||
playlistInfos: BPList;
|
||||
playlistPath: string;
|
||||
}
|
||||
|
||||
export interface CustomDataBPList {
|
||||
syncURL?: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user