feat: update audioplayer

This commit is contained in:
Khairul Hidayat 2024-03-24 00:21:31 +07:00
parent 523f9a850c
commit ba474ceffe
3 changed files with 149 additions and 70 deletions

View File

@ -87,11 +87,11 @@ const FilesPage = () => {
<ActionButton <ActionButton
icon={<Ionicons name="chevron-back" />} icon={<Ionicons name="chevron-back" />}
disabled={parentPath == null} disabled={parentPath == null}
onPress={() => setParams({ ...params, path: parentPath })} onPress={() => setParams({ path: parentPath })}
/> />
<ActionButton <ActionButton
icon={<Ionicons name="home-outline" />} icon={<Ionicons name="home-outline" />}
onPress={() => setParams({ ...params, path: "" })} onPress={() => setParams({ path: "" })}
/> />
<Input <Input
placeholder="/" placeholder="/"
@ -107,7 +107,7 @@ const FilesPage = () => {
files={data} files={data}
onSelect={(file) => { onSelect={(file) => {
if (file.isDirectory) { if (file.isDirectory) {
setParams({ ...params, path: file.path }); setParams({ path: file.path });
} else { } else {
setViewFile(file); setViewFile(file);
} }

View File

@ -1,5 +1,5 @@
import { AVPlaybackStatusSuccess, Audio } from "expo-av"; import { AVPlaybackStatusSuccess, Audio } from "expo-av";
import { useEffect, useMemo, useRef, useState } from "react"; import { forwardRef, useEffect, useMemo, useRef, useState } from "react";
import jsmediatags from "jsmediatags/build2/jsmediatags"; import jsmediatags from "jsmediatags/build2/jsmediatags";
import { MediaTags } from "@/types/mediaTags"; import { MediaTags } from "@/types/mediaTags";
import Box from "../ui/Box"; import Box from "../ui/Box";
@ -20,6 +20,7 @@ import { Slider } from "@miblanchard/react-native-slider";
import bgImage from "@/assets/images/audioplayer-bg.jpeg"; import bgImage from "@/assets/images/audioplayer-bg.jpeg";
import { FileItem } from "@/types/files"; import { FileItem } from "@/types/files";
import Input from "@ui/Input"; import Input from "@ui/Input";
import { useAsyncStorage } from "@/hooks/useAsyncStorage";
type Props = { type Props = {
path: string; path: string;
@ -32,11 +33,21 @@ const AudioPlayer = ({ path, uri }: Props) => {
const [curFileIdx, setFileIdx] = useState(-1); const [curFileIdx, setFileIdx] = useState(-1);
const [status, setStatus] = useState<AVPlaybackStatusSuccess | null>(null); const [status, setStatus] = useState<AVPlaybackStatusSuccess | null>(null);
const [mediaTags, setMediaTags] = useState<MediaTags | null>(null); const [mediaTags, setMediaTags] = useState<MediaTags | null>(null);
const [playback, setPlayback] = useAsyncStorage("playback", {
repeat: false,
shuffle: false,
});
const filename = getFilename(decodeURIComponent(uri)); const filename = getFilename(decodeURIComponent(uri));
const playlist = useMemo(() => { const playlist = useMemo(() => {
return files.filter((f) => getFileType(f.name) === "audio"); let items = files.filter((f) => getFileType(f.name) === "audio");
}, [files]);
if (playback.shuffle) {
items = items.sort(() => Math.random() - 0.5);
}
return items;
}, [files, playback.shuffle]);
const playIdx = (idx: number) => { const playIdx = (idx: number) => {
if (!playlist.length || idx < 0) { if (!playlist.length || idx < 0) {
@ -49,12 +60,12 @@ const AudioPlayer = ({ path, uri }: Props) => {
} }
}; };
const playNext = (increment = 1) => { const playNext = (increment = 1, startIdx?: number) => {
if (!playlist.length || curFileIdx < 0) { const curIdx = startIdx ?? curFileIdx;
if (!playlist.length || curIdx < 0) {
return; return;
} }
playIdx(curIdx + increment);
playIdx(curFileIdx + increment);
}; };
useEffect(() => { useEffect(() => {
@ -65,12 +76,14 @@ const AudioPlayer = ({ path, uri }: Props) => {
const fileIdx = playlist.findIndex((file) => path === file.path); const fileIdx = playlist.findIndex((file) => path === file.path);
setFileIdx(fileIdx); setFileIdx(fileIdx);
const onNext = () => playIdx(fileIdx + 1); const onNext = () => playNext(1, fileIdx);
async function play() { async function play() {
try { try {
const { sound } = await Audio.Sound.createAsync({ uri }); const { sound } = await Audio.Sound.createAsync({ uri });
soundRef.current = sound; soundRef.current = sound;
sound.setIsLoopingAsync(playback.repeat);
sound.setOnPlaybackStatusUpdate((st: AVPlaybackStatusSuccess) => { sound.setOnPlaybackStatusUpdate((st: AVPlaybackStatusSuccess) => {
setStatus(st as any); setStatus(st as any);
@ -93,31 +106,32 @@ const AudioPlayer = ({ path, uri }: Props) => {
} }
} }
// function loadMediaTags() { function loadMediaTags() {
// const tagsReader = new jsmediatags.Reader(uri + "&dl=true"); const tagsReader = new jsmediatags.Reader(uri + "&dl=true");
// setMediaTags(null); setMediaTags(null);
// tagsReader.read({ tagsReader.read({
// onSuccess: (result: any) => { onSuccess: (result: any) => {
// const mediaTagsResult = { ...result }; const mediaTagsResult = { ...result };
// if (result?.tags?.picture) { if (result?.tags?.picture) {
// const { data, format } = result.tags.picture; const { data, format } = result.tags.picture;
// let base64String = ""; let base64String = "";
// for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
// base64String += String.fromCharCode(data[i]); base64String += String.fromCharCode(data[i]);
// } }
// mediaTagsResult.picture = `data:${format};base64,${base64encode( mediaTagsResult.picture = `data:${format};base64,${base64encode(
// base64String base64String
// )}`; )}`;
// delete data?.tags?.picture; delete data?.tags?.picture;
// } }
// setMediaTags(mediaTagsResult); setMediaTags(mediaTagsResult);
// }, },
// }); });
// } }
loadMediaTags();
play(); play();
return () => { return () => {
@ -126,6 +140,12 @@ const AudioPlayer = ({ path, uri }: Props) => {
}; };
}, [uri, path, playlist]); }, [uri, path, playlist]);
useEffect(() => {
if (status?.isLoaded) {
soundRef.current?.setIsLoopingAsync(playback.repeat);
}
}, [playback.repeat, status?.isLoaded]);
return ( return (
<HStack className="flex-1 items-stretch"> <HStack className="flex-1 items-stretch">
<Box className="flex-1 relative overflow-hidden"> <Box className="flex-1 relative overflow-hidden">
@ -160,17 +180,18 @@ const AudioPlayer = ({ path, uri }: Props) => {
</Text> </Text>
) : null} ) : null}
<Box className="w-full max-w-3xl mx-auto my-4 md:my-8">
<Slider <Slider
minimumValue={0} minimumValue={0}
maximumValue={100} maximumValue={100}
value={ value={
((status?.positionMillis || 0) / (status?.durationMillis || 1)) * ((status?.positionMillis || 0) /
(status?.durationMillis || 1)) *
100 100
} }
thumbStyle={cn("bg-blue-500")} thumbStyle={cn("bg-blue-500")}
trackStyle={cn("bg-white/30 rounded-full h-2")} trackStyle={cn("bg-white/30 rounded-full h-2")}
minimumTrackTintColor="#6366F1" minimumTrackTintColor="#6366F1"
containerStyle={cn("w-full max-w-3xl mx-auto my-4 md:my-8")}
onValueChange={async (value) => { onValueChange={async (value) => {
if (!soundRef.current) { if (!soundRef.current) {
return; return;
@ -185,17 +206,36 @@ const AudioPlayer = ({ path, uri }: Props) => {
soundRef.current.setPositionAsync(pos); soundRef.current.setPositionAsync(pos);
}} }}
/> />
<HStack className="justify-between">
<Text className="text-white">
{formatTime(status?.positionMillis || 0)}
</Text>
<Text className="text-white">
{formatTime(status?.durationMillis || 0)}
</Text>
</HStack>
</Box>
<HStack className="gap-4"> <HStack className="gap-4">
<Button <Button
icon={<Ionicons name="chevron-back" />} icon={<Ionicons name="repeat" />}
iconClassName="text-[32px] md:text-[40px]" iconClassName={`text-[24px] md:text-[32px] text-white ${
playback.repeat ? "opacity-100" : "opacity-50"
}`}
variant="ghost"
className="w-16 h-16 md:w-20 md:h-20 rounded-full"
onPress={() => setPlayback({ repeat: !playback.repeat })}
/>
<Button
icon={<Ionicons name="play-back" />}
iconClassName="text-[24px] md:text-[32px] text-white"
variant="ghost"
className="w-16 h-16 md:w-20 md:h-20 rounded-full" className="w-16 h-16 md:w-20 md:h-20 rounded-full"
onPress={() => playNext(-1)} onPress={() => playNext(-1)}
/> />
<Button <Button
icon={<Ionicons name={status?.isPlaying ? "pause" : "play"} />} icon={<Ionicons name={status?.isPlaying ? "pause" : "play"} />}
iconClassName="text-[40px] md:text-[48px]" iconClassName="text-[32px] md:text-[36px]"
className="w-20 h-20 md:w-24 md:h-24 rounded-full" className="w-20 h-20 md:w-24 md:h-24 rounded-full"
onPress={() => { onPress={() => {
if (!soundRef.current) { if (!soundRef.current) {
@ -209,11 +249,21 @@ const AudioPlayer = ({ path, uri }: Props) => {
}} }}
/> />
<Button <Button
icon={<Ionicons name="chevron-forward" />} icon={<Ionicons name="play-forward" />}
iconClassName="text-[32px] md:text-[40px]" iconClassName="text-[24px] md:text-[32px] text-white"
variant="ghost"
className="w-16 h-16 md:w-20 md:h-20 rounded-full" className="w-16 h-16 md:w-20 md:h-20 rounded-full"
onPress={() => playNext()} onPress={() => playNext()}
/> />
<Button
icon={<Ionicons name="shuffle" />}
iconClassName={`text-[24px] md:text-[32px] text-white ${
playback.shuffle ? "opacity-100" : "opacity-50"
}`}
variant="ghost"
className="w-16 h-16 md:w-20 md:h-20 rounded-full"
onPress={() => setPlayback({ shuffle: !playback.shuffle })}
/>
</HStack> </HStack>
</Box> </Box>
</Box> </Box>
@ -229,14 +279,29 @@ type PlaylistProps = {
playIdx: (idx: number) => void; playIdx: (idx: number) => void;
}; };
const PLAYLIST_ITEM_HEIGHT = 49;
const Playlist = ({ playlist, currentIdx, playIdx }: PlaylistProps) => { const Playlist = ({ playlist, currentIdx, playIdx }: PlaylistProps) => {
const containerRef = useRef<any>();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const list = useMemo(() => { useEffect(() => {
if (!search?.length) { if (currentIdx >= 0) {
return playlist; containerRef.current?.scrollToIndex({
index: currentIdx,
animated: true,
});
} }
return playlist.filter((i) => }, [currentIdx]);
const list = useMemo(() => {
const items = playlist.map((i, idx) => ({ ...i, idx }));
if (!search?.length) {
return items;
}
return items.filter((i) =>
i.name.toLowerCase().includes(search.toLowerCase()) i.name.toLowerCase().includes(search.toLowerCase())
); );
}, [search, playlist]); }, [search, playlist]);
@ -257,15 +322,22 @@ const Playlist = ({ playlist, currentIdx, playIdx }: PlaylistProps) => {
</HStack> </HStack>
<FlatList <FlatList
ref={containerRef}
data={list} data={list}
keyExtractor={(i) => i.path} keyExtractor={(i) => i.path}
renderItem={({ item, index }) => ( renderItem={({ item }) => (
<PlaylistItem <PlaylistItem
file={item} file={item}
isCurrent={index === currentIdx} isCurrent={item.idx === currentIdx}
onPress={() => playIdx(index)} onPress={() => playIdx(item.idx)}
/> />
)} )}
onScrollToIndexFailed={({ index }) => {
containerRef.current?.scrollToOffset({
offset: (index ?? 0) * PLAYLIST_ITEM_HEIGHT,
animated: true,
});
}}
/> />
</Box> </Box>
); );
@ -293,4 +365,10 @@ const PlaylistItem = ({ file, isCurrent, onPress }: PlaylistItemProps) => {
); );
}; };
function formatTime(time: number) {
const minutes = Math.floor(time / 60 / 1000);
const seconds = Math.floor((time / 1000) % 60);
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
}
export default AudioPlayer; export default AudioPlayer;

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import AsyncStorage from "@react-native-async-storage/async-storage"; import AsyncStorage from "@react-native-async-storage/async-storage";
export const useAsyncStorage = <T = any>(key: string, defaultValue: T) => { export const useAsyncStorage = <T = object>(key: string, defaultValue: T) => {
const [value, setValue] = useState<T>(defaultValue); const [value, setValue] = useState<T>(defaultValue);
useEffect(() => { useEffect(() => {
@ -19,11 +19,12 @@ export const useAsyncStorage = <T = any>(key: string, defaultValue: T) => {
init(); init();
}, [key]); }, [key]);
const setValueToAsyncStorage = async (newValue: T) => { const setValueToAsyncStorage = async (newValue: Partial<T>) => {
try { try {
const jsonValue = JSON.stringify(newValue); const values = { ...value, ...newValue };
const jsonValue = JSON.stringify(values);
await AsyncStorage.setItem(key, jsonValue); await AsyncStorage.setItem(key, jsonValue);
setValue(newValue); setValue(values);
} catch (e) { } catch (e) {
console.warn(e); console.warn(e);
} }