mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
[feature-107] Fix issues with virtualScroll + add on scrollEnd on virtualScroll + Fix issue with submit playlists filter
This commit is contained in:
+1
-1
@@ -55,7 +55,7 @@ export function DownloadPlaylistModalHeader({ className, value, onSubmit }: Prop
|
||||
<DownloadPlaylistFilterPanel className="z-10 translate-y-1" params={filter} onSubmit={handleFilterSubmit}/>
|
||||
</BsmDropdownButton>
|
||||
<input className="h-full theme-color-1 rounded-full px-2 grow pb-0.5" type="text" placeholder="Rechercher une playlist" value={query} onChange={e => setQuery(e.target.value)} />
|
||||
<BsmButton type="submit" className="shrink-0 rounded-full py-1 px-3 !theme-color-1 flex justify-center items-center capitalize" icon="search" text="modals.download-maps.search-btn" withBar={false} />
|
||||
<BsmButton className="shrink-0 rounded-full py-1 px-3 !theme-color-1 flex justify-center items-center capitalize" icon="search" text="modals.download-maps.search-btn" withBar={false} onClick={() => submit(searchParams)} />
|
||||
<BsmSelect className="theme-color-1 rounded-full px-1 pb-0.5 text-center cursor-pointer" options={sortOptions} selected={order} onChange={handleOrderChange}/>
|
||||
</form>
|
||||
)
|
||||
|
||||
+46
-21
@@ -1,24 +1,24 @@
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook"
|
||||
import { ModalComponent, ModalService } from "renderer/services/modale.service"
|
||||
import { Observable, lastValueFrom } from "rxjs"
|
||||
import { Observable, lastValueFrom, take } from "rxjs"
|
||||
import { BSVersion } from "shared/bs-version.interface"
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"
|
||||
import { LocalBPListsDetails } from "shared/models/playlists/local-playlist.models"
|
||||
import { DownloadPlaylistModalHeader } from "./download-playlist-modal-header.component"
|
||||
import { BsvPlaylist, BsvSearchOrder, PlaylistSearchParams } from "shared/models/maps/beat-saver.model"
|
||||
import { useState } from "react"
|
||||
import { useCallback, useState } from "react"
|
||||
import { useOnUpdate } from "renderer/hooks/use-on-update.hook"
|
||||
import { useService } from "renderer/hooks/use-service.hook"
|
||||
import { BeatSaverService } from "renderer/services/thrird-partys/beat-saver.service"
|
||||
import { PlaylistItem } from "renderer/components/maps-playlists-panel/playlists/playlist-item.component"
|
||||
import { PlaylistItemComponentPropsMapper } from "shared/mappers/playlist/playlist-item-component-props.mapper"
|
||||
import { motion } from "framer-motion"
|
||||
import { PlaylistDownloaderService } from "renderer/services/playlist-downloader.service"
|
||||
import { BsvPlaylistDetailsModal } from "../playlist-details-modal/bsv-playlist-details-modal.component"
|
||||
import { BsmImage } from "renderer/components/shared/bsm-image.component"
|
||||
import BeatWaiting from "../../../../../../../assets/images/apngs/beat-waiting.png"
|
||||
import BeatConflict from "../../../../../../../assets/images/apngs/beat-conflict.png"
|
||||
import { cn } from "renderer/helpers/css-class.helpers"
|
||||
import { VirtualScroll } from "renderer/components/shared/virtual-scroll/virtual-scroll.component"
|
||||
|
||||
// TODO : Translate
|
||||
|
||||
@@ -32,8 +32,8 @@ export const DownloadPlaylistModal: ModalComponent<void, {version: BSVersion, ow
|
||||
|
||||
const [playlists, setPlaylists] = useState<BsvPlaylist[]>(null);
|
||||
const ownedPlaylists = useObservable(() => ownedPlaylists$, []);
|
||||
const ownedMaps = useObservable(() => ownedMaps$, []);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
const [searchParams, setSearchParams] = useState<PlaylistSearchParams>({
|
||||
q: "",
|
||||
@@ -42,24 +42,44 @@ export const DownloadPlaylistModal: ModalComponent<void, {version: BSVersion, ow
|
||||
});
|
||||
|
||||
useOnUpdate(() => {
|
||||
setLoading(() => true);
|
||||
beatSaver.searchPlaylists(searchParams)
|
||||
.then(playlists => setPlaylists(prev => [...(prev ?? []), ...(playlists ?? [])] ))
|
||||
.catch(() => setError(() => true));
|
||||
}, [searchParams])
|
||||
.catch(() => setError(() => true))
|
||||
.finally(() => setLoading(() => false));
|
||||
}, [searchParams]);
|
||||
|
||||
const handleNewSearch = (value: Omit<PlaylistSearchParams, "page">) => {
|
||||
setPlaylists(() => []);
|
||||
setSearchParams(() => ({ ...value, page: 0}));
|
||||
};
|
||||
|
||||
const loadMorePlaylists = () => {
|
||||
setSearchParams(prev => ({ ...prev, page: prev.page + 1 }));
|
||||
};
|
||||
|
||||
const openPlaylist = (playlist: BsvPlaylist) => {
|
||||
modal.openModal(BsvPlaylistDetailsModal, { data: { playlist, version, installedMaps$: ownedMaps$ }, noStyle: true })
|
||||
};
|
||||
|
||||
const loadMorePlaylists = () => {
|
||||
if(loading){ return; }
|
||||
setSearchParams(prev => ({ ...prev, page: prev.page + 1 }));
|
||||
};
|
||||
|
||||
const renderPlaylist = useCallback((playlist: BsvPlaylist) => {
|
||||
|
||||
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 }));
|
||||
}
|
||||
|
||||
return (
|
||||
<PlaylistItem
|
||||
key={playlist.playlistId}
|
||||
{...PlaylistItemComponentPropsMapper.fromBsvPlaylist(playlist)}
|
||||
onClickOpen={() => openPlaylist(playlist)}
|
||||
onClickDownload={onClickDownload}
|
||||
/>
|
||||
);
|
||||
}, [version]);
|
||||
|
||||
return (
|
||||
<div className="max-w-[95vw] w-[970px] h-[85vh] flex flex-col gap-3">
|
||||
<DownloadPlaylistModalHeader className="h-9 w-full" value={searchParams} onSubmit={handleNewSearch}/>
|
||||
@@ -85,17 +105,22 @@ export const DownloadPlaylistModal: ModalComponent<void, {version: BSVersion, ow
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ul className="p-2 size-full flex flex-row flex-wrap justify-start content-start gap-3 grow overflow-y-scroll overflow-x-hidden z-[1]">
|
||||
{playlists.map(playlist => (
|
||||
<PlaylistItem
|
||||
key={playlist.playlistId}
|
||||
{...PlaylistItemComponentPropsMapper.fromBsvPlaylist(playlist)}
|
||||
onClickOpen={() => openPlaylist(playlist)}
|
||||
onClickDownload={() => lastValueFrom(playlistDownloader.downloadPlaylist({ downloadSource: playlist.downloadURL, ignoreSongsHashs: ownedMaps.map(map => map.hash), version }))}
|
||||
/>
|
||||
))}
|
||||
<motion.span className="block w-full h-8" onViewportEnter={loadMorePlaylists}/>
|
||||
</ul>
|
||||
<VirtualScroll
|
||||
classNames={{
|
||||
mainDiv: "size-full overflow-hidden",
|
||||
rows: "gap-2 px-2 py-2"
|
||||
}}
|
||||
itemHeight={120}
|
||||
items={playlists}
|
||||
maxColumns={2}
|
||||
minItemWidth={80}
|
||||
scrollEnd={{
|
||||
onScrollEnd: loadMorePlaylists,
|
||||
margin: 120
|
||||
}}
|
||||
renderItem={renderPlaylist}
|
||||
itemKey={items => items.map(item => item.playlistId).join("-")}
|
||||
/>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import equal from "fast-deep-equal";
|
||||
import { CSSProperties, memo } from "react";
|
||||
import { cn } from "renderer/helpers/css-class.helpers";
|
||||
|
||||
@@ -10,9 +9,10 @@ type Props<T> = {
|
||||
}
|
||||
|
||||
function VirtualRowComponent<T>({ className, style, items, renderItem }: Props<T>) {
|
||||
|
||||
return (
|
||||
<ul className={cn("h-fit w-full flex flex-nowrap", className)} style={style}>
|
||||
{items.map((item) => (
|
||||
{items?.map((item) => (
|
||||
renderItem(item)
|
||||
))}
|
||||
</ul>
|
||||
@@ -21,4 +21,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, equal);
|
||||
export const VirtualRow = typedMemo(VirtualRowComponent);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useLayoutEffect, useRef, useState } from "react";
|
||||
import { VariableSizeList } from "react-window";
|
||||
import { 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";
|
||||
import { VirtualRow } from "./virtual-row.component";
|
||||
@@ -14,6 +14,11 @@ type ClassNames = {
|
||||
rows?: string;
|
||||
}
|
||||
|
||||
type ScrollEndHandler = {
|
||||
onScrollEnd: () => void;
|
||||
margin?: number;
|
||||
}
|
||||
|
||||
type Props<T = unknown> = {
|
||||
className?: string;
|
||||
classNames?: ClassNames;
|
||||
@@ -23,13 +28,14 @@ type Props<T = unknown> = {
|
||||
itemHeight: number;
|
||||
items: T[];
|
||||
renderItem: (item: T) => JSX.Element;
|
||||
itemKey: (item: T[]) => string;
|
||||
scrollEnd?: ScrollEndHandler;
|
||||
}
|
||||
|
||||
export function VirtualScroll<T = unknown>({ className, classNames, minItemWidth, maxColumns, minColumns, itemHeight, items, renderItem}: Props<T>) {
|
||||
|
||||
console.log("omg les classes", className);
|
||||
export function VirtualScroll<T = unknown>({ className, classNames, minItemWidth, maxColumns, minColumns, itemHeight, items, scrollEnd, renderItem, itemKey}: Props<T>) {
|
||||
|
||||
const ref = useRef(null);
|
||||
const listRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [itemPerRow, setItemPerRow] = useState(1);
|
||||
const [itemsToRender, setItemsToRender] = useState<T[][]>([]);
|
||||
@@ -48,7 +54,6 @@ export function VirtualScroll<T = unknown>({ className, classNames, minItemWidth
|
||||
const observer = new ResizeObserver(() => {
|
||||
updateItemPerRow(ref.current?.clientWidth || 0);
|
||||
listHeight$.next(ref.current?.clientHeight || 0);
|
||||
console.log("list height", ref.current?.clientHeight);
|
||||
});
|
||||
|
||||
observer.observe(ref.current);
|
||||
@@ -61,10 +66,41 @@ export function VirtualScroll<T = unknown>({ className, classNames, minItemWidth
|
||||
setItemsToRender(() => splitedItems);
|
||||
}, [itemPerRow, items])
|
||||
|
||||
const handleScroll = (e: ListOnScrollProps) => {
|
||||
if(!scrollEnd?.onScrollEnd || !listRef.current){ return; }
|
||||
|
||||
const { scrollDirection, scrollOffset, scrollUpdateWasRequested } = e;
|
||||
const { scrollHeight } = listRef.current;
|
||||
|
||||
if(!scrollHeight || !listHeight){ return; }
|
||||
|
||||
const margin = scrollEnd.margin ?? 0;
|
||||
|
||||
if (scrollDirection === "forward" && !scrollUpdateWasRequested && scrollOffset + listHeight + margin >= scrollHeight) {
|
||||
scrollEnd.onScrollEnd();
|
||||
}
|
||||
};
|
||||
|
||||
const renderRow = useCallback((props: ListChildComponentProps<any>) => {
|
||||
return <VirtualRow items={props.data[props.index]} renderItem={renderItem} className={classNames?.rows} style={props.style}/>;
|
||||
}, [renderItem, classNames?.rows]);
|
||||
|
||||
return (
|
||||
<div ref={ref} className={cn(className, classNames?.mainDiv)}>
|
||||
<VariableSizeList className={cn("scrollbar-default", classNames?.variableList)} width="100%" height={listHeight} layout="vertical" itemCount={itemsToRender.length} itemKey={i => i} itemSize={() => itemHeight} itemData={itemsToRender} style={{ scrollbarGutter: "stable both-edges" }}>
|
||||
{props => <VirtualRow key={props.index} items={props.data[props.index]} renderItem={renderItem} className={classNames?.rows} style={props.style}/>}
|
||||
<VariableSizeList
|
||||
innerRef={listRef}
|
||||
className={cn("scrollbar-default", classNames?.variableList)}
|
||||
width="100%" height={listHeight}
|
||||
layout="vertical"
|
||||
itemCount={itemsToRender.length}
|
||||
itemKey={i => itemKey?.(itemsToRender[i]) ?? i}
|
||||
itemSize={() => itemHeight}
|
||||
itemData={itemsToRender}
|
||||
style={{ scrollbarGutter: "stable both-edges" }}
|
||||
onScroll={handleScroll}
|
||||
|
||||
>
|
||||
{renderRow}
|
||||
</VariableSizeList>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user