mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
[feature-107] multiple lists now uses virtual scroll
This commit is contained in:
+71
-68
@@ -39,6 +39,7 @@ export const LocalMapsListPanel = forwardRef<unknown, Props>(({ version, classNa
|
||||
|
||||
const {maps$, setMaps} = useContext(InstalledMapsContext);
|
||||
const maps = useObservable(() => maps$, undefined);
|
||||
const [renderableMaps, setRenderableMaps] = useState<RenderableMap[]>([]);
|
||||
const [subs] = useState<Subscription[]>([]);
|
||||
const [selectedMaps, setSelectedMaps] = useState<BsmLocalMap[]>([]);
|
||||
const isActiveOnce = useChangeUntilEqual(isActive, { untilEqual: true });
|
||||
@@ -46,25 +47,25 @@ export const LocalMapsListPanel = forwardRef<unknown, Props>(({ version, classNa
|
||||
|
||||
const loadPercent$ = useConstant(() => new BehaviorSubject(0));
|
||||
|
||||
useImperativeHandle(
|
||||
forwardRef,
|
||||
() => ({
|
||||
deleteMaps() {
|
||||
const mapsToDelete = selectedMaps.length === 0 ? maps : selectedMaps;
|
||||
mapsManager.deleteMaps(mapsToDelete, version).then(res => {
|
||||
if (!res) {
|
||||
return;
|
||||
}
|
||||
removeMapsFromList(mapsToDelete);
|
||||
setSelectedMaps([]);
|
||||
});
|
||||
},
|
||||
exportMaps() {
|
||||
mapsManager.exportMaps(version, selectedMaps);
|
||||
}
|
||||
}),
|
||||
[selectedMaps, maps, version]
|
||||
);
|
||||
useImperativeHandle(forwardRef, () => ({
|
||||
deleteMaps() {
|
||||
const mapsToDelete = selectedMaps.length === 0 ? maps : selectedMaps;
|
||||
mapsManager.deleteMaps(mapsToDelete, version).then(res => {
|
||||
if (!res) {
|
||||
return;
|
||||
}
|
||||
removeMapsFromList(mapsToDelete);
|
||||
setSelectedMaps([]);
|
||||
});
|
||||
},
|
||||
exportMaps() {
|
||||
mapsManager.exportMaps(version, selectedMaps);
|
||||
}
|
||||
}),[selectedMaps, maps, version]);
|
||||
|
||||
useOnUpdate(() => {
|
||||
setRenderableMaps(() => maps?.map(map => ({ map, selected: selectedMaps.some(selectedMap => selectedMap.hash === map.hash) }) ));
|
||||
}, [maps, selectedMaps]);
|
||||
|
||||
useOnUpdate(() => {
|
||||
if(linkedState === FolderLinkState.Pending || linkedState === FolderLinkState.Processing) return () => {};
|
||||
@@ -120,27 +121,24 @@ export const LocalMapsListPanel = forwardRef<unknown, Props>(({ version, classNa
|
||||
};
|
||||
|
||||
const removeMapsFromList = (mapsToRemove: BsmLocalMap[]) => {
|
||||
const filtredMaps = maps.filter(map => !mapsToRemove.some(toDeleteMaps => map.hash === toDeleteMaps.hash));
|
||||
const filtredMaps = maps$.value.filter(map => !mapsToRemove.some(toDeleteMaps => map.hash === toDeleteMaps.hash));
|
||||
setMaps(filtredMaps);
|
||||
|
||||
const filtredSelectedMaps = selectedMaps.filter(map => !mapsToRemove.some(toDeleteMaps => map.hash === toDeleteMaps.hash));
|
||||
setSelectedMaps(filtredSelectedMaps);
|
||||
setSelectedMaps(selectedMaps => selectedMaps.filter(map => !mapsToRemove.some(toDeleteMaps => map.hash === toDeleteMaps.hash)));
|
||||
};
|
||||
|
||||
const handleDelete = useCallback(
|
||||
(map: BsmLocalMap) => {
|
||||
mapsManager.deleteMaps([map], version).then(res => {
|
||||
if (!res) {
|
||||
return;
|
||||
}
|
||||
removeMapsFromList([map]);
|
||||
});
|
||||
},
|
||||
[version, maps]
|
||||
);
|
||||
const handleDelete = (map: BsmLocalMap) => {
|
||||
mapsManager.deleteMaps([map], version).then(res => {
|
||||
if (!res) {
|
||||
return;
|
||||
}
|
||||
removeMapsFromList([map]);
|
||||
});
|
||||
}
|
||||
|
||||
const onMapSelected = useCallback(
|
||||
(map: BsmLocalMap) => {
|
||||
const onMapSelected = (map: BsmLocalMap) => {
|
||||
|
||||
setSelectedMaps(selectedMaps => {
|
||||
const mapsCopy = [...selectedMaps];
|
||||
if (mapsCopy.some(selectedMap => selectedMap.hash === map.hash)) {
|
||||
const i = mapsCopy.findIndex(selectedMap => selectedMap.hash === map.hash);
|
||||
@@ -148,11 +146,36 @@ export const LocalMapsListPanel = forwardRef<unknown, Props>(({ version, classNa
|
||||
} else {
|
||||
mapsCopy.push(map);
|
||||
}
|
||||
return mapsCopy;
|
||||
});
|
||||
}
|
||||
|
||||
setSelectedMaps(mapsCopy)
|
||||
},
|
||||
[selectedMaps]
|
||||
);
|
||||
const renderMap = useCallback((renderableMap: RenderableMap) => {
|
||||
const map = renderableMap.map;
|
||||
return (
|
||||
<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?.duration}
|
||||
selected={renderableMap.selected}
|
||||
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}
|
||||
onDelete={handleDelete}
|
||||
onSelected={onMapSelected}
|
||||
callBackParam={map}
|
||||
/>
|
||||
);
|
||||
}, [version])
|
||||
|
||||
const isMapFitFilter = (map: BsmLocalMap): boolean => {
|
||||
// Can be more clean and optimized i think
|
||||
@@ -322,12 +345,8 @@ export const LocalMapsListPanel = forwardRef<unknown, Props>(({ version, classNa
|
||||
return true;
|
||||
};
|
||||
|
||||
const preppedMaps: BsmLocalMap[] = (() => {
|
||||
if (!maps) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return maps.filter(isMapFitFilter);
|
||||
const preppedMaps: RenderableMap[] = (() => {
|
||||
return renderableMaps?.filter(renderableMap => isMapFitFilter(renderableMap.map)) ?? [];
|
||||
})();
|
||||
|
||||
if (!maps) {
|
||||
@@ -366,28 +385,12 @@ export const LocalMapsListPanel = forwardRef<unknown, Props>(({ version, classNa
|
||||
maxColumns={3}
|
||||
minItemWidth={400}
|
||||
items={preppedMaps}
|
||||
renderItem={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?.duration}
|
||||
selected={selectedMaps.some(selectedMap => selectedMap.hash === map.hash)}
|
||||
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}
|
||||
onDelete={handleDelete}
|
||||
onSelected={onMapSelected}
|
||||
callBackParam={map}
|
||||
/>
|
||||
)}/>
|
||||
rowKey={rowMaps => rowMaps.map(map => map.map.hash).join("")}
|
||||
renderItem={renderMap}/>
|
||||
);
|
||||
});
|
||||
|
||||
type RenderableMap = {
|
||||
map: BsmLocalMap;
|
||||
selected: boolean;
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ import Tippy from "@tippyjs/react";
|
||||
import { BsvMapDetail, RawMapInfoData, SongDetailDiffCharactertistic, SongDetails, SongDiffName } from "shared/models/maps";
|
||||
import { useConstant } from "renderer/hooks/use-constant.hook";
|
||||
import { CalendarDateTime, getLocalTimeZone } from "@internationalized/date";
|
||||
import { typedMemo } from "renderer/helpers/typed-memo";
|
||||
|
||||
export type ParsedMapDiff = { type: SongDiffName; name: string; stars: number };
|
||||
|
||||
@@ -52,7 +53,7 @@ export type MapItemProps<T = unknown> = {
|
||||
onDoubleClick?: (param: T) => void;
|
||||
};
|
||||
|
||||
export const MapItem = memo(({ hash, title, autor, songAutor, coverUrl, songUrl, autorId, mapId, diffs, ranked, bpm, duration, likes, createdAt, selected, downloading, showOwned, callBackParam, onDelete, onDownload, onSelected, onCancelDownload, onDoubleClick }: MapItemProps) => {
|
||||
export function MapItemComponent <T = unknown>({ hash, title, autor, songAutor, coverUrl, songUrl, autorId, mapId, diffs, ranked, bpm, duration, likes, createdAt, selected, downloading, showOwned, callBackParam, onDelete, onDownload, onSelected, onCancelDownload, onDoubleClick }: MapItemProps<T>) {
|
||||
const linkOpener = useService(LinkOpenerService);
|
||||
const audioPlayer = useService(AudioPlayerService);
|
||||
|
||||
@@ -337,7 +338,9 @@ export const MapItem = memo(({ hash, title, autor, songAutor, coverUrl, songUrl,
|
||||
</div>
|
||||
</motion.li>
|
||||
);
|
||||
}, equal);
|
||||
};
|
||||
|
||||
export const MapItem = typedMemo(MapItemComponent, equal)
|
||||
|
||||
export function extractMapDiffs({rawMapInfo, songDetails, bsvMap}: {rawMapInfo?: RawMapInfoData, songDetails?: SongDetails, bsvMap?: BsvMapDetail}): Map<SongDetailDiffCharactertistic, ParsedMapDiff[]> {
|
||||
const res = new Map<SongDetailDiffCharactertistic, ParsedMapDiff[]>();
|
||||
|
||||
+33
-22
@@ -1,4 +1,4 @@
|
||||
import { forwardRef, useContext, useState } from "react";
|
||||
import { forwardRef, useCallback, 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";
|
||||
@@ -24,6 +24,7 @@ import { NotificationService } from "renderer/services/notification.service";
|
||||
import { DeletePlaylistModal } from "renderer/components/modal/modal-types/playlist/delete-playlist-modal.component";
|
||||
import { OsDiagnosticService } from "renderer/services/os-diagnostic.service";
|
||||
import { PlaylistItemComponentPropsMapper } from "shared/mappers/playlist/playlist-item-component-props.mapper";
|
||||
import { VirtualScroll } from "renderer/components/shared/virtual-scroll/virtual-scroll.component";
|
||||
|
||||
type Props = {
|
||||
version: BSVersion;
|
||||
@@ -79,9 +80,7 @@ export const LocalPlaylistsListPanel = forwardRef<unknown, Props>(({ version, cl
|
||||
});
|
||||
|
||||
const onPlaylistDownloadedCB = (downloaded: LocalBPListsDetails) => {
|
||||
console.log("onPlaylistDownloadedCB", downloaded);
|
||||
const newPlaylist = (() => {
|
||||
console.log(playlists);
|
||||
const index = playlists$.value.findIndex(p => p.path === downloaded.path);
|
||||
if(index === -1){
|
||||
return [...playlists$.value, downloaded];
|
||||
@@ -120,18 +119,17 @@ export const LocalPlaylistsListPanel = forwardRef<unknown, Props>(({ version, cl
|
||||
};
|
||||
|
||||
const deletePlaylist = async (bpList: LocalBPList) => {
|
||||
// !! Need to call the modal to confirm the deletion and to ask if the maps should be deleted too
|
||||
const { exitCode, data: deleteMaps } = await modals.openModal(DeletePlaylistModal, { data: bpList });
|
||||
|
||||
if(exitCode !== ModalExitCode.COMPLETED){ return; }
|
||||
|
||||
lastValueFrom(playlistService.deletePlaylist({ version, bpList, deleteMaps })).then(() => {
|
||||
setPlaylists(playlists.filter(p => p.path !== bpList.path));
|
||||
setPlaylists(playlists$.value.filter(p => p.path !== bpList.path));
|
||||
})
|
||||
};
|
||||
|
||||
const openPlaylistDetails = (playlistKey: string) => {
|
||||
const localPlaylist$ = playlists$.pipe(map(playlists => playlists.find(p => p.path === playlistKey)));
|
||||
const openPlaylistDetails = (playlistPath: string) => {
|
||||
const localPlaylist$ = playlists$.pipe(map(playlists => playlists.find(p => p.path === playlistPath)));
|
||||
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()))),
|
||||
@@ -144,6 +142,22 @@ export const LocalPlaylistsListPanel = forwardRef<unknown, Props>(({ version, cl
|
||||
})
|
||||
};
|
||||
|
||||
const renderPlaylist = useCallback((playlist: LocalBPListsDetails) => {
|
||||
return (
|
||||
<PlaylistItem
|
||||
key={playlist.path}
|
||||
{...PlaylistItemComponentPropsMapper.fromLocalBPListDetails(playlist)}
|
||||
isDownloading$={playlistDownloader.$isPlaylistDownloading(playlist.customData?.syncURL ?? playlist.path, version)}
|
||||
isInQueue$={playlistDownloader.$isPlaylistInQueue(playlist.customData?.syncURL ?? playlist.path, version)}
|
||||
onClickOpen={() => openPlaylistDetails(playlist.path)}
|
||||
onClickDelete={() => deletePlaylist(playlist)}
|
||||
onClickSync={isOnline && (() => installPlaylist(playlist))}
|
||||
onClickOpenFile={() => viewPlaylistFile(playlist.path)}
|
||||
onClickCancelDownload={() => playlistDownloader.cancelDownload(playlist.customData?.syncURL ?? playlist.path, version)}
|
||||
/>
|
||||
);
|
||||
}, [isOnline, version]);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{(() => {
|
||||
@@ -155,21 +169,18 @@ export const LocalPlaylistsListPanel = forwardRef<unknown, Props>(({ version, cl
|
||||
|
||||
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 =>
|
||||
<PlaylistItem
|
||||
key={p.path}
|
||||
{...PlaylistItemComponentPropsMapper.fromLocalBPListDetails(p)}
|
||||
isDownloading$={playlistDownloader.$isPlaylistDownloading(p.customData?.syncURL ?? p.path, version)}
|
||||
isInQueue$={playlistDownloader.$isPlaylistInQueue(p.customData?.syncURL ?? p.path, version)}
|
||||
onClickOpen={() => openPlaylistDetails(p.path)}
|
||||
onClickDelete={() => deletePlaylist(p)}
|
||||
onClickSync={isOnline && (() => installPlaylist(p))}
|
||||
onClickOpenFile={() => viewPlaylistFile(p.path)}
|
||||
onClickCancelDownload={() => playlistDownloader.cancelDownload(p.customData?.syncURL ?? p.path, version)}
|
||||
/>
|
||||
)}
|
||||
</ul>
|
||||
<VirtualScroll
|
||||
classNames={{
|
||||
mainDiv: "size-full",
|
||||
rows: "gap-2 px-2 py-2"
|
||||
}}
|
||||
itemHeight={120}
|
||||
maxColumns={4}
|
||||
minItemWidth={390}
|
||||
items={playlists}
|
||||
renderItem={renderPlaylist}
|
||||
rowKey={rowPlaylists => rowPlaylists.map(p => p.path).join("-")}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { useConstant } from "renderer/hooks/use-constant.hook";
|
||||
import { getLocalTimeZone, parseAbsolute, toCalendarDateTime } from "@internationalized/date";
|
||||
import { VirtualScroll } from "renderer/components/shared/virtual-scroll/virtual-scroll.component";
|
||||
|
||||
export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; ownedMaps: BsmLocalMap[] }> = ({ options: {data : { ownedMaps, version }} }) => {
|
||||
const beatSaver = useService(BeatSaverService);
|
||||
@@ -35,6 +36,7 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
|
||||
const [filter, setFilter] = useState<MapFilter>({});
|
||||
const [query, setQuery] = useState("");
|
||||
const [maps, setMaps] = useState<BsvMapDetail[]>([]);
|
||||
const [downloadbleMaps, setDownloadbleMaps] = useState<DownloadableMap[]>([]);
|
||||
const [sortOrder, setSortOrder] = useState<BsvSearchOrder>(BsvSearchOrder.Latest);
|
||||
const [ownedMapHashs, setOwnedMapHashs] = useState<string[]>(ownedMaps?.map(map => map.hash) ?? []);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -46,19 +48,27 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
|
||||
q: query,
|
||||
});
|
||||
|
||||
const loaderRef = useRef(null);
|
||||
|
||||
const sortOptions: BsmSelectOption<BsvSearchOrder>[] = useConstant(() => {
|
||||
return Object.values(BsvSearchOrder).map(sort => ({ text: `beat-saver.maps-sorts.${sort}`, value: sort }));
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setDownloadbleMaps(() => maps.map(map => {
|
||||
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 { map, isOwned: isMapOwned, idDownloading: isDownloading, isInQueue: inQueue };
|
||||
}));
|
||||
}, [maps, currentDownload, mapsInQueue, ownedMapHashs, currentDownload])
|
||||
|
||||
useEffect(() => {
|
||||
loadMaps(searchParams);
|
||||
}, [searchParams]);
|
||||
|
||||
useEffect(() => {
|
||||
const onMapDownloaded = (map: BsmLocalMap, targerVersion: BSVersion) => {
|
||||
if (!equal(targerVersion, version)) {
|
||||
const onMapDownloaded = (map: BsmLocalMap, targetVersion: BSVersion) => {
|
||||
if (!equal(targetVersion, version)) {
|
||||
return;
|
||||
}
|
||||
const downloadedHash = map.hash;
|
||||
@@ -121,10 +131,11 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
|
||||
})
|
||||
};
|
||||
|
||||
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);
|
||||
const renderMap = useCallback((downloadableMap: DownloadableMap) => {
|
||||
const map = downloadableMap.map;
|
||||
|
||||
const downloadable = !downloadableMap.isOwned && !downloadableMap.isInQueue;
|
||||
const cancelable = downloadableMap.isInQueue && !downloadableMap.idDownloading;
|
||||
|
||||
return <MapItem
|
||||
autor={map.metadata.levelAuthorName}
|
||||
@@ -141,13 +152,13 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
|
||||
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}
|
||||
onDownload={downloadable && handleDownloadMap}
|
||||
onDoubleClick={downloadable && handleDownloadMap}
|
||||
onCancelDownload={cancelable && handleCancelDownload}
|
||||
downloading={downloadableMap.idDownloading}
|
||||
showOwned={downloadableMap.isOwned}
|
||||
callBackParam={map} />;
|
||||
};
|
||||
}, [version]);
|
||||
|
||||
const handleDownloadMap = useCallback((map: BsvMapDetail) => {
|
||||
mapsDownloader.addMapToDownload({ map, version });
|
||||
@@ -176,6 +187,9 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
|
||||
};
|
||||
|
||||
const handleLoadMore = () => {
|
||||
|
||||
if(loading){ return; }
|
||||
|
||||
setSearchParams(prev => {
|
||||
return { ...prev, page: prev.page + 1 };
|
||||
});
|
||||
@@ -206,7 +220,62 @@ 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-default z-0">
|
||||
|
||||
{(() => {
|
||||
if(downloadbleMaps?.length) {
|
||||
return (
|
||||
<VirtualScroll
|
||||
classNames={{
|
||||
mainDiv: "size-full",
|
||||
rows: "gap-x-2 px-2 py-2"
|
||||
}}
|
||||
minColumns={1}
|
||||
maxColumns={2}
|
||||
itemHeight={108}
|
||||
minItemWidth={400}
|
||||
items={downloadbleMaps}
|
||||
rowKey={mapsInRow => mapsInRow.map(map => map.map.id).join("-")}
|
||||
renderItem={renderMap}
|
||||
scrollEnd={{
|
||||
onScrollEnd: handleLoadMore,
|
||||
margin: 100
|
||||
}}
|
||||
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<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=" " />
|
||||
<span className="text-lg">
|
||||
{(() => {
|
||||
if (loading) {
|
||||
return t("modals.download-maps.loading-maps");
|
||||
}
|
||||
if (isOnline) {
|
||||
return t("modals.download-maps.no-maps-found");
|
||||
}
|
||||
return t("modals.download-maps.no-internet");
|
||||
})()}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
})()}
|
||||
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
type DownloadableMap = {
|
||||
map: BsvMapDetail;
|
||||
isOwned: boolean;
|
||||
idDownloading: boolean;
|
||||
isInQueue: boolean;
|
||||
};
|
||||
|
||||
{/* <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=" " />
|
||||
@@ -228,7 +297,4 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
|
||||
<motion.span onViewportEnter={handleLoadMore} ref={loaderRef} className="block w-full h-8" />
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
</ul> */}
|
||||
|
||||
+58
-23
@@ -1,4 +1,3 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import { BsmSelect, BsmSelectOption } from "renderer/components/shared/bsm-select.component";
|
||||
import { ModalComponent } from "renderer/services/modale.service";
|
||||
@@ -22,6 +21,7 @@ import equal from "fast-deep-equal";
|
||||
import { catchError, of } from "rxjs";
|
||||
import Tippy from "@tippyjs/react";
|
||||
import { BsmLocalModel } from "shared/models/models/bsm-local-model.interface";
|
||||
import { VirtualScroll } from "renderer/components/shared/virtual-scroll/virtual-scroll.component";
|
||||
|
||||
export const DownloadModelsModal: ModalComponent<void, { version: BSVersion; type: MSModelType; owned: BsmLocalModel[] }> = ({ options: {data: { version, type, owned }} }) => {
|
||||
const modelsDownloader = useService(ModelsDownloaderService);
|
||||
@@ -47,6 +47,7 @@ export const DownloadModelsModal: ModalComponent<void, { version: BSVersion; typ
|
||||
const currentDownload = useObservable(() => modelsDownloader.currentDownload$(), null);
|
||||
const downloadQueue = useObservable(() => modelsDownloader.getQueue$(), []);
|
||||
const [msModels, msModels$] = useBehaviorSubject<MSModel[]>([]);
|
||||
const [renderableModels, setRenderableModels] = useState<RenderableModel[]>([]);
|
||||
const isOnline = useObservable(() => os.isOnline$, true);
|
||||
const [error, error$] = useBehaviorSubject(false);
|
||||
const [isLoading, isLoading$] = useBehaviorSubject(false);
|
||||
@@ -57,6 +58,15 @@ export const DownloadModelsModal: ModalComponent<void, { version: BSVersion; typ
|
||||
const [searhInput, searhInput$] = useBehaviorSubject("");
|
||||
const [getQuery, getQuery$] = useBehaviorSubject<MSGetQuery>({ type: currentType, platform: MSModelPlatform.PC, start: 0, end: 25, sort: currentSort, sortDirection: MSGetSortDirection.Descending });
|
||||
|
||||
useOnUpdate(() => {
|
||||
setRenderableModels(() => msModels?.map(msModel => ({
|
||||
model: msModel,
|
||||
isInQueue: downloadQueue.some(download => download.model.id === msModel.id && equal(download.version, version)),
|
||||
isDownloading: equal(currentDownload, { model: msModel, version } as ModelDownload),
|
||||
isOwned: ownedModels.some(owned => owned.hash === msModel.hash),
|
||||
})))
|
||||
}, [msModels, currentDownload, downloadQueue, ownedModels]);
|
||||
|
||||
useOnUpdate(() => {
|
||||
const sub = modelsDownloader.onModelsDownloaded(model => {
|
||||
if (!equal(model.version, version)) {
|
||||
@@ -101,6 +111,9 @@ export const DownloadModelsModal: ModalComponent<void, { version: BSVersion; typ
|
||||
}, [currentType, currentSort]);
|
||||
|
||||
const loadMore = () => {
|
||||
|
||||
if(isLoading){ return; }
|
||||
|
||||
const currentQuery = getQuery;
|
||||
currentQuery.start += 25;
|
||||
currentQuery.end += 25;
|
||||
@@ -128,13 +141,19 @@ export const DownloadModelsModal: ModalComponent<void, { version: BSVersion; typ
|
||||
modelsDownloader.removeFromDownloadQueue({ model, version });
|
||||
}, []);
|
||||
|
||||
const modelPendingDownload = (model: MSModel) => {
|
||||
return downloadQueue.some(download => download.model.id === model.id && equal(download.version, version));
|
||||
};
|
||||
|
||||
const isModelOwned = (model: MSModel) => {
|
||||
return !!ownedModels.some(owned => owned.hash === model.hash);
|
||||
};
|
||||
const renderModel = useCallback((renderModel: RenderableModel) => {
|
||||
return (
|
||||
<ModelItem
|
||||
key={renderModel.model.id}
|
||||
{...renderModel.model}
|
||||
callbackValue={renderModel.model}
|
||||
isDownloading={renderModel.isDownloading}
|
||||
onDownload={!renderModel.isOwned ? handleDownloadModel : undefined}
|
||||
onDoubleClick={!renderModel.isOwned ? handleDownloadModel : undefined}
|
||||
onCancelDownload={renderModel.isInQueue ? handleCancelDownload : undefined}
|
||||
/>
|
||||
);
|
||||
}, [version]);
|
||||
|
||||
const renderFilterTips = useConstant(() => (
|
||||
<div className="w-fit flex">
|
||||
@@ -217,21 +236,37 @@ 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-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=" " />
|
||||
{renderLoadingStatus()}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{msModels.map(model => (
|
||||
<ModelItem key={model.id} {...model} callbackValue={model} isDownloading={equal(currentDownload, { model, version } as ModelDownload)} onDownload={!isModelOwned(model) ? handleDownloadModel : undefined} onDoubleClick={!isModelOwned(model) ? handleDownloadModel : undefined} onCancelDownload={modelPendingDownload(model) ? handleCancelDownload : undefined} />
|
||||
))}
|
||||
<motion.span onViewportEnter={loadMore} className="block w-full h-8" />
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
|
||||
{!msModels.length ? (
|
||||
<div className="size-full flex flex-col items-center justify-center">
|
||||
<img className={`w-32 h-32 ${isLoading && "spin-loading"}`} src={isLoading && !error ? BeatWaitingImg : BeatConflictImg} alt=" " />
|
||||
{renderLoadingStatus()}
|
||||
</div>
|
||||
) : (
|
||||
<VirtualScroll
|
||||
classNames={{
|
||||
mainDiv: "size-full",
|
||||
rows: "gap-x-4 p-4 first:pt-1.5",
|
||||
}}
|
||||
maxColumns={Infinity}
|
||||
itemHeight={272}
|
||||
minItemWidth={256}
|
||||
items={renderableModels}
|
||||
rowKey={rowModels => rowModels.map(m => m.model.id).join("-")}
|
||||
renderItem={renderModel}
|
||||
scrollEnd={{
|
||||
onScrollEnd: loadMore,
|
||||
margin: 100,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
type RenderableModel = {
|
||||
model: MSModel;
|
||||
isInQueue: boolean;
|
||||
isDownloading: boolean;
|
||||
isOwned: boolean;
|
||||
};
|
||||
|
||||
+36
-10
@@ -31,7 +31,10 @@ export const DownloadPlaylistModal: ModalComponent<void, {version: BSVersion, ow
|
||||
const playlistDownloader = useService(PlaylistDownloaderService);
|
||||
|
||||
const [playlists, setPlaylists] = useState<BsvPlaylist[]>(null);
|
||||
const [downloadablePlaylists, setDownloadablePlaylists] = useState<DownloadablePlaylist[]>(null);
|
||||
|
||||
const ownedPlaylists = useObservable(() => ownedPlaylists$, []);
|
||||
const ownedMaps = useObservable(() => ownedMaps$, []);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
@@ -41,6 +44,14 @@ export const DownloadPlaylistModal: ModalComponent<void, {version: BSVersion, ow
|
||||
page: 0,
|
||||
});
|
||||
|
||||
useOnUpdate(() => {
|
||||
setDownloadablePlaylists(() => playlists?.map(playlist => ({
|
||||
playlist,
|
||||
isOwned: ownedPlaylists.some(ownedPlaylist => ownedPlaylist.id === playlist.playlistId),
|
||||
ownedMaps: ownedMaps
|
||||
})));
|
||||
}, [playlists, ownedPlaylists, ownedMaps])
|
||||
|
||||
useOnUpdate(() => {
|
||||
setLoading(() => true);
|
||||
beatSaver.searchPlaylists(searchParams)
|
||||
@@ -63,19 +74,28 @@ export const DownloadPlaylistModal: ModalComponent<void, {version: BSVersion, ow
|
||||
setSearchParams(prev => ({ ...prev, page: prev.page + 1 }));
|
||||
};
|
||||
|
||||
const renderPlaylist = useCallback((playlist: BsvPlaylist) => {
|
||||
const renderPlaylist = useCallback((downloadablePlaylists: DownloadablePlaylist) => {
|
||||
|
||||
const onClickDownload = async () => {
|
||||
const ownedMaps = await lastValueFrom(ownedMaps$.pipe(take(1)));
|
||||
await lastValueFrom(playlistDownloader.downloadPlaylist({ downloadSource: playlist.downloadURL, ignoreSongsHashs: ownedMaps.map(map => map.hash), version }));
|
||||
const playlist = downloadablePlaylists.playlist;
|
||||
|
||||
const onClickDownload = () => {
|
||||
lastValueFrom(playlistDownloader.downloadPlaylist({
|
||||
downloadSource: playlist.downloadURL,
|
||||
ignoreSongsHashs: downloadablePlaylists.ownedMaps.map(map => map.hash),
|
||||
version
|
||||
}));
|
||||
}
|
||||
|
||||
return (
|
||||
<PlaylistItem
|
||||
key={playlist.playlistId}
|
||||
{...PlaylistItemComponentPropsMapper.fromBsvPlaylist(playlist)}
|
||||
isDownloading$={playlistDownloader.$isPlaylistDownloading(playlist.downloadURL, version)}
|
||||
isInQueue$={playlistDownloader.$isPlaylistInQueue(playlist.downloadURL, version)}
|
||||
onClickOpen={() => openPlaylist(playlist)}
|
||||
onClickDownload={onClickDownload}
|
||||
onClickDownload={downloadablePlaylists.isOwned ? null : onClickDownload}
|
||||
onClickSync={downloadablePlaylists.isOwned ? onClickDownload : null}
|
||||
onClickCancelDownload={() => playlistDownloader.cancelDownload(playlist.downloadURL, version)}
|
||||
/>
|
||||
);
|
||||
}, [version]);
|
||||
@@ -84,7 +104,7 @@ export const DownloadPlaylistModal: ModalComponent<void, {version: BSVersion, ow
|
||||
<div className="max-w-[95vw] w-[970px] h-[85vh] flex flex-col gap-3">
|
||||
<DownloadPlaylistModalHeader className="h-9 w-full" value={searchParams} onSubmit={handleNewSearch}/>
|
||||
{(() => {
|
||||
if(!Array.isArray(playlists)){
|
||||
if(!Array.isArray(downloadablePlaylists)){
|
||||
return (
|
||||
<div className="w-full flex flex-col justify-center items-center mt-44">
|
||||
<BsmImage className={cn(["size-32", !error && "spin-loading"])} image={error ? BeatConflict : BeatWaiting} />
|
||||
@@ -94,7 +114,7 @@ export const DownloadPlaylistModal: ModalComponent<void, {version: BSVersion, ow
|
||||
</div>
|
||||
)
|
||||
}
|
||||
else if(playlists.length === 0){
|
||||
else if(downloadablePlaylists.length === 0){
|
||||
return (
|
||||
<div className="w-full flex flex-col justify-center items-center mt-44">
|
||||
<BsmImage className="size-32" image={BeatConflict} />
|
||||
@@ -107,11 +127,11 @@ export const DownloadPlaylistModal: ModalComponent<void, {version: BSVersion, ow
|
||||
return (
|
||||
<VirtualScroll
|
||||
classNames={{
|
||||
mainDiv: "size-full overflow-hidden",
|
||||
mainDiv: "size-full",
|
||||
rows: "gap-2 px-2 py-2"
|
||||
}}
|
||||
itemHeight={120}
|
||||
items={playlists}
|
||||
items={downloadablePlaylists}
|
||||
maxColumns={2}
|
||||
minItemWidth={80}
|
||||
scrollEnd={{
|
||||
@@ -119,10 +139,16 @@ export const DownloadPlaylistModal: ModalComponent<void, {version: BSVersion, ow
|
||||
margin: 120
|
||||
}}
|
||||
renderItem={renderPlaylist}
|
||||
itemKey={items => items.map(item => item.playlistId).join("-")}
|
||||
rowKey={items => items.map(item => item.playlist.playlistId).join("-")}
|
||||
/>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type DownloadablePlaylist = {
|
||||
playlist: BsvPlaylist;
|
||||
isOwned: boolean;
|
||||
ownedMaps: BsmLocalMap[];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import equal from "fast-deep-equal";
|
||||
import { memo, useRef, useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { MSModel } from "shared/models/models/model-saber.model";
|
||||
import { BsmImage } from "../shared/bsm-image.component";
|
||||
import { motion } from "framer-motion";
|
||||
@@ -19,6 +19,7 @@ import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import useDoubleClick from "use-double-click";
|
||||
import { useDelayedState } from "renderer/hooks/use-delayed-state.hook";
|
||||
import { ChevronTopIcon } from "../svgs/icons/chevron-top-icon.component";
|
||||
import { typedMemo } from "renderer/helpers/typed-memo";
|
||||
|
||||
type Props<T> = {
|
||||
selected?: boolean;
|
||||
@@ -193,6 +194,4 @@ function ModelItemElement<T = unknown>(props: Props<T>) {
|
||||
);
|
||||
}
|
||||
|
||||
const typedMemo: <T, P>(c: T, propsAreEqual?: (prevProps: Readonly<P>, nextProps: Readonly<P>) => boolean) => T = memo;
|
||||
|
||||
export const ModelItem = typedMemo(ModelItemElement, equal);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { forwardRef, useImperativeHandle } from "react";
|
||||
import { forwardRef, useCallback, useImperativeHandle, useState } from "react";
|
||||
import { MSModelType } from "shared/models/models/model-saber.model";
|
||||
import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
|
||||
import { useConstant } from "renderer/hooks/use-constant.hook";
|
||||
@@ -7,7 +7,6 @@ import { ModelsManagerService } from "renderer/services/models-management/models
|
||||
import { useSwitchableObservable } from "renderer/hooks/use-switchable-observable.hook";
|
||||
import { BsmLocalModel } from "shared/models/models/bsm-local-model.interface";
|
||||
import { ModelItem } from "./model-item.component";
|
||||
import { useBehaviorSubject } from "renderer/hooks/use-behavior-subject.hook";
|
||||
import { BsmImage } from "../shared/bsm-image.component";
|
||||
import BeatConflict from "../../../../assets/images/apngs/beat-conflict.png";
|
||||
import { BehaviorSubject, distinctUntilChanged, map, startWith } from "rxjs";
|
||||
@@ -19,6 +18,7 @@ import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { ModelsDownloaderService } from "renderer/services/models-management/models-downloader.service";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { BsContentLoader } from "../shared/bs-content-loader.component";
|
||||
import { VirtualScroll } from "../shared/virtual-scroll/virtual-scroll.component";
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
@@ -36,8 +36,9 @@ export const ModelsGrid = forwardRef<unknown, Props>(({ className, version, type
|
||||
const t = useTranslation();
|
||||
|
||||
const [models, setModelsLoadObservable, , setModels] = useSwitchableObservable<BsmLocalModel[]>();
|
||||
const [renderableModels, setRenderableModels] = useState<RenderableModel[]>([]);
|
||||
const progress$ = useConstant(() => new BehaviorSubject(0));
|
||||
const [modelsSelected, modelsSelected$] = useBehaviorSubject<BsmLocalModel[]>([]);
|
||||
const [modelsSelected, setModelsSelected] = useState<BsmLocalModel[]>([]);
|
||||
|
||||
const isLoading = !models;
|
||||
const hasModels = !!models?.length;
|
||||
@@ -61,13 +62,20 @@ export const ModelsGrid = forwardRef<unknown, Props>(({ className, version, type
|
||||
}
|
||||
const newModels = models.filter(m => !deletedModels.some(d => d.hash === m.hash));
|
||||
setModels(() => newModels);
|
||||
modelsSelected$.next([]);
|
||||
setModelsSelected(() => []);
|
||||
});
|
||||
},
|
||||
}),
|
||||
[modelsSelected, models]
|
||||
);
|
||||
|
||||
useOnUpdate(() => setRenderableModels(() => (
|
||||
models?.map(model => ({
|
||||
model,
|
||||
selected: modelsSelected.some(m => m.hash === model.hash)
|
||||
})) ?? [])
|
||||
), [modelsSelected, models]);
|
||||
|
||||
useOnUpdate(() => {
|
||||
if (!active) {
|
||||
return;
|
||||
@@ -134,27 +142,28 @@ export const ModelsGrid = forwardRef<unknown, Props>(({ className, version, type
|
||||
};
|
||||
|
||||
const handleModelClick = (model: BsmLocalModel) => {
|
||||
const prunedArray = Array.from(new Set(modelsSelected));
|
||||
if (prunedArray.some(m => m.hash === model.hash)) {
|
||||
prunedArray.splice(
|
||||
prunedArray.findIndex(m => m.hash === model.hash),
|
||||
1
|
||||
);
|
||||
} else {
|
||||
prunedArray.push(model);
|
||||
}
|
||||
modelsSelected$.next(prunedArray);
|
||||
setModelsSelected(prev => {
|
||||
const newModels = [...prev];
|
||||
const index = newModels.findIndex(m => m.hash === model.hash);
|
||||
if (index === -1) {
|
||||
newModels.push(model);
|
||||
} else {
|
||||
newModels.splice(index, 1);
|
||||
}
|
||||
return newModels;
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
const filtredModels = () => {
|
||||
if (!active) {
|
||||
return models;
|
||||
return renderableModels;
|
||||
}
|
||||
|
||||
const lowerSearch = search?.toLowerCase();
|
||||
|
||||
return models?.filter(model => {
|
||||
const findedInRawValues = Object.values(model).some(value => {
|
||||
return renderableModels?.filter(model => {
|
||||
const findedInRawValues = Object.values(model.model).some(value => {
|
||||
if (typeof value !== "string" && typeof value !== "number") {
|
||||
return false;
|
||||
}
|
||||
@@ -165,11 +174,11 @@ export const ModelsGrid = forwardRef<unknown, Props>(({ className, version, type
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!model.model) {
|
||||
if (!model.model.model) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Object.values(model.model).some(value => {
|
||||
return Object.values(model.model.model).some(value => {
|
||||
if (typeof value !== "string" && typeof value !== "number" && !Array.isArray(value)) {
|
||||
return false;
|
||||
}
|
||||
@@ -189,10 +198,29 @@ export const ModelsGrid = forwardRef<unknown, Props>(({ className, version, type
|
||||
return;
|
||||
}
|
||||
setModels(prev => prev.filter(m => m.hash !== model.hash));
|
||||
modelsSelected$.next(modelsSelected.filter(m => m.hash !== model.hash));
|
||||
setModelsSelected(modelsSelected => modelsSelected.filter(m => m.hash !== model.hash));
|
||||
});
|
||||
};
|
||||
|
||||
const renderModel = useCallback((renderableModel: RenderableModel) => {
|
||||
|
||||
const model = renderableModel.model;
|
||||
|
||||
return (
|
||||
<ModelItem
|
||||
{...model?.model}
|
||||
key={model.path}
|
||||
hash={model.model?.hash ?? model.hash}
|
||||
path={model.path}
|
||||
type={model.type}
|
||||
name={model.model?.name ?? model.fileName}
|
||||
selected={renderableModel.selected}
|
||||
onClick={() => handleModelClick(model)}
|
||||
onDelete={() => handleDelete(model)}
|
||||
/>
|
||||
)
|
||||
}, [version]);
|
||||
|
||||
const renderContent = () => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -220,17 +248,30 @@ 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-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)} />
|
||||
))}
|
||||
</ul>
|
||||
<VirtualScroll
|
||||
classNames={{
|
||||
mainDiv: "size-full",
|
||||
rows: "gap-x-4 p-4"
|
||||
}}
|
||||
itemHeight={272}
|
||||
minItemWidth={256}
|
||||
maxColumns={Infinity}
|
||||
items={filtredModels()}
|
||||
rowKey={rowModels => rowModels.map(m => m.model.path).join("-")}
|
||||
renderItem={renderModel}
|
||||
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`w-full h-full flex-shrink-0 ${className ?? ""}`}>
|
||||
<div className={`size-full flex-shrink-0 ${className ?? ""}`}>
|
||||
{renderContent()}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
type RenderableModel = {
|
||||
model: BsmLocalModel;
|
||||
selected: boolean;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CSSProperties, memo } from "react";
|
||||
import { CSSProperties } from "react";
|
||||
import { cn } from "renderer/helpers/css-class.helpers";
|
||||
import { typedMemo } from "renderer/helpers/typed-memo";
|
||||
|
||||
type Props<T> = {
|
||||
className?: string;
|
||||
@@ -19,6 +20,4 @@ function VirtualRowComponent<T>({ className, style, items, renderItem }: Props<T
|
||||
);
|
||||
}
|
||||
|
||||
const typedMemo: <T, P>(c: T, propsAreEqual?: (prevProps: Readonly<P>, nextProps: Readonly<P>) => boolean) => T = memo;
|
||||
|
||||
export const VirtualRow = typedMemo(VirtualRowComponent);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useLayoutEffect, useRef, useState } from "react";
|
||||
import { Key, useCallback, useLayoutEffect, useRef, useState } from "react";
|
||||
import { ListChildComponentProps, ListOnScrollProps, VariableSizeList } from "react-window";
|
||||
import { cn } from "renderer/helpers/css-class.helpers";
|
||||
import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
|
||||
@@ -28,11 +28,11 @@ type Props<T = unknown> = {
|
||||
itemHeight: number;
|
||||
items: T[];
|
||||
renderItem: (item: T) => JSX.Element;
|
||||
itemKey: (item: T[]) => string;
|
||||
rowKey: (rowItems: T[]) => Key;
|
||||
scrollEnd?: ScrollEndHandler;
|
||||
}
|
||||
|
||||
export function VirtualScroll<T = unknown>({ className, classNames, minItemWidth, maxColumns, minColumns, itemHeight, items, scrollEnd, renderItem, itemKey}: Props<T>) {
|
||||
export function VirtualScroll<T = unknown>({ className, classNames, minItemWidth, maxColumns, minColumns, itemHeight, items, scrollEnd, renderItem, rowKey}: Props<T>) {
|
||||
|
||||
const ref = useRef(null);
|
||||
const listRef = useRef<HTMLDivElement>(null);
|
||||
@@ -93,7 +93,7 @@ export function VirtualScroll<T = unknown>({ className, classNames, minItemWidth
|
||||
width="100%" height={listHeight}
|
||||
layout="vertical"
|
||||
itemCount={itemsToRender.length}
|
||||
itemKey={i => itemKey?.(itemsToRender[i]) ?? i}
|
||||
itemKey={i => rowKey?.(itemsToRender[i]) ?? i}
|
||||
itemSize={() => itemHeight}
|
||||
itemData={itemsToRender}
|
||||
style={{ scrollbarGutter: "stable both-edges" }}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { memo } from "react";
|
||||
|
||||
export const typedMemo: <T, P>(c: T, propsAreEqual?: (prevProps: Readonly<P>, nextProps: Readonly<P>) => boolean) => T = memo;
|
||||
@@ -45,7 +45,6 @@ export class MapsDownloaderService {
|
||||
});
|
||||
|
||||
this.ipc.watch<{map: BsmLocalMap, version?: BSVersion}>("map-downloaded").subscribe((data) => {
|
||||
console.log(data);
|
||||
this.downloadedListerners.forEach(func => func(data.map, data.version));
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user