diff --git a/src/app/apps/files.tsx b/src/app/apps/files.tsx
index dbaec41..db19095 100644
--- a/src/app/apps/files.tsx
+++ b/src/app/apps/files.tsx
@@ -87,11 +87,11 @@ const FilesPage = () => {
}
disabled={parentPath == null}
- onPress={() => setParams({ ...params, path: parentPath })}
+ onPress={() => setParams({ path: parentPath })}
/>
}
- onPress={() => setParams({ ...params, path: "" })}
+ onPress={() => setParams({ path: "" })}
/>
{
files={data}
onSelect={(file) => {
if (file.isDirectory) {
- setParams({ ...params, path: file.path });
+ setParams({ path: file.path });
} else {
setViewFile(file);
}
diff --git a/src/components/containers/AudioPlayer.tsx b/src/components/containers/AudioPlayer.tsx
index 9d4688c..a05f971 100644
--- a/src/components/containers/AudioPlayer.tsx
+++ b/src/components/containers/AudioPlayer.tsx
@@ -1,5 +1,5 @@
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 { MediaTags } from "@/types/mediaTags";
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 { FileItem } from "@/types/files";
import Input from "@ui/Input";
+import { useAsyncStorage } from "@/hooks/useAsyncStorage";
type Props = {
path: string;
@@ -32,11 +33,21 @@ const AudioPlayer = ({ path, uri }: Props) => {
const [curFileIdx, setFileIdx] = useState(-1);
const [status, setStatus] = useState(null);
const [mediaTags, setMediaTags] = useState(null);
+ const [playback, setPlayback] = useAsyncStorage("playback", {
+ repeat: false,
+ shuffle: false,
+ });
const filename = getFilename(decodeURIComponent(uri));
const playlist = useMemo(() => {
- return files.filter((f) => getFileType(f.name) === "audio");
- }, [files]);
+ let items = files.filter((f) => getFileType(f.name) === "audio");
+
+ if (playback.shuffle) {
+ items = items.sort(() => Math.random() - 0.5);
+ }
+
+ return items;
+ }, [files, playback.shuffle]);
const playIdx = (idx: number) => {
if (!playlist.length || idx < 0) {
@@ -49,12 +60,12 @@ const AudioPlayer = ({ path, uri }: Props) => {
}
};
- const playNext = (increment = 1) => {
- if (!playlist.length || curFileIdx < 0) {
+ const playNext = (increment = 1, startIdx?: number) => {
+ const curIdx = startIdx ?? curFileIdx;
+ if (!playlist.length || curIdx < 0) {
return;
}
-
- playIdx(curFileIdx + increment);
+ playIdx(curIdx + increment);
};
useEffect(() => {
@@ -65,12 +76,14 @@ const AudioPlayer = ({ path, uri }: Props) => {
const fileIdx = playlist.findIndex((file) => path === file.path);
setFileIdx(fileIdx);
- const onNext = () => playIdx(fileIdx + 1);
+ const onNext = () => playNext(1, fileIdx);
async function play() {
try {
const { sound } = await Audio.Sound.createAsync({ uri });
soundRef.current = sound;
+
+ sound.setIsLoopingAsync(playback.repeat);
sound.setOnPlaybackStatusUpdate((st: AVPlaybackStatusSuccess) => {
setStatus(st as any);
@@ -93,31 +106,32 @@ const AudioPlayer = ({ path, uri }: Props) => {
}
}
- // function loadMediaTags() {
- // const tagsReader = new jsmediatags.Reader(uri + "&dl=true");
- // setMediaTags(null);
+ function loadMediaTags() {
+ const tagsReader = new jsmediatags.Reader(uri + "&dl=true");
+ setMediaTags(null);
- // tagsReader.read({
- // onSuccess: (result: any) => {
- // const mediaTagsResult = { ...result };
+ tagsReader.read({
+ onSuccess: (result: any) => {
+ const mediaTagsResult = { ...result };
- // if (result?.tags?.picture) {
- // const { data, format } = result.tags.picture;
- // let base64String = "";
- // for (let i = 0; i < data.length; i++) {
- // base64String += String.fromCharCode(data[i]);
- // }
- // mediaTagsResult.picture = `data:${format};base64,${base64encode(
- // base64String
- // )}`;
- // delete data?.tags?.picture;
- // }
+ if (result?.tags?.picture) {
+ const { data, format } = result.tags.picture;
+ let base64String = "";
+ for (let i = 0; i < data.length; i++) {
+ base64String += String.fromCharCode(data[i]);
+ }
+ mediaTagsResult.picture = `data:${format};base64,${base64encode(
+ base64String
+ )}`;
+ delete data?.tags?.picture;
+ }
- // setMediaTags(mediaTagsResult);
- // },
- // });
- // }
+ setMediaTags(mediaTagsResult);
+ },
+ });
+ }
+ loadMediaTags();
play();
return () => {
@@ -126,6 +140,12 @@ const AudioPlayer = ({ path, uri }: Props) => {
};
}, [uri, path, playlist]);
+ useEffect(() => {
+ if (status?.isLoaded) {
+ soundRef.current?.setIsLoopingAsync(playback.repeat);
+ }
+ }, [playback.repeat, status?.isLoaded]);
+
return (
@@ -160,42 +180,62 @@ const AudioPlayer = ({ path, uri }: Props) => {
) : null}
- {
- if (!soundRef.current) {
- return;
+
+ {
+ if (!soundRef.current) {
+ return;
+ }
- if (!status?.isPlaying) {
- await soundRef.current.playAsync();
- }
+ if (!status?.isPlaying) {
+ await soundRef.current.playAsync();
+ }
- const [progress] = value;
- const pos = (progress / 100.0) * (status?.durationMillis || 0);
- soundRef.current.setPositionAsync(pos);
- }}
- />
+ const [progress] = value;
+ const pos = (progress / 100.0) * (status?.durationMillis || 0);
+ soundRef.current.setPositionAsync(pos);
+ }}
+ />
+
+
+ {formatTime(status?.positionMillis || 0)}
+
+
+ {formatTime(status?.durationMillis || 0)}
+
+
+
}
- iconClassName="text-[32px] md:text-[40px]"
+ icon={}
+ 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 })}
+ />
+ }
+ iconClassName="text-[24px] md:text-[32px] text-white"
+ variant="ghost"
className="w-16 h-16 md:w-20 md:h-20 rounded-full"
onPress={() => playNext(-1)}
/>
}
- 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"
onPress={() => {
if (!soundRef.current) {
@@ -209,11 +249,21 @@ const AudioPlayer = ({ path, uri }: Props) => {
}}
/>
}
- iconClassName="text-[32px] md:text-[40px]"
+ icon={}
+ iconClassName="text-[24px] md:text-[32px] text-white"
+ variant="ghost"
className="w-16 h-16 md:w-20 md:h-20 rounded-full"
onPress={() => playNext()}
/>
+ }
+ 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 })}
+ />
@@ -229,14 +279,29 @@ type PlaylistProps = {
playIdx: (idx: number) => void;
};
+const PLAYLIST_ITEM_HEIGHT = 49;
+
const Playlist = ({ playlist, currentIdx, playIdx }: PlaylistProps) => {
+ const containerRef = useRef();
const [search, setSearch] = useState("");
- const list = useMemo(() => {
- if (!search?.length) {
- return playlist;
+ useEffect(() => {
+ if (currentIdx >= 0) {
+ 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())
);
}, [search, playlist]);
@@ -257,15 +322,22 @@ const Playlist = ({ playlist, currentIdx, playIdx }: PlaylistProps) => {
i.path}
- renderItem={({ item, index }) => (
+ renderItem={({ item }) => (
playIdx(index)}
+ isCurrent={item.idx === currentIdx}
+ onPress={() => playIdx(item.idx)}
/>
)}
+ onScrollToIndexFailed={({ index }) => {
+ containerRef.current?.scrollToOffset({
+ offset: (index ?? 0) * PLAYLIST_ITEM_HEIGHT,
+ animated: true,
+ });
+ }}
/>
);
@@ -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;
diff --git a/src/hooks/useAsyncStorage.ts b/src/hooks/useAsyncStorage.ts
index e59829c..189bec0 100644
--- a/src/hooks/useAsyncStorage.ts
+++ b/src/hooks/useAsyncStorage.ts
@@ -1,7 +1,7 @@
import { useEffect, useState } from "react";
import AsyncStorage from "@react-native-async-storage/async-storage";
-export const useAsyncStorage = (key: string, defaultValue: T) => {
+export const useAsyncStorage = (key: string, defaultValue: T) => {
const [value, setValue] = useState(defaultValue);
useEffect(() => {
@@ -19,11 +19,12 @@ export const useAsyncStorage = (key: string, defaultValue: T) => {
init();
}, [key]);
- const setValueToAsyncStorage = async (newValue: T) => {
+ const setValueToAsyncStorage = async (newValue: Partial) => {
try {
- const jsonValue = JSON.stringify(newValue);
+ const values = { ...value, ...newValue };
+ const jsonValue = JSON.stringify(values);
await AsyncStorage.setItem(key, jsonValue);
- setValue(newValue);
+ setValue(values);
} catch (e) {
console.warn(e);
}