Animate file explorer refresh icon while fetching
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user