also some other UX improvements to the AZ scroller
This commit is contained in:
Violet Caulfield
2025-11-10 01:10:06 -06:00
parent cc78a783d4
commit a782f90da7
4 changed files with 60 additions and 42 deletions

View File

@@ -41,9 +41,8 @@ export default function Albums({
.filter((value, index, indices) => indices.indexOf(value) === index)
}, [showAlphabeticalSelector, albumsInfiniteQuery.data])
const { mutate: alphabetSelectorMutate } = useAlphabetSelector(
(letter) => (pendingLetterRef.current = letter.toUpperCase()),
)
const { mutateAsync: alphabetSelectorMutate, isPending: isAlphabetSelectorPending } =
useAlphabetSelector((letter) => (pendingLetterRef.current = letter.toUpperCase()))
const ItemSeparatorComponent = useCallback(
({ leadingItem, trailingItem }: { leadingItem: unknown; trailingItem: unknown }) =>
@@ -126,7 +125,7 @@ export default function Albums({
ItemSeparatorComponent={ItemSeparatorComponent}
refreshControl={
<RefreshControl
refreshing={albumsInfiniteQuery.isFetching}
refreshing={albumsInfiniteQuery.isFetching && !isAlphabetSelectorPending}
onRefresh={albumsInfiniteQuery.refetch}
tintColor={theme.primary.val}
/>

View File

@@ -46,7 +46,7 @@ export default function Artists({
const pendingLetterRef = useRef<string | null>(null)
const { mutate: alphabetSelectorMutate, isPending: isAlphabetSelectorPending } =
const { mutateAsync: alphabetSelectorMutate, isPending: isAlphabetSelectorPending } =
useAlphabetSelector((letter) => (pendingLetterRef.current = letter.toUpperCase()))
const stickyHeaderIndices = useMemo(() => {
@@ -126,7 +126,7 @@ export default function Artists({
data={artists}
refreshControl={
<RefreshControl
refreshing={artistsInfiniteQuery.isFetching || isAlphabetSelectorPending}
refreshing={artistsInfiniteQuery.isPending && !isAlphabetSelectorPending}
onRefresh={() => artistsInfiniteQuery.refetch()}
tintColor={theme.primary.val}
/>

View File

@@ -1,17 +1,10 @@
import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react'
import { LayoutChangeEvent, View as RNView } from 'react-native'
import { getToken, useTheme, View, YStack } from 'tamagui'
import { LayoutChangeEvent, Platform, View as RNView } from 'react-native'
import { getToken, Spinner, useTheme, View, YStack } from 'tamagui'
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
Easing,
withSpring,
} from 'react-native-reanimated'
import { runOnJS } from 'react-native-worklets'
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated'
import { scheduleOnRN } from 'react-native-worklets'
import { Text } from '../helpers/text'
import { useSafeAreaFrame } from 'react-native-safe-area-context'
import { UseInfiniteQueryResult, useMutation } from '@tanstack/react-query'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client'
import useHapticFeedback from '../../../hooks/use-haptic-feedback'
@@ -30,12 +23,13 @@ const alphabet = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')
export default function AZScroller({
onLetterSelect,
}: {
onLetterSelect: (letter: string) => void
onLetterSelect: (letter: string) => Promise<void>
}) {
const { width } = useSafeAreaFrame()
const theme = useTheme()
const trigger = useHapticFeedback()
const [operationPending, setOperationPending] = useState<boolean>(false)
const overlayOpacity = useSharedValue(0)
const gesturePositionY = useSharedValue(0)
@@ -79,7 +73,7 @@ export default function AZScroller({
const letter = alphabet[index]
selectedLetter.value = letter
setOverlayLetter(letter)
runOnJS(showOverlay)()
scheduleOnRN(showOverlay)
}
})
.onUpdate((e) => {
@@ -90,13 +84,20 @@ export default function AZScroller({
const letter = alphabet[index]
selectedLetter.value = letter
setOverlayLetter(letter)
runOnJS(showOverlay)()
scheduleOnRN(showOverlay)
}
})
.onEnd(() => {
runOnJS(hideOverlay)()
if (selectedLetter.value) {
runOnJS(onLetterSelect)(selectedLetter.value.toLowerCase())
scheduleOnRN(async () => {
setOperationPending(true)
onLetterSelect(selectedLetter.value.toLowerCase()).then(() => {
scheduleOnRN(hideOverlay)
setOperationPending(false)
})
})
} else {
scheduleOnRN(hideOverlay)
}
}),
[onLetterSelect],
@@ -114,13 +115,21 @@ export default function AZScroller({
const letter = alphabet[index]
selectedLetter.value = letter
setOverlayLetter(letter)
runOnJS(showOverlay)()
scheduleOnRN(showOverlay)
}
})
.onEnd(() => {
runOnJS(hideOverlay)()
if (selectedLetter.value)
runOnJS(onLetterSelect)(selectedLetter.value.toLowerCase())
if (selectedLetter.value) {
scheduleOnRN(async () => {
setOperationPending(true)
onLetterSelect(selectedLetter.value.toLowerCase()).then(() => {
scheduleOnRN(hideOverlay)
setOperationPending(false)
})
})
} else {
scheduleOnRN(hideOverlay)
}
}),
[onLetterSelect],
)
@@ -155,6 +164,8 @@ export default function AZScroller({
requestAnimationFrame(() => {
alphabetSelectorRef.current?.measureInWindow((x, y, width, height) => {
alphabetSelectorTopY.current = y
if (Platform.OS === 'android') alphabetSelectorTopY.current += 20
})
})
}}
@@ -200,17 +211,26 @@ export default function AZScroller({
animatedOverlayStyle,
]}
>
<Animated.Text
style={{
fontSize: getToken('$12'),
textAlign: 'center',
fontFamily: 'Figtree-Bold',
color: theme.background.val,
marginHorizontal: 'auto',
}}
>
{overlayLetter}
</Animated.Text>
{operationPending ? (
<Spinner
size='large'
color={theme.background.val}
alignSelf='center'
justify={'center'}
/>
) : (
<Animated.Text
style={{
fontSize: getToken('$12'),
textAlign: 'center',
fontFamily: 'Figtree-Bold',
color: theme.background.val,
marginHorizontal: 'auto',
}}
>
{overlayLetter}
</Animated.Text>
)}
</Animated.View>
</>
)

View File

@@ -46,9 +46,8 @@ export default function Tracks({
.filter((value, index, indices) => indices.indexOf(value) === index)
}, [showAlphabeticalSelector, tracksInfiniteQuery.data])
const { mutate: alphabetSelectorMutate } = useAlphabetSelector(
(letter) => (pendingLetterRef.current = letter.toUpperCase()),
)
const { mutateAsync: alphabetSelectorMutate, isPending: isAlphabetSelectorPending } =
useAlphabetSelector((letter) => (pendingLetterRef.current = letter.toUpperCase()))
// Memoize the expensive tracks processing to prevent memory leaks
const tracksToDisplay = React.useMemo(
@@ -151,7 +150,7 @@ export default function Tracks({
renderItem={renderItem}
refreshControl={
<RefreshControl
refreshing={tracksInfiniteQuery.isFetching}
refreshing={tracksInfiniteQuery.isFetching && !isAlphabetSelectorPending}
onRefresh={tracksInfiniteQuery.refetch}
tintColor={theme.primary.val}
/>