diff --git a/src/app/_layout.tsx b/src/app/_layout.tsx
index 54fa87a..c299457 100644
--- a/src/app/_layout.tsx
+++ b/src/app/_layout.tsx
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react";
-import { Slot, Stack, router, usePathname } from "expo-router";
+import { Stack, router, usePathname } from "expo-router";
import { QueryClientProvider } from "react-query";
import queryClient from "@/lib/queryClient";
import { View } from "react-native";
@@ -12,6 +12,9 @@ import { useStore } from "zustand";
import authStore from "@/stores/authStore";
import { toastStore } from "@/stores/toastStore";
import Dialog from "@ui/Dialog";
+import AudioPlayerProvider from "@/components/containers/AudioPlayerProvider";
+import AudioPlayer from "@/components/pages/music/AudioPlayer";
+import MiniPlayer from "@/components/pages/music/MiniPlayer";
const RootLayout = () => {
const insets = useSafeAreaInsets();
@@ -40,13 +43,15 @@ const RootLayout = () => {
+
+
{
@@ -54,6 +59,7 @@ const RootLayout = () => {
}}
/>
+
);
};
diff --git a/src/app/apps/files.tsx b/src/app/apps/files.tsx
index db19095..3907a28 100644
--- a/src/app/apps/files.tsx
+++ b/src/app/apps/files.tsx
@@ -19,6 +19,8 @@ import Head from "@/components/utility/Head";
import Container from "@ui/Container";
import FileUpload from "@/components/pages/files/FileUpload";
import ActionButton from "@/components/pages/files/ActionButton";
+import { getFileType } from "@/lib/utils";
+import { audioPlayer } from "@/stores/audioPlayerStore";
const FilesPage = () => {
const { isLoggedIn } = useAuth();
@@ -105,12 +107,17 @@ const FilesPage = () => {
{
+ onSelect={(file, idx) => {
if (file.isDirectory) {
- setParams({ path: file.path });
- } else {
- setViewFile(file);
+ return setParams({ path: file.path });
}
+
+ const fileType = getFileType(file.path);
+ if (fileType === "audio") {
+ return audioPlayer.play(data, idx);
+ }
+
+ setViewFile(file);
}}
/>
diff --git a/src/components/containers/AudioPlayer.tsx b/src/components/containers/AudioPlayer.tsx
deleted file mode 100644
index a05f971..0000000
--- a/src/components/containers/AudioPlayer.tsx
+++ /dev/null
@@ -1,374 +0,0 @@
-import { AVPlaybackStatusSuccess, Audio } from "expo-av";
-import { forwardRef, useEffect, useMemo, useRef, useState } from "react";
-import jsmediatags from "jsmediatags/build2/jsmediatags";
-import { MediaTags } from "@/types/mediaTags";
-import Box from "../ui/Box";
-import Text from "../ui/Text";
-import {
- base64encode,
- cn,
- encodeUrl,
- getFileType,
- getFilename,
-} from "@/lib/utils";
-import { FlatList, Image, Pressable } from "react-native";
-import { useFilesContext } from "../pages/files/FilesContext";
-import { HStack } from "../ui/Stack";
-import Button from "../ui/Button";
-import { Ionicons } from "../ui/Icons";
-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;
- uri: string;
-};
-
-const AudioPlayer = ({ path, uri }: Props) => {
- const { files, setViewFile } = useFilesContext();
- const soundRef = useRef(null);
- 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(() => {
- 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) {
- return;
- }
-
- const file = playlist[idx % playlist.length];
- if (file) {
- setViewFile(file);
- }
- };
-
- const playNext = (increment = 1, startIdx?: number) => {
- const curIdx = startIdx ?? curFileIdx;
- if (!playlist.length || curIdx < 0) {
- return;
- }
- playIdx(curIdx + increment);
- };
-
- useEffect(() => {
- if (!playlist?.length) {
- return;
- }
-
- const fileIdx = playlist.findIndex((file) => path === file.path);
- setFileIdx(fileIdx);
-
- 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);
-
- if (st.didJustFinish) {
- onNext();
- }
- });
-
- await sound.playAsync();
-
- if (soundRef.current !== sound) {
- await sound.unloadAsync();
- }
- } catch (err) {
- if (err instanceof DOMException) {
- if (err.name === "NotSupportedError") {
- setTimeout(onNext, 3000);
- }
- }
- }
- }
-
- function loadMediaTags() {
- const tagsReader = new jsmediatags.Reader(uri + "&dl=true");
- setMediaTags(null);
-
- 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;
- }
-
- setMediaTags(mediaTagsResult);
- },
- });
- }
-
- loadMediaTags();
- play();
-
- return () => {
- soundRef.current?.unloadAsync();
- soundRef.current = null;
- };
- }, [uri, path, playlist]);
-
- useEffect(() => {
- if (status?.isLoaded) {
- soundRef.current?.setIsLoopingAsync(playback.repeat);
- }
- }, [playback.repeat, status?.isLoaded]);
-
- return (
-
-
-
-
-
-
-
- {mediaTags?.picture ? (
-
- ) : null}
-
- Now Playing
-
- {mediaTags?.tags?.title || filename}
-
- {mediaTags?.tags?.artist ? (
-
- {mediaTags.tags.artist}
-
- ) : null}
-
-
- {
- if (!soundRef.current) {
- return;
- }
-
- if (!status?.isPlaying) {
- await soundRef.current.playAsync();
- }
-
- 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-[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-[32px] md:text-[36px]"
- className="w-20 h-20 md:w-24 md:h-24 rounded-full"
- onPress={() => {
- if (!soundRef.current) {
- return;
- }
- if (status?.isPlaying) {
- soundRef.current?.pauseAsync();
- } else {
- soundRef.current?.playAsync();
- }
- }}
- />
- }
- 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 })}
- />
-
-
-
-
-
-
- );
-};
-
-type PlaylistProps = {
- playlist: FileItem[];
- currentIdx: number;
- playIdx: (idx: number) => void;
-};
-
-const PLAYLIST_ITEM_HEIGHT = 49;
-
-const Playlist = ({ playlist, currentIdx, playIdx }: PlaylistProps) => {
- const containerRef = useRef();
- const [search, setSearch] = useState("");
-
- useEffect(() => {
- if (currentIdx >= 0) {
- containerRef.current?.scrollToIndex({
- index: currentIdx,
- animated: true,
- });
- }
- }, [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]);
-
- return (
-
-
-
- Playlist
-
-
-
-
- i.path}
- renderItem={({ item }) => (
- playIdx(item.idx)}
- />
- )}
- onScrollToIndexFailed={({ index }) => {
- containerRef.current?.scrollToOffset({
- offset: (index ?? 0) * PLAYLIST_ITEM_HEIGHT,
- animated: true,
- });
- }}
- />
-
- );
-};
-
-type PlaylistItemProps = {
- file: FileItem;
- isCurrent: boolean;
- onPress: () => void;
-};
-
-const PlaylistItem = ({ file, isCurrent, onPress }: PlaylistItemProps) => {
- return (
-
-
- {file.name}
-
-
- );
-};
-
-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/components/containers/AudioPlayerProvider.tsx b/src/components/containers/AudioPlayerProvider.tsx
new file mode 100644
index 0000000..7c58820
--- /dev/null
+++ b/src/components/containers/AudioPlayerProvider.tsx
@@ -0,0 +1,94 @@
+import { getFileUrl } from "@/app/apps/lib";
+import { base64encode } from "@/lib/utils";
+import { audioPlayer, audioPlayerStore } from "@/stores/audioPlayerStore";
+import { AVPlaybackStatusSuccess, Audio } from "expo-av";
+import jsmediatags from "jsmediatags/build2/jsmediatags";
+import { useEffect, useRef } from "react";
+import { useStore } from "zustand";
+
+const AudioPlayerProvider = () => {
+ const { currentIdx, playlist, repeat, status } = useStore(audioPlayerStore);
+
+ const soundRef = useRef(null);
+
+ useEffect(() => {
+ if (!playlist?.length || currentIdx < 0) {
+ return;
+ }
+
+ const fileData = playlist[currentIdx];
+ const uri = getFileUrl(fileData.path);
+
+ async function play() {
+ try {
+ const { sound } = await Audio.Sound.createAsync({ uri });
+ soundRef.current = sound;
+ audioPlayerStore.setState({ sound });
+
+ sound.setIsLoopingAsync(repeat);
+ sound.setOnPlaybackStatusUpdate((st: AVPlaybackStatusSuccess) => {
+ audioPlayerStore.setState({ status: st as any });
+
+ if (st.didJustFinish) {
+ audioPlayer.next();
+ }
+ });
+
+ await sound.playAsync();
+
+ if (soundRef.current !== sound) {
+ await sound.unloadAsync();
+ }
+ } catch (err) {
+ if (err instanceof DOMException) {
+ if (err.name === "NotSupportedError") {
+ setTimeout(audioPlayer.next, 1000);
+ }
+ }
+ }
+ }
+
+ function loadMediaTags() {
+ const tagsReader = new jsmediatags.Reader(uri + "&dl=true");
+ audioPlayerStore.setState({ mediaTags: null });
+
+ 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;
+ }
+
+ audioPlayerStore.setState({ mediaTags: mediaTagsResult });
+ },
+ });
+ }
+
+ loadMediaTags();
+ play();
+
+ return () => {
+ soundRef.current?.unloadAsync();
+ soundRef.current = null;
+ };
+ }, [currentIdx, playlist]);
+
+ useEffect(() => {
+ if (status?.isLoaded) {
+ soundRef.current?.setIsLoopingAsync(repeat);
+ }
+ }, [repeat, status?.isLoaded]);
+
+ return null;
+};
+
+export default AudioPlayerProvider;
diff --git a/src/components/pages/files/FileInlineViewer.tsx b/src/components/pages/files/FileInlineViewer.tsx
index 7a2f0c3..3d7582e 100644
--- a/src/components/pages/files/FileInlineViewer.tsx
+++ b/src/components/pages/files/FileInlineViewer.tsx
@@ -9,7 +9,6 @@ import Modal from "react-native-modal";
import { Video, ResizeMode } from "expo-av";
import { getFileUrl, openFile } from "@/app/apps/lib";
import { Image } from "react-native";
-import AudioPlayer from "@/components/containers/AudioPlayer";
import { FileItem } from "@/types/files";
type Props = {
@@ -36,10 +35,6 @@ const FileViewer = ({ file }: Pick) => {
);
}
- if (fileType === "audio") {
- return ;
- }
-
if (fileType === "image") {
return (
void;
+ onSelect?: (file: FileItem, idx: number) => void;
// onMenu?: (file: FileItem) => void;
- onLongPress?: (file: FileItem) => void;
+ onLongPress?: (file: FileItem, idx: number) => void;
};
const FileList = ({ files, onSelect, onLongPress }: FileListProps) => {
@@ -23,11 +23,11 @@ const FileList = ({ files, onSelect, onLongPress }: FileListProps) => {
style={cn("flex-1")}
contentContainerStyle={cn("bg-white")}
data={files || []}
- renderItem={({ item }) => (
+ renderItem={({ item, index }) => (
onSelect?.(item)}
- onLongPress={() => onLongPress?.(item)}
+ onPress={() => onSelect?.(item, index)}
+ onLongPress={() => onLongPress?.(item, index)}
onMenuPress={() => openFileMenu(item)}
/>
)}
diff --git a/src/components/pages/home/Apps.tsx b/src/components/pages/home/Apps.tsx
index 890bee8..2d8f75e 100644
--- a/src/components/pages/home/Apps.tsx
+++ b/src/components/pages/home/Apps.tsx
@@ -7,6 +7,7 @@ import Button from "@ui/Button";
import { useNavigation } from "expo-router";
import { showDialog } from "@/stores/dialogStore";
import { wakePcUp } from "@/app/apps/lib";
+import { audioPlayerStore } from "@/stores/audioPlayerStore";
type Props = ComponentProps;
@@ -19,6 +20,11 @@ const Apps = (props: Props) => {
icon: ,
path: "files",
},
+ {
+ name: "Music",
+ icon: ,
+ action: () => audioPlayerStore.setState({ expanded: true }),
+ },
{
name: "Terminal",
icon: ,
diff --git a/src/components/pages/music/AudioPlayer.tsx b/src/components/pages/music/AudioPlayer.tsx
new file mode 100644
index 0000000..d73c411
--- /dev/null
+++ b/src/components/pages/music/AudioPlayer.tsx
@@ -0,0 +1,252 @@
+import React, { useEffect, useMemo, useRef, useState } from "react";
+import Box from "../../ui/Box";
+import Text from "../../ui/Text";
+import { cn, getFilename } from "@/lib/utils";
+import { FlatList, Image, Pressable } from "react-native";
+import { HStack } from "../../ui/Stack";
+import Button from "../../ui/Button";
+import { Ionicons } from "../../ui/Icons";
+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 { useStore } from "zustand";
+import { audioPlayer, audioPlayerStore } from "@/stores/audioPlayerStore";
+import Modal from "react-native-modal";
+
+const AudioPlayer = () => {
+ const expanded = useStore(audioPlayerStore, (i) => i.expanded);
+ const onClose = () => audioPlayerStore.setState({ expanded: false });
+
+ return (
+
+
+
+ );
+};
+
+const AudioPlayerView = React.memo(({ onClose }: { onClose: () => void }) => {
+ const { playlist, currentIdx, status, repeat, shuffle, mediaTags } =
+ useStore(audioPlayerStore);
+
+ const current = playlist[currentIdx];
+ const filename = getFilename(current?.path);
+
+ return (
+
+
+
+
+
+
+ }
+ iconClassName="text-white text-2xl"
+ variant="ghost"
+ className="absolute top-4 left-0 z-10"
+ size="lg"
+ onPress={onClose}
+ />
+
+
+ {mediaTags?.picture ? (
+
+ ) : null}
+
+ Now Playing
+
+ {mediaTags?.tags?.title || filename}
+
+ {mediaTags?.tags?.artist ? (
+
+ {mediaTags.tags.artist}
+
+ ) : null}
+
+
+ {
+ const [progress] = value;
+ audioPlayer.seek(progress);
+ }}
+ />
+
+
+ {formatTime(status?.positionMillis || 0)}
+
+
+ {formatTime(status?.durationMillis || 0)}
+
+
+
+
+
+ }
+ iconClassName={`text-[24px] md:text-[32px] text-white ${
+ repeat ? "opacity-100" : "opacity-50"
+ }`}
+ variant="ghost"
+ className="w-16 h-16 md:w-20 md:h-20 rounded-full"
+ onPress={() => audioPlayerStore.setState({ repeat: !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={audioPlayer.prev}
+ />
+ }
+ iconClassName="text-[32px] md:text-[36px]"
+ className="w-20 h-20 md:w-24 md:h-24 rounded-full"
+ onPress={audioPlayer.togglePlay}
+ />
+ }
+ iconClassName="text-[24px] md:text-[32px] text-white"
+ variant="ghost"
+ className="w-16 h-16 md:w-20 md:h-20 rounded-full"
+ onPress={audioPlayer.next}
+ />
+ }
+ iconClassName={`text-[24px] md:text-[32px] text-white ${
+ shuffle ? "opacity-100" : "opacity-50"
+ }`}
+ variant="ghost"
+ className="w-16 h-16 md:w-20 md:h-20 rounded-full"
+ onPress={() => audioPlayerStore.setState({ shuffle: !shuffle })}
+ />
+
+
+
+
+
+
+ );
+});
+
+const PLAYLIST_ITEM_HEIGHT = 49;
+
+const Playlist = () => {
+ const playlist = useStore(audioPlayerStore, (i) => i.playlist);
+ const currentIdx = useStore(audioPlayerStore, (i) => i.currentIdx);
+ const containerRef = useRef();
+ const [search, setSearch] = useState("");
+
+ useEffect(() => {
+ if (currentIdx >= 0) {
+ containerRef.current?.scrollToOffset({
+ offset: currentIdx * PLAYLIST_ITEM_HEIGHT,
+ animated: true,
+ });
+ }
+ }, [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]);
+
+ return (
+
+
+
+
+ Playlist
+
+
+
+
+ i.path}
+ renderItem={({ item }) => (
+
+ audioPlayerStore.setState({ currentIdx: item.idx })
+ }
+ />
+ )}
+ />
+
+
+ );
+};
+
+type PlaylistItemProps = {
+ file: FileItem;
+ isCurrent: boolean;
+ onPress: () => void;
+};
+
+const PlaylistItem = ({ file, isCurrent, onPress }: PlaylistItemProps) => {
+ return (
+
+
+ {file.name}
+
+
+ );
+};
+
+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 React.memo(AudioPlayer);
diff --git a/src/components/pages/music/MiniPlayer.tsx b/src/components/pages/music/MiniPlayer.tsx
new file mode 100644
index 0000000..4ddee19
--- /dev/null
+++ b/src/components/pages/music/MiniPlayer.tsx
@@ -0,0 +1,73 @@
+import { cn, getFilename } from "@/lib/utils";
+import { audioPlayer, audioPlayerStore } from "@/stores/audioPlayerStore";
+import { Slider } from "@miblanchard/react-native-slider";
+import Box from "@ui/Box";
+import Button from "@ui/Button";
+import { Ionicons } from "@ui/Icons";
+import { HStack } from "@ui/Stack";
+import Text from "@ui/Text";
+import React from "react";
+import { Pressable } from "react-native";
+import { useStore } from "zustand";
+
+const MiniPlayer = () => {
+ const { status, playlist, currentIdx, mediaTags } =
+ useStore(audioPlayerStore);
+ const current = playlist[currentIdx];
+ const filename = getFilename(current?.path);
+
+ if (!status?.isLoaded) {
+ return null;
+ }
+
+ return (
+ <>
+
+
+ audioPlayerStore.setState({ expanded: true })}
+ >
+
+ {mediaTags?.tags?.title || filename || "..."}
+
+
+
+
+
+ }
+ onPress={audioPlayer.prev}
+ />
+ }
+ onPress={audioPlayer.togglePlay}
+ />
+ }
+ onPress={audioPlayer.next}
+ />
+
+
+ >
+ );
+};
+
+export default MiniPlayer;
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 885d8c0..6b1df37 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -54,7 +54,13 @@ export const getFileType = (path?: string | null) => {
};
export const getFilename = (path?: string | null) => {
+ if (!path) {
+ return null;
+ }
+
let fname = path.split("/").pop()?.split(".").slice(0, -1).join(".");
- fname = fname.substring(0, fname.indexOf("?"));
+ if (fname.indexOf("?") > -1) {
+ fname = fname.substring(0, fname.indexOf("?"));
+ }
return fname;
};
diff --git a/src/stores/audioPlayerStore.ts b/src/stores/audioPlayerStore.ts
new file mode 100644
index 0000000..93f082f
--- /dev/null
+++ b/src/stores/audioPlayerStore.ts
@@ -0,0 +1,111 @@
+import { FileItem } from "@/types/files";
+import { createStore } from "zustand";
+import { createJSONStorage, persist } from "zustand/middleware";
+import AsyncStorage from "@react-native-async-storage/async-storage";
+import { AVPlaybackStatusSuccess, Audio } from "expo-av";
+import { MediaTags } from "@/types/mediaTags";
+import { getFileType } from "@/lib/utils";
+
+type PlaylistItem = FileItem & {
+ idx: number;
+};
+
+type AudioPlayerStore = {
+ playlist: PlaylistItem[];
+ currentIdx: number;
+ repeat: boolean;
+ shuffle: boolean;
+ sound: Audio.Sound | null;
+ status: AVPlaybackStatusSuccess | null;
+ mediaTags: MediaTags | null;
+ expanded: boolean;
+};
+
+export const audioPlayerStore = createStore(
+ persist(
+ () => ({
+ playlist: [],
+ currentIdx: -1,
+ repeat: false,
+ shuffle: false,
+ sound: null,
+ status: null,
+ mediaTags: null,
+ expanded: false,
+ }),
+ {
+ name: "audioPlayer",
+ storage: createJSONStorage(() => AsyncStorage),
+ partialize: (state) => ({
+ ...state,
+ sound: null,
+ status: null,
+ mediaTags: null,
+ expanded: false,
+ }),
+ }
+ )
+);
+
+const play = (files: FileItem[], idx: number) => {
+ const playlist: PlaylistItem[] = files
+ .map((f, idx) => ({ ...f, idx }))
+ .filter((f) => getFileType(f.name) === "audio");
+ const currentIdx = playlist.findIndex((f) => f.idx === idx);
+
+ audioPlayerStore.setState({
+ playlist: playlist.map((f, idx) => ({ ...f, idx })),
+ currentIdx,
+ status: null,
+ mediaTags: null,
+ });
+};
+
+const advanceBy = (increment: number) => {
+ const { playlist, currentIdx, shuffle } = audioPlayerStore.getState();
+ if (playlist.length > 0 && currentIdx >= 0) {
+ const idx = shuffle
+ ? Math.floor(Math.random() * playlist.length)
+ : (currentIdx + increment) % playlist.length;
+ audioPlayerStore.setState({ currentIdx: idx });
+ }
+};
+
+const togglePlay = async () => {
+ const { sound, status } = audioPlayerStore.getState();
+ if (!sound) {
+ return;
+ }
+
+ if (status?.isPlaying) {
+ await sound.pauseAsync();
+ } else {
+ await sound.playAsync();
+ }
+};
+
+const next = () => advanceBy(1);
+const prev = () => advanceBy(-1);
+
+const seek = async (progress: number) => {
+ const { sound, status } = audioPlayerStore.getState();
+ if (!sound) {
+ return;
+ }
+
+ if (!status.isPlaying) {
+ await sound.playAsync();
+ }
+
+ const pos = (progress / 100.0) * (status?.durationMillis || 0);
+ sound.setPositionAsync(pos);
+};
+
+export const audioPlayer = {
+ store: audioPlayerStore,
+ play,
+ togglePlay,
+ prev,
+ next,
+ seek,
+};