mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-12 05:10:52 +02:00
make queue faster
This commit is contained in:
+154
-119
@@ -187,10 +187,20 @@ export const QueueTray = memo(function QueueTray({ onClose, embedded = false }:
|
|||||||
const upcomingTotal =
|
const upcomingTotal =
|
||||||
activeIndex >= 0 ? Math.max(0, tracks.length - activeIndex - 1) : tracks.length;
|
activeIndex >= 0 ? Math.max(0, tracks.length - activeIndex - 1) : tracks.length;
|
||||||
const baseOffset = activeIndex >= 0 ? activeIndex + 1 : 0;
|
const baseOffset = activeIndex >= 0 ? activeIndex + 1 : 0;
|
||||||
|
// Row callbacks resolve indices at call time from refs so their identities
|
||||||
|
// survive track advances — an index captured at render time would go stale.
|
||||||
|
const baseOffsetRef = useRef(baseOffset);
|
||||||
|
useEffect(() => {
|
||||||
|
baseOffsetRef.current = baseOffset;
|
||||||
|
}, [baseOffset]);
|
||||||
|
|
||||||
const entrySerial = useRef(0);
|
// Built synchronously so a warm mirror paints on the list's first frame; the
|
||||||
const entriesRef = useRef<QueueEntry[]>([]);
|
// update effect below takes over from there.
|
||||||
const [entries, setEntries] = useState<QueueEntry[]>([]);
|
const [entries, setEntries] = useState<QueueEntry[]>(() =>
|
||||||
|
hasSnapshot ? reconcileQueueEntries(upcomingTracks, [], { current: 0 }) : []
|
||||||
|
);
|
||||||
|
const entrySerial = useRef(entries.length);
|
||||||
|
const entriesRef = useRef<QueueEntry[]>(entries);
|
||||||
const [editMode, setEditMode] = useState(false);
|
const [editMode, setEditMode] = useState(false);
|
||||||
const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set());
|
const [selectedKeys, setSelectedKeys] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
@@ -201,6 +211,9 @@ export const QueueTray = memo(function QueueTray({ onClose, embedded = false }:
|
|||||||
const dKey = useSharedValue('');
|
const dKey = useSharedValue('');
|
||||||
const dSettling = useSharedValue(false);
|
const dSettling = useSharedValue(false);
|
||||||
const dIndexByKey = useSharedValue<QueueIndexByKey>({});
|
const dIndexByKey = useSharedValue<QueueIndexByKey>({});
|
||||||
|
// Entry count for the drag clamp, set at drag-arm — capturing it in the
|
||||||
|
// gesture closure instead forced a gesture rebuild on every queue change.
|
||||||
|
const dCount = useSharedValue(0);
|
||||||
// The index map is only read by worklets while a drag is active/settling, so
|
// The index map is only read by worklets while a drag is active/settling, so
|
||||||
// it's maintained only inside that window — serializing a map with one entry
|
// it's maintained only inside that window — serializing a map with one entry
|
||||||
// per queued track to the UI runtime on every queue change froze long queues.
|
// per queued track to the UI runtime on every queue change froze long queues.
|
||||||
@@ -216,7 +229,8 @@ export const QueueTray = memo(function QueueTray({ onClose, embedded = false }:
|
|||||||
target: SharedValue<number>,
|
target: SharedValue<number>,
|
||||||
key: SharedValue<string>,
|
key: SharedValue<string>,
|
||||||
settling: SharedValue<boolean>,
|
settling: SharedValue<boolean>,
|
||||||
indexMap: SharedValue<QueueIndexByKey>
|
indexMap: SharedValue<QueueIndexByKey>,
|
||||||
|
count: SharedValue<number>
|
||||||
) => {
|
) => {
|
||||||
'worklet';
|
'worklet';
|
||||||
active.value = false;
|
active.value = false;
|
||||||
@@ -226,9 +240,10 @@ export const QueueTray = memo(function QueueTray({ onClose, embedded = false }:
|
|||||||
target.value = -1;
|
target.value = -1;
|
||||||
key.value = '';
|
key.value = '';
|
||||||
indexMap.value = {};
|
indexMap.value = {};
|
||||||
|
count.value = 0;
|
||||||
}
|
}
|
||||||
)(dActive, dTy, dStart, dTarget, dKey, dSettling, dIndexByKey);
|
)(dActive, dTy, dStart, dTarget, dKey, dSettling, dIndexByKey, dCount);
|
||||||
}, [dActive, dIndexByKey, dKey, dSettling, dStart, dTarget, dTy]);
|
}, [dActive, dCount, dIndexByKey, dKey, dSettling, dStart, dTarget, dTy]);
|
||||||
|
|
||||||
const clearDragAfterReorderCommit = useCallback(() => {
|
const clearDragAfterReorderCommit = useCallback(() => {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
@@ -237,18 +252,28 @@ export const QueueTray = memo(function QueueTray({ onClose, embedded = false }:
|
|||||||
}, [clearDragState]);
|
}, [clearDragState]);
|
||||||
|
|
||||||
const updateDragIndexMap = useCallback(
|
const updateDragIndexMap = useCallback(
|
||||||
(indexMap: QueueIndexByKey) => {
|
(indexMap: QueueIndexByKey, count: number) => {
|
||||||
runOnUI((sharedIndexMap: SharedValue<QueueIndexByKey>, nextIndexMap: QueueIndexByKey) => {
|
runOnUI(
|
||||||
'worklet';
|
(
|
||||||
sharedIndexMap.value = nextIndexMap;
|
sharedIndexMap: SharedValue<QueueIndexByKey>,
|
||||||
})(dIndexByKey, indexMap);
|
sharedCount: SharedValue<number>,
|
||||||
|
nextIndexMap: QueueIndexByKey,
|
||||||
|
nextCount: number
|
||||||
|
) => {
|
||||||
|
'worklet';
|
||||||
|
sharedIndexMap.value = nextIndexMap;
|
||||||
|
sharedCount.value = nextCount;
|
||||||
|
}
|
||||||
|
)(dIndexByKey, dCount, indexMap, count);
|
||||||
},
|
},
|
||||||
[dIndexByKey]
|
[dCount, dIndexByKey]
|
||||||
);
|
);
|
||||||
|
|
||||||
const setVisibleEntries = useCallback((nextEntries: QueueEntry[]) => {
|
const setVisibleEntries = useCallback((nextEntries: QueueEntry[]) => {
|
||||||
entriesRef.current = nextEntries;
|
entriesRef.current = nextEntries;
|
||||||
if (dragInFlightRef.current) updateDragIndexMap(indexQueueEntriesByKey(nextEntries));
|
if (dragInFlightRef.current) {
|
||||||
|
updateDragIndexMap(indexQueueEntriesByKey(nextEntries), nextEntries.length);
|
||||||
|
}
|
||||||
setEntries(nextEntries);
|
setEntries(nextEntries);
|
||||||
}, [updateDragIndexMap]);
|
}, [updateDragIndexMap]);
|
||||||
|
|
||||||
@@ -261,29 +286,26 @@ export const QueueTray = memo(function QueueTray({ onClose, embedded = false }:
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
setEntries((previous) => {
|
||||||
let frame: number | null = null;
|
const next = hasSnapshot
|
||||||
|
? reconcileQueueEntries(
|
||||||
frame = requestAnimationFrame(() => {
|
upcomingTracks,
|
||||||
if (cancelled) return;
|
entriesRef.current.length > 0 ? entriesRef.current : previous,
|
||||||
setEntries((previous) => {
|
entrySerial
|
||||||
const next = hasSnapshot
|
)
|
||||||
? reconcileQueueEntries(
|
: [];
|
||||||
upcomingTracks,
|
// The mount-time reconcile of the synchronous initial state is a no-op;
|
||||||
entriesRef.current.length > 0 ? entriesRef.current : previous,
|
// bail so it doesn't cost a render.
|
||||||
entrySerial
|
const unchanged =
|
||||||
)
|
next.length === previous.length &&
|
||||||
: [];
|
next.every((entry, index) => entry === previous[index]);
|
||||||
entriesRef.current = next;
|
const resolved = unchanged ? previous : next;
|
||||||
if (dragInFlightRef.current) updateDragIndexMap(indexQueueEntriesByKey(next));
|
entriesRef.current = resolved;
|
||||||
return next;
|
if (dragInFlightRef.current) {
|
||||||
});
|
updateDragIndexMap(indexQueueEntriesByKey(resolved), resolved.length);
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
if (frame != null) cancelAnimationFrame(frame);
|
|
||||||
};
|
|
||||||
}, [hasSnapshot, upcomingTracks, updateDragIndexMap]);
|
}, [hasSnapshot, upcomingTracks, updateDragIndexMap]);
|
||||||
|
|
||||||
const visibleSelectedKeys = useMemo(() => {
|
const visibleSelectedKeys = useMemo(() => {
|
||||||
@@ -322,18 +344,19 @@ export const QueueTray = memo(function QueueTray({ onClose, embedded = false }:
|
|||||||
setVisibleEntries(nextEntries);
|
setVisibleEntries(nextEntries);
|
||||||
clearDragAfterReorderCommit();
|
clearDragAfterReorderCommit();
|
||||||
commitNativeMove(
|
commitNativeMove(
|
||||||
baseOffset + from,
|
baseOffsetRef.current + from,
|
||||||
baseOffset + to,
|
baseOffsetRef.current + to,
|
||||||
nextEntries.map((entry) => entry.track)
|
nextEntries.map((entry) => entry.track)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[baseOffset, clearDragAfterReorderCommit, clearDragState, commitNativeMove, setVisibleEntries]
|
[clearDragAfterReorderCommit, clearDragState, commitNativeMove, setVisibleEntries]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onDragArm = useCallback(
|
const onDragArm = useCallback(
|
||||||
(entryKey: string) => {
|
(entryKey: string) => {
|
||||||
dragInFlightRef.current = true;
|
dragInFlightRef.current = true;
|
||||||
const indexMap = indexQueueEntriesByKey(entriesRef.current);
|
const snapshot = entriesRef.current;
|
||||||
|
const indexMap = indexQueueEntriesByKey(snapshot);
|
||||||
const currentIndex = indexMap[entryKey] ?? -1;
|
const currentIndex = indexMap[entryKey] ?? -1;
|
||||||
|
|
||||||
runOnUI(
|
runOnUI(
|
||||||
@@ -343,33 +366,50 @@ export const QueueTray = memo(function QueueTray({ onClose, embedded = false }:
|
|||||||
start: SharedValue<number>,
|
start: SharedValue<number>,
|
||||||
target: SharedValue<number>,
|
target: SharedValue<number>,
|
||||||
sharedIndexMap: SharedValue<QueueIndexByKey>,
|
sharedIndexMap: SharedValue<QueueIndexByKey>,
|
||||||
|
sharedCount: SharedValue<number>,
|
||||||
armedKey: string,
|
armedKey: string,
|
||||||
armedIndex: number,
|
armedIndex: number,
|
||||||
nextIndexMap: QueueIndexByKey
|
nextIndexMap: QueueIndexByKey,
|
||||||
|
armedCount: number
|
||||||
) => {
|
) => {
|
||||||
'worklet';
|
'worklet';
|
||||||
if (!active.value || key.value !== armedKey) return;
|
if (!active.value || key.value !== armedKey) return;
|
||||||
sharedIndexMap.value = nextIndexMap;
|
sharedIndexMap.value = nextIndexMap;
|
||||||
|
sharedCount.value = armedCount;
|
||||||
start.value = armedIndex;
|
start.value = armedIndex;
|
||||||
target.value = armedIndex;
|
target.value = armedIndex;
|
||||||
}
|
}
|
||||||
)(dActive, dKey, dStart, dTarget, dIndexByKey, entryKey, currentIndex, indexMap);
|
)(
|
||||||
|
dActive,
|
||||||
|
dKey,
|
||||||
|
dStart,
|
||||||
|
dTarget,
|
||||||
|
dIndexByKey,
|
||||||
|
dCount,
|
||||||
|
entryKey,
|
||||||
|
currentIndex,
|
||||||
|
indexMap,
|
||||||
|
snapshot.length
|
||||||
|
);
|
||||||
playHaptic('queueLift');
|
playHaptic('queueLift');
|
||||||
},
|
},
|
||||||
[dActive, dIndexByKey, dKey, dStart, dTarget]
|
[dActive, dCount, dIndexByKey, dKey, dStart, dTarget]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onDragAbort = useCallback(() => {
|
const onDragAbort = useCallback(() => {
|
||||||
dragInFlightRef.current = false;
|
dragInFlightRef.current = false;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Depends only on the entry key: entry count and indices are resolved at
|
||||||
|
// drag time through shared values / refs, so row gestures survive every
|
||||||
|
// queue mutation and rebuild only when FlashList recycles a row.
|
||||||
const makeDragGesture = useCallback(
|
const makeDragGesture = useCallback(
|
||||||
(
|
(entryKey: string): GestureType =>
|
||||||
longPress: boolean,
|
Gesture.Pan()
|
||||||
entryKey: string,
|
// Vertical-only so a horizontal swipe starting on the handle still
|
||||||
entryCount: number
|
// falls through to the row's SwipeableRow pan.
|
||||||
): GestureType => {
|
.activeOffsetY([-2, 2])
|
||||||
const gesture = Gesture.Pan()
|
.failOffsetX([-14, 14])
|
||||||
.onStart(() => {
|
.onStart(() => {
|
||||||
dStart.value = -1;
|
dStart.value = -1;
|
||||||
dTarget.value = -1;
|
dTarget.value = -1;
|
||||||
@@ -384,7 +424,7 @@ export const QueueTray = memo(function QueueTray({ onClose, embedded = false }:
|
|||||||
if (dStart.value < 0) return;
|
if (dStart.value < 0) return;
|
||||||
const nextTarget = clampLocal(
|
const nextTarget = clampLocal(
|
||||||
Math.round(dStart.value + event.translationY / QUEUE_ROW_HEIGHT),
|
Math.round(dStart.value + event.translationY / QUEUE_ROW_HEIGHT),
|
||||||
entryCount
|
dCount.value
|
||||||
);
|
);
|
||||||
if (nextTarget !== dTarget.value) {
|
if (nextTarget !== dTarget.value) {
|
||||||
dTarget.value = nextTarget;
|
dTarget.value = nextTarget;
|
||||||
@@ -425,11 +465,20 @@ export const QueueTray = memo(function QueueTray({ onClose, embedded = false }:
|
|||||||
dKey.value = '';
|
dKey.value = '';
|
||||||
dIndexByKey.value = {};
|
dIndexByKey.value = {};
|
||||||
runOnJS(onDragAbort)();
|
runOnJS(onDragAbort)();
|
||||||
});
|
}),
|
||||||
|
[
|
||||||
return longPress ? gesture.activateAfterLongPress(250) : gesture.minDistance(1);
|
dActive,
|
||||||
},
|
dCount,
|
||||||
[dActive, dIndexByKey, dKey, dSettling, dStart, dTarget, dTy, finishDrag, onDragAbort, onDragArm]
|
dIndexByKey,
|
||||||
|
dKey,
|
||||||
|
dSettling,
|
||||||
|
dStart,
|
||||||
|
dTarget,
|
||||||
|
dTy,
|
||||||
|
finishDrag,
|
||||||
|
onDragAbort,
|
||||||
|
onDragArm,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const runAndRefresh = useCallback(
|
const runAndRefresh = useCallback(
|
||||||
@@ -440,30 +489,36 @@ export const QueueTray = memo(function QueueTray({ onClose, embedded = false }:
|
|||||||
);
|
);
|
||||||
|
|
||||||
const jump = useCallback(
|
const jump = useCallback(
|
||||||
(localIndex: number) => {
|
(key: string) => {
|
||||||
runAndRefresh(jumpToQueueIndex(baseOffset + localIndex));
|
const localIndex = entriesRef.current.findIndex((entry) => entry.key === key);
|
||||||
|
if (localIndex < 0) return;
|
||||||
|
runAndRefresh(jumpToQueueIndex(baseOffsetRef.current + localIndex));
|
||||||
},
|
},
|
||||||
[baseOffset, runAndRefresh]
|
[runAndRefresh]
|
||||||
);
|
);
|
||||||
|
|
||||||
const playNext = useCallback(
|
const playNext = useCallback(
|
||||||
(localIndex: number) => {
|
(key: string) => {
|
||||||
|
const localIndex = entriesRef.current.findIndex((entry) => entry.key === key);
|
||||||
|
if (localIndex < 0) return;
|
||||||
const nextEntries = moveQueueEntry(entriesRef.current, localIndex, 0);
|
const nextEntries = moveQueueEntry(entriesRef.current, localIndex, 0);
|
||||||
setOptimisticEntries(nextEntries);
|
setOptimisticEntries(nextEntries);
|
||||||
runAndRefresh(requeueToTop(baseOffset + localIndex));
|
runAndRefresh(requeueToTop(baseOffsetRef.current + localIndex));
|
||||||
},
|
},
|
||||||
[baseOffset, runAndRefresh, setOptimisticEntries]
|
[runAndRefresh, setOptimisticEntries]
|
||||||
);
|
);
|
||||||
|
|
||||||
const remove = useCallback(
|
const remove = useCallback(
|
||||||
(localIndex: number) => {
|
(key: string) => {
|
||||||
const action = removeQueueEntryAt(entriesRef.current, localIndex, baseOffset);
|
const localIndex = entriesRef.current.findIndex((entry) => entry.key === key);
|
||||||
|
if (localIndex < 0) return;
|
||||||
|
const action = removeQueueEntryAt(entriesRef.current, localIndex, baseOffsetRef.current);
|
||||||
if (!action) return;
|
if (!action) return;
|
||||||
|
|
||||||
setOptimisticEntries(action.nextEntries);
|
setOptimisticEntries(action.nextEntries);
|
||||||
runAndRefresh(removeFromQueue(action.absoluteIndex, { updateMirror: false }));
|
runAndRefresh(removeFromQueue(action.absoluteIndex, { updateMirror: false }));
|
||||||
},
|
},
|
||||||
[baseOffset, runAndRefresh, setOptimisticEntries]
|
[runAndRefresh, setOptimisticEntries]
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggleSelect = useCallback((key: string) => {
|
const toggleSelect = useCallback((key: string) => {
|
||||||
@@ -534,11 +589,9 @@ export const QueueTray = memo(function QueueTray({ onClose, embedded = false }:
|
|||||||
);
|
);
|
||||||
|
|
||||||
const renderItem = useCallback(
|
const renderItem = useCallback(
|
||||||
({ item, index }: ListRenderItemInfo<QueueEntry>) => (
|
({ item }: ListRenderItemInfo<QueueEntry>) => (
|
||||||
<QueueRow
|
<QueueRow
|
||||||
entry={item}
|
entry={item}
|
||||||
entryCount={entries.length}
|
|
||||||
localIndex={index}
|
|
||||||
actionsEnabled={queueReady}
|
actionsEnabled={queueReady}
|
||||||
editMode={editMode}
|
editMode={editMode}
|
||||||
selected={visibleSelectedKeys.has(item.key)}
|
selected={visibleSelectedKeys.has(item.key)}
|
||||||
@@ -550,9 +603,9 @@ export const QueueTray = memo(function QueueTray({ onClose, embedded = false }:
|
|||||||
dKey={dKey}
|
dKey={dKey}
|
||||||
dSettling={dSettling}
|
dSettling={dSettling}
|
||||||
dIndexByKey={dIndexByKey}
|
dIndexByKey={dIndexByKey}
|
||||||
onJumpIndex={jump}
|
onJumpKey={jump}
|
||||||
onPlayNextIndex={playNext}
|
onPlayNextKey={playNext}
|
||||||
onRemoveIndex={remove}
|
onRemoveKey={remove}
|
||||||
onToggleSelectKey={toggleSelect}
|
onToggleSelectKey={toggleSelect}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@@ -565,7 +618,6 @@ export const QueueTray = memo(function QueueTray({ onClose, embedded = false }:
|
|||||||
dTarget,
|
dTarget,
|
||||||
dTy,
|
dTy,
|
||||||
editMode,
|
editMode,
|
||||||
entries.length,
|
|
||||||
jump,
|
jump,
|
||||||
makeDragGesture,
|
makeDragGesture,
|
||||||
playNext,
|
playNext,
|
||||||
@@ -758,16 +810,10 @@ const Artwork = memo(function Artwork({ uri, title }: { uri?: string; title?: st
|
|||||||
|
|
||||||
interface QueueRowProps {
|
interface QueueRowProps {
|
||||||
entry: QueueEntry;
|
entry: QueueEntry;
|
||||||
entryCount: number;
|
|
||||||
localIndex: number;
|
|
||||||
actionsEnabled: boolean;
|
actionsEnabled: boolean;
|
||||||
editMode: boolean;
|
editMode: boolean;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
makeDragGesture: (
|
makeDragGesture: (entryKey: string) => GestureType;
|
||||||
longPress: boolean,
|
|
||||||
entryKey: string,
|
|
||||||
entryCount: number
|
|
||||||
) => GestureType;
|
|
||||||
dStart: SharedValue<number>;
|
dStart: SharedValue<number>;
|
||||||
dTarget: SharedValue<number>;
|
dTarget: SharedValue<number>;
|
||||||
dTy: SharedValue<number>;
|
dTy: SharedValue<number>;
|
||||||
@@ -775,16 +821,14 @@ interface QueueRowProps {
|
|||||||
dKey: SharedValue<string>;
|
dKey: SharedValue<string>;
|
||||||
dSettling: SharedValue<boolean>;
|
dSettling: SharedValue<boolean>;
|
||||||
dIndexByKey: SharedValue<QueueIndexByKey>;
|
dIndexByKey: SharedValue<QueueIndexByKey>;
|
||||||
onJumpIndex: (localIndex: number) => void;
|
onJumpKey: (key: string) => void;
|
||||||
onPlayNextIndex: (localIndex: number) => void;
|
onPlayNextKey: (key: string) => void;
|
||||||
onRemoveIndex: (localIndex: number) => void;
|
onRemoveKey: (key: string) => void;
|
||||||
onToggleSelectKey: (key: string) => void;
|
onToggleSelectKey: (key: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QueueRow = memo(function QueueRow({
|
const QueueRow = memo(function QueueRow({
|
||||||
entry,
|
entry,
|
||||||
entryCount,
|
|
||||||
localIndex,
|
|
||||||
actionsEnabled,
|
actionsEnabled,
|
||||||
editMode,
|
editMode,
|
||||||
selected,
|
selected,
|
||||||
@@ -796,9 +840,9 @@ const QueueRow = memo(function QueueRow({
|
|||||||
dKey,
|
dKey,
|
||||||
dSettling,
|
dSettling,
|
||||||
dIndexByKey,
|
dIndexByKey,
|
||||||
onJumpIndex,
|
onJumpKey,
|
||||||
onPlayNextIndex,
|
onPlayNextKey,
|
||||||
onRemoveIndex,
|
onRemoveKey,
|
||||||
onToggleSelectKey,
|
onToggleSelectKey,
|
||||||
}: QueueRowProps) {
|
}: QueueRowProps) {
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
@@ -808,38 +852,35 @@ const QueueRow = memo(function QueueRow({
|
|||||||
const title = trackTitle(entry.track);
|
const title = trackTitle(entry.track);
|
||||||
const artist = trackArtist(entry.track);
|
const artist = trackArtist(entry.track);
|
||||||
|
|
||||||
const gesture = useMemo(
|
const gesture = useMemo(() => makeDragGesture(entryKey), [entryKey, makeDragGesture]);
|
||||||
() => makeDragGesture(!editMode, entryKey, entryCount),
|
|
||||||
[editMode, entryCount, entryKey, makeDragGesture]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onJump = useCallback(() => onJumpIndex(localIndex), [localIndex, onJumpIndex]);
|
const onJump = useCallback(() => onJumpKey(entryKey), [entryKey, onJumpKey]);
|
||||||
const onPlayNext = useCallback(
|
const onPlayNext = useCallback(() => onPlayNextKey(entryKey), [entryKey, onPlayNextKey]);
|
||||||
() => onPlayNextIndex(localIndex),
|
const onRemove = useCallback(() => onRemoveKey(entryKey), [entryKey, onRemoveKey]);
|
||||||
[localIndex, onPlayNextIndex]
|
|
||||||
);
|
|
||||||
const onRemove = useCallback(() => onRemoveIndex(localIndex), [localIndex, onRemoveIndex]);
|
|
||||||
const onToggleSelect = useCallback(
|
const onToggleSelect = useCallback(
|
||||||
() => onToggleSelectKey(entryKey),
|
() => onToggleSelectKey(entryKey),
|
||||||
[entryKey, onToggleSelectKey]
|
[entryKey, onToggleSelectKey]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Idle rows must stay near-free: the early-exit branch creates no timing
|
||||||
|
// animations, so scroll-time evaluations (mount/recycle) cost almost nothing.
|
||||||
|
// Row indices come from dIndexByKey, which is only populated while a drag is
|
||||||
|
// in flight — the idle branch never needs them.
|
||||||
const rowMotionStyle = useAnimatedStyle(() => {
|
const rowMotionStyle = useAnimatedStyle(() => {
|
||||||
if (!dActive.value || dStart.value < 0) {
|
if (!dActive.value || dStart.value < 0) {
|
||||||
return {
|
return {
|
||||||
transform: [
|
transform: [{ translateY: 0 }, { scale: 1 }],
|
||||||
{ translateY: withTiming(0, motion.quick) },
|
|
||||||
{ scale: withTiming(1, motion.quick) },
|
|
||||||
],
|
|
||||||
zIndex: 0,
|
zIndex: 0,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shadowOpacity: withTiming(0, motion.quick),
|
shadowOpacity: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentIndex = dIndexByKey.value[entryKey] ?? -1;
|
||||||
|
|
||||||
if (dKey.value === entryKey) {
|
if (dKey.value === entryKey) {
|
||||||
const currentIndex = dIndexByKey.value[entryKey] ?? localIndex;
|
const baseIndexDelta =
|
||||||
const baseIndexDelta = (currentIndex - dStart.value) * QUEUE_ROW_HEIGHT;
|
currentIndex < 0 ? 0 : (currentIndex - dStart.value) * QUEUE_ROW_HEIGHT;
|
||||||
return {
|
return {
|
||||||
transform: [
|
transform: [
|
||||||
{ translateY: dTy.value - baseIndexDelta },
|
{ translateY: dTy.value - baseIndexDelta },
|
||||||
@@ -851,36 +892,31 @@ const QueueRow = memo(function QueueRow({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentIndex = dIndexByKey.value[entryKey] ?? localIndex;
|
|
||||||
if (dSettling.value) {
|
if (dSettling.value) {
|
||||||
return {
|
return {
|
||||||
transform: [
|
transform: [{ translateY: withTiming(0, motion.quick) }, { scale: 1 }],
|
||||||
{ translateY: withTiming(0, motion.quick) },
|
|
||||||
{ scale: withTiming(1, motion.quick) },
|
|
||||||
],
|
|
||||||
zIndex: 0,
|
zIndex: 0,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shadowOpacity: withTiming(0, motion.quick),
|
shadowOpacity: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const start = dStart.value;
|
const start = dStart.value;
|
||||||
const target = dTarget.value;
|
const target = dTarget.value;
|
||||||
let shift = 0;
|
let shift = 0;
|
||||||
if (start < target && currentIndex > start && currentIndex <= target) {
|
if (currentIndex >= 0) {
|
||||||
shift = -QUEUE_ROW_HEIGHT;
|
if (start < target && currentIndex > start && currentIndex <= target) {
|
||||||
} else if (start > target && currentIndex >= target && currentIndex < start) {
|
shift = -QUEUE_ROW_HEIGHT;
|
||||||
shift = QUEUE_ROW_HEIGHT;
|
} else if (start > target && currentIndex >= target && currentIndex < start) {
|
||||||
|
shift = QUEUE_ROW_HEIGHT;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
transform: [
|
transform: [{ translateY: withTiming(shift, motion.quick) }, { scale: 1 }],
|
||||||
{ translateY: withTiming(shift, motion.quick) },
|
|
||||||
{ scale: withTiming(1, motion.quick) },
|
|
||||||
],
|
|
||||||
zIndex: 0,
|
zIndex: 0,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shadowOpacity: withTiming(0, motion.quick),
|
shadowOpacity: 0,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -925,7 +961,7 @@ const QueueRow = memo(function QueueRow({
|
|||||||
{artist}
|
{artist}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
{editMode ? (
|
{actionsEnabled ? (
|
||||||
<GestureDetector gesture={gesture}>
|
<GestureDetector gesture={gesture}>
|
||||||
<View
|
<View
|
||||||
style={styles.dragHandle}
|
style={styles.dragHandle}
|
||||||
@@ -976,7 +1012,6 @@ const QueueRow = memo(function QueueRow({
|
|||||||
return (
|
return (
|
||||||
<Animated.View style={[styles.rowOuter, rowMotionStyle]}>
|
<Animated.View style={[styles.rowOuter, rowMotionStyle]}>
|
||||||
<SwipeableRow
|
<SwipeableRow
|
||||||
dragGesture={gesture}
|
|
||||||
swipeRight={{ icon: 'play-skip-forward', color: colors.accent, onCommit: onPlayNext }}
|
swipeRight={{ icon: 'play-skip-forward', color: colors.accent, onCommit: onPlayNext }}
|
||||||
swipeLeft={{ icon: 'trash', color: colors.warning, onCommit: onRemove }}
|
swipeLeft={{ icon: 'trash', color: colors.warning, onCommit: onRemove }}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user