[feature-107] Fix issues with virtualScroll + add on scrollEnd on virtualScroll + Fix issue with submit playlists filter

This commit is contained in:
MathieuG-P
2024-05-30 20:20:40 +02:00
parent 7562b07832
commit 044174c711
4 changed files with 94 additions and 33 deletions
@@ -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>
)
@@ -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>
)