fix slider + list bugs

This commit is contained in:
Boof2015
2026-07-25 15:46:38 -04:00
parent 0f34ca2e8d
commit a33a3b7137
15 changed files with 960 additions and 209 deletions
+1 -1
View File
@@ -112,7 +112,7 @@ export default function AlbumScreen() {
onScroll={onScroll}
scrollEventThrottle={scrollEventThrottle}
onEndReached={() => void loadMore()}
onEndReachedThreshold={0.6}
onEndReachedThreshold={2}
contentContainerStyle={{
paddingTop: insets.top + expandedHeight,
paddingHorizontal: spacing.lg,
+10
View File
@@ -221,6 +221,16 @@ export default function ArtistScreen() {
renderItem={renderItem}
onScroll={onScroll}
scrollEventThrottle={scrollEventThrottle}
// This screen composes four paged sources into one list and had no paging
// trigger at all, so anything past the first page was unreachable. Each
// loadMore no-ops once its own cursor runs out.
onEndReached={() => {
void allPage.loadMore();
void songsPage.loadMore();
void appearancesPage.loadMore();
void albumsPage.loadMore();
}}
onEndReachedThreshold={2}
contentContainerStyle={{
paddingTop: insets.top + expandedHeight,
paddingHorizontal: spacing.lg,
@@ -51,7 +51,7 @@ export default function ArtistAlbumsScreen() {
keyExtractor={(album) => album.identity_key}
showsVerticalScrollIndicator={false}
onEndReached={() => void page.loadMore()}
onEndReachedThreshold={0.6}
onEndReachedThreshold={2}
renderItem={({ item }) => (
<View style={styles.gridCell}>
<AlbumGridItem
@@ -73,7 +73,7 @@ export default function ArtistAppearancesScreen() {
keyExtractor={(track) => String(track.id)}
showsVerticalScrollIndicator={false}
onEndReached={() => void loadMore()}
onEndReachedThreshold={0.6}
onEndReachedThreshold={2}
renderItem={({ item, index }) => (
<TrackRow
track={item}
@@ -73,7 +73,7 @@ export default function ArtistSongsScreen() {
keyExtractor={(track) => String(track.id)}
showsVerticalScrollIndicator={false}
onEndReached={() => void loadMore()}
onEndReachedThreshold={0.6}
onEndReachedThreshold={2}
renderItem={({ item, index }) => (
<TrackRow
track={item}
+91 -8
View File
@@ -1,9 +1,12 @@
import {
useCallback,
useEffect,
useMemo,
useRef,
useState
} from 'react';
import {
ActivityIndicator,
BackHandler,
View,
Pressable,
@@ -68,6 +71,14 @@ import type {
const TRACK_SORT_OPTIONS: TrackSort[] = ['artist', 'title', 'recently_added', 'duration'];
const ALBUM_SORT_OPTIONS: AlbumSort[] = ['artist', 'name', 'recently_added', 'year'];
const ARTIST_SORT_OPTIONS: ArtistSort[] = ['name', 'track_count'];
/** How long the finger has to settle on a rail letter before the list jumps. */
const JUMP_DEBOUNCE_MS = 100;
/**
* Screens of runway to keep ahead of the scroll. The old 0.6 was less than a fling
* covers before a page comes back, so the list hit a wall that looked like the end.
*/
const END_REACHED_THRESHOLD = 2;
const START_REACHED_THRESHOLD = 0.5;
export default function LibraryScreen() {
const colors = useColors();
@@ -87,8 +98,15 @@ export default function LibraryScreen() {
const loadNextTracks = useLibraryStore((s) => s.loadNextTracks);
const loadNextAlbums = useLibraryStore((s) => s.loadNextAlbums);
const loadNextArtists = useLibraryStore((s) => s.loadNextArtists);
const loadPreviousTracks = useLibraryStore((s) => s.loadPreviousTracks);
const loadPreviousAlbums = useLibraryStore((s) => s.loadPreviousAlbums);
const loadPreviousArtists = useLibraryStore((s) => s.loadPreviousArtists);
const trackNextCursor = useLibraryStore((s) => s.trackNextCursor);
const albumNextCursor = useLibraryStore((s) => s.albumNextCursor);
const artistNextCursor = useLibraryStore((s) => s.artistNextCursor);
const sectionAnchors = useLibraryStore((s) => s.sectionAnchors);
const sectionJumpRevision = useLibraryStore((s) => s.sectionJumpRevision);
const jumpAnchorIndex = useLibraryStore((s) => s.jumpAnchorIndex);
const jumpToSection = useLibraryStore((s) => s.jumpToSection);
const isScanning = useLibraryStore((s) => s.isScanning);
const scanError = useLibraryStore((s) => s.scanError);
@@ -127,19 +145,64 @@ export default function LibraryScreen() {
};
const openSearch = () => openQuickSearch();
// Sits at the end of the loaded window whenever more pages exist, so a fling that
// outruns the loader stops on a spinner rather than on what looks like the end of
// the list. Gated on the cursor rather than on an in-flight flag so it does not
// blink between pages.
const listFooter = useMemo(
() => (
<View style={styles.listFooter}>
<ActivityIndicator size="small" color={colors.textTertiary} />
</View>
),
[colors.textTertiary]
);
const railVisible = sectionAnchors.length > 1;
const railLetters = useMemo(
() => new Set(sectionAnchors.map((entry) => entry.label)),
[sectionAnchors]
);
const jumpToLetter = (letter: string) => {
const anchor = sectionAnchors.find((entry) => entry.label === letter);
// A jump refills the window and remounts the list, so firing one per letter crossed
// made a fast scrub ~27 rebuilds. The bubble and haptic still track every letter
// (they live in the rail); only the jump itself waits for the finger to settle, and
// lifting off flushes it immediately.
const pendingJump = useRef<{ letter: string; timer: ReturnType<typeof setTimeout> } | null>(null);
const runJump = useCallback((letter: string) => {
const anchor = useLibraryStore.getState().sectionAnchors.find((entry) => entry.label === letter);
if (!anchor) return;
void jumpToSection(anchor.cursor).then((applied) => {
if (applied) scrollTop.setScrollAtTop(true);
// A jump usually lands with a page of rows above it, so the list is not at true
// top and pull-to-search must stay disarmed.
if (applied) scrollTop.setScrollAtTop(useLibraryStore.getState().jumpAnchorIndex === 0);
});
};
}, [jumpToSection, scrollTop]);
const jumpToLetter = useCallback((letter: string) => {
if (pendingJump.current) clearTimeout(pendingJump.current.timer);
pendingJump.current = {
letter,
timer: setTimeout(() => {
pendingJump.current = null;
runJump(letter);
}, JUMP_DEBOUNCE_MS),
};
}, [runJump]);
const flushJump = useCallback(() => {
const pending = pendingJump.current;
if (!pending) return;
clearTimeout(pending.timer);
pendingJump.current = null;
runJump(pending.letter);
}, [runJump]);
useEffect(() => () => {
if (pendingJump.current) clearTimeout(pendingJump.current.timer);
pendingJump.current = null;
}, []);
// Multi-select (tracks view): long-press arms it, batch actions live in the
// bottom bar, selection order follows the current display order.
@@ -303,8 +366,12 @@ export default function LibraryScreen() {
renderScrollComponent={PullSearchScrollView}
onScroll={scrollTop.onScroll}
scrollEventThrottle={scrollTop.scrollEventThrottle}
initialScrollIndex={jumpAnchorIndex}
onEndReached={() => void loadNextAlbums()}
onEndReachedThreshold={0.6}
onEndReachedThreshold={END_REACHED_THRESHOLD}
onStartReached={() => void loadPreviousAlbums()}
onStartReachedThreshold={START_REACHED_THRESHOLD}
ListFooterComponent={albumNextCursor ? listFooter : null}
renderItem={({ item }) => (
<View style={styles.gridCell}>
<AlbumGridItem
@@ -332,8 +399,12 @@ export default function LibraryScreen() {
renderScrollComponent={PullSearchScrollView}
onScroll={scrollTop.onScroll}
scrollEventThrottle={scrollTop.scrollEventThrottle}
initialScrollIndex={jumpAnchorIndex}
onEndReached={() => void loadNextArtists()}
onEndReachedThreshold={0.6}
onEndReachedThreshold={END_REACHED_THRESHOLD}
onStartReached={() => void loadPreviousArtists()}
onStartReachedThreshold={START_REACHED_THRESHOLD}
ListFooterComponent={artistNextCursor ? listFooter : null}
renderItem={({ item }) => (
<View style={styles.gridCell}>
<ArtistGridItem
@@ -360,8 +431,12 @@ export default function LibraryScreen() {
renderScrollComponent={PullSearchScrollView}
onScroll={scrollTop.onScroll}
scrollEventThrottle={scrollTop.scrollEventThrottle}
initialScrollIndex={jumpAnchorIndex}
onEndReached={() => void loadNextTracks()}
onEndReachedThreshold={0.6}
onEndReachedThreshold={END_REACHED_THRESHOLD}
onStartReached={() => void loadPreviousTracks()}
onStartReachedThreshold={START_REACHED_THRESHOLD}
ListFooterComponent={trackNextCursor ? listFooter : null}
extraData={selectMode ? selectedIds : undefined}
renderItem={({ item, index }) => (
<TrackRow
@@ -393,7 +468,11 @@ export default function LibraryScreen() {
) : null}
{railVisible ? (
<AlphabetRail activeLetters={railLetters} onJumpToLetter={jumpToLetter} />
<AlphabetRail
activeLetters={railLetters}
onJumpToLetter={jumpToLetter}
onScrubEnd={flushJump}
/>
) : null}
</View>
</>
@@ -481,4 +560,8 @@ const styles = StyleSheet.create({
listArea: {
flex: 1,
},
listFooter: {
paddingVertical: spacing.lg,
alignItems: 'center',
},
});
+1 -1
View File
@@ -284,7 +284,7 @@ export default function PlaylistScreen() {
onEndReached={() => {
if (!isFavorites) void loadNextEntries();
}}
onEndReachedThreshold={0.6}
onEndReachedThreshold={2}
contentContainerStyle={{
paddingTop: insets.top + expandedHeight,
paddingHorizontal: spacing.lg,