1import { useAudioPlayer, useAudioPlayerStatus } from "expo-audio";
2import { useState } from "react";
3import { Pressable, View } from "react-native";
4import { AppText as Text } from "./AppText";
5
6function timestamp(seconds: number) {
7 const value = Number.isFinite(seconds) ? Math.max(0, Math.floor(seconds)) : 0;
8 return `${Math.floor(value / 60)}:${String(value % 60).padStart(2, "0")}`;
9}
10
11export function AudioFilePreview(props: { uri: string; onRetry: () => void }) {
12 const player = useAudioPlayer({ uri: props.uri }, { updateInterval: 500 });
13 const status = useAudioPlayerStatus(player);
14 const [seekError, setSeekError] = useState(false);
15 const seek = (seconds: number, play = false) => {
16 setSeekError(false);
17 void player
18 .seekTo(seconds)
19 .then(() => {
20 if (play) player.play();
21 })
22 .catch(() => setSeekError(true));
23 };
24 return (
25 <View className="flex-1 items-center justify-center gap-5 p-6">
26 <Text className="text-foreground-muted">
27 {timestamp(status.currentTime)} / {timestamp(status.duration)}
28 </Text>
29 <View className="flex-row items-center gap-4">
30 <Pressable
31 accessibilityRole="button"
32 accessibilityLabel="Back 15 seconds"
33 disabled={!status.isLoaded}
34 onPress={() => seek(Math.max(0, status.currentTime - 15))}
35 className="p-4"
36 >
37 <Text className="text-foreground">−15s</Text>
38 </Pressable>
39 <Pressable
40 accessibilityRole="button"
41 disabled={!status.isLoaded}
42 onPress={() => {
43 if (status.playing) player.pause();
44 else if (status.didJustFinish || status.currentTime >= status.duration) seek(0, true);
45 else player.play();
46 }}
47 className="rounded-xl bg-subtle px-6 py-4"
48 >
49 <Text className="text-foreground">
50 {!status.isLoaded ? "Loading…" : status.playing ? "Pause" : "Play"}
51 </Text>
52 </Pressable>
53 <Pressable
54 accessibilityRole="button"
55 accessibilityLabel="Forward 15 seconds"
56 disabled={!status.isLoaded}
57 onPress={() => seek(Math.min(status.duration, status.currentTime + 15))}
58 className="p-4"
59 >
60 <Text className="text-foreground">+15s</Text>
61 </Pressable>
62 </View>
63 {status.error || seekError ? (
64 <View className="items-center gap-3">
65 <Text className="text-center text-foreground">
66 This audio could not be played. Try again or save it to open in another app.
67 </Text>
68 <Pressable accessibilityRole="button" onPress={props.onRetry} className="p-3">
69 <Text className="text-foreground">Try again</Text>
70 </Pressable>
71 </View>
72 ) : null}
73 </View>
74 );
75}