mirror of
https://github.com/khairul169/home-lab.git
synced 2025-04-28 16:49:36 +07:00
feat: update audioplayer
This commit is contained in:
parent
523f9a850c
commit
ba474ceffe
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user