Animate file explorer refresh icon while fetching

This commit is contained in:
Mohamed Boudra
2026-02-12 14:08:30 +07:00
parent 3319c2d40c
commit f55d0cd25f
@@ -14,9 +14,13 @@ import {
import { ScrollView, Gesture, GestureDetector } from "react-native-gesture-handler"; import { ScrollView, Gesture, GestureDetector } from "react-native-gesture-handler";
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles"; import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
import Animated, { import Animated, {
cancelAnimation,
Easing,
runOnJS, runOnJS,
useAnimatedStyle, useAnimatedStyle,
useSharedValue, useSharedValue,
withRepeat,
withTiming,
} from "react-native-reanimated"; } from "react-native-reanimated";
import { Fonts } from "@/constants/theme"; import { Fonts } from "@/constants/theme";
import * as Clipboard from "expo-clipboard"; import * as Clipboard from "expo-clipboard";
@@ -294,6 +298,48 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
const handleRefresh = useCallback(() => { const handleRefresh = useCallback(() => {
void refetchExplorer(); void refetchExplorer();
}, [refetchExplorer]); }, [refetchExplorer]);
const refreshIconRotation = useSharedValue(0);
useEffect(() => {
if (isRefreshFetching) {
refreshIconRotation.value = 0;
refreshIconRotation.value = withRepeat(
withTiming(360, {
duration: 700,
easing: Easing.linear,
}),
-1,
false
);
return;
}
cancelAnimation(refreshIconRotation);
const remainder = refreshIconRotation.value % 360;
if (Math.abs(remainder) < 0.001) {
refreshIconRotation.value = 0;
return;
}
const remaining = 360 - remainder;
const duration = Math.max(80, Math.round((remaining / 360) * 700));
refreshIconRotation.value = withTiming(
360,
{
duration,
easing: Easing.linear,
},
(finished) => {
if (finished) {
refreshIconRotation.value = 0;
}
}
);
}, [isRefreshFetching, refreshIconRotation]);
const refreshIconAnimatedStyle = useAnimatedStyle(() => ({
transform: [{ rotate: `${refreshIconRotation.value}deg` }],
}));
const currentSortLabel = SORT_OPTIONS.find((opt) => opt.value === sortOption)?.label ?? "Name"; const currentSortLabel = SORT_OPTIONS.find((opt) => opt.value === sortOption)?.label ?? "Name";
@@ -644,14 +690,13 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
styles.iconButton, styles.iconButton,
(hovered || pressed) && styles.iconButtonHovered, (hovered || pressed) && styles.iconButtonHovered,
pressed && styles.iconButtonPressed, pressed && styles.iconButtonPressed,
isRefreshFetching && styles.iconButtonLoading,
]} ]}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel="Refresh files" accessibilityLabel="Refresh files"
> >
<View style={isRefreshFetching ? styles.refreshIconLoading : undefined}> <Animated.View style={[styles.refreshIcon, refreshIconAnimatedStyle]}>
<RotateCw size={16} color={theme.colors.foregroundMuted} /> <RotateCw size={16} color={theme.colors.foregroundMuted} />
</View> </Animated.View>
</Pressable> </Pressable>
<Pressable style={styles.sortButton} onPress={handleSortCycle}> <Pressable style={styles.sortButton} onPress={handleSortCycle}>
<Text style={styles.sortButtonText}>{currentSortLabel}</Text> <Text style={styles.sortButtonText}>{currentSortLabel}</Text>
@@ -682,14 +727,13 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
styles.iconButton, styles.iconButton,
(hovered || pressed) && styles.iconButtonHovered, (hovered || pressed) && styles.iconButtonHovered,
pressed && styles.iconButtonPressed, pressed && styles.iconButtonPressed,
isRefreshFetching && styles.iconButtonLoading,
]} ]}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel="Refresh files" accessibilityLabel="Refresh files"
> >
<View style={isRefreshFetching ? styles.refreshIconLoading : undefined}> <Animated.View style={[styles.refreshIcon, refreshIconAnimatedStyle]}>
<RotateCw size={16} color={theme.colors.foregroundMuted} /> <RotateCw size={16} color={theme.colors.foregroundMuted} />
</View> </Animated.View>
</Pressable> </Pressable>
<Pressable style={styles.sortButton} onPress={handleSortCycle}> <Pressable style={styles.sortButton} onPress={handleSortCycle}>
<Text style={styles.sortButtonText}>{currentSortLabel}</Text> <Text style={styles.sortButtonText}>{currentSortLabel}</Text>
@@ -1246,11 +1290,11 @@ const styles = StyleSheet.create((theme) => ({
opacity: 0.8, opacity: 0.8,
transform: [{ scale: 0.96 }], transform: [{ scale: 0.96 }],
}, },
iconButtonLoading: { refreshIcon: {
opacity: 0.8, width: 16,
}, height: 16,
refreshIconLoading: { alignItems: "center",
opacity: 0.55, justifyContent: "center",
}, },
previewContent: { previewContent: {
flex: 1, flex: 1,