mirror of
https://github.com/khairul169/home-lab.git
synced 2025-04-28 16:49:36 +07:00
fix: audio playback fix, add playlist view
This commit is contained in:
parent
5925f08a6e
commit
e93b33d3d8
File diff suppressed because one or more lines are too long
@ -32,6 +32,6 @@
|
||||
</noscript>
|
||||
<!-- The root element for your Expo app. -->
|
||||
<div id="root"></div>
|
||||
<script src="/_expo/static/js/web/entry-d757cfde51c8abc54961c1bb601f2b48.js" defer></script>
|
||||
<script src="/_expo/static/js/web/entry-120b4de7dbb4d4577d15df21ce04e72d.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -5,7 +5,7 @@ import { useAuth } from "@/stores/authStore";
|
||||
import BackButton from "@ui/BackButton";
|
||||
import Input from "@ui/Input";
|
||||
import { Stack, router, useLocalSearchParams } from "expo-router";
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useMutation, useQuery } from "react-query";
|
||||
import FileDrop from "@/components/pages/files/FileDrop";
|
||||
import { showToast } from "@/stores/toastStore";
|
||||
@ -15,12 +15,14 @@ import { Ionicons } from "@ui/Icons";
|
||||
import FileInlineViewer from "@/components/pages/files/FileInlineViewer";
|
||||
import { decodeUrl, encodeUrl } from "@/lib/utils";
|
||||
import { FilesContext } from "@/components/pages/files/FilesContext";
|
||||
import { FileItem } from "@/types/files";
|
||||
|
||||
const FilesPage = () => {
|
||||
const { isLoggedIn } = useAuth();
|
||||
const [params, setParams] = useAsyncStorage("files", {
|
||||
path: "",
|
||||
});
|
||||
const [viewFile, setViewFile] = useState<FileItem | null>(null);
|
||||
const searchParams = useLocalSearchParams();
|
||||
const parentPath =
|
||||
params.path.length > 0
|
||||
@ -64,7 +66,7 @@ const FilesPage = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<FilesContext.Provider value={{ files: data }}>
|
||||
<FilesContext.Provider value={{ files: data, viewFile, setViewFile }}>
|
||||
<Stack.Screen
|
||||
options={{ headerLeft: () => <BackButton />, title: "Files" }}
|
||||
/>
|
||||
@ -98,15 +100,16 @@ const FilesPage = () => {
|
||||
files={data}
|
||||
onSelect={(file) => {
|
||||
if (file.isDirectory) {
|
||||
return setParams({ ...params, path: file.path });
|
||||
setParams({ ...params, path: file.path });
|
||||
} else {
|
||||
setViewFile(file);
|
||||
}
|
||||
router.push("/apps/files?view=" + encodeUrl(file.path));
|
||||
}}
|
||||
/>
|
||||
</FileDrop>
|
||||
|
||||
<FileInlineViewer
|
||||
path={decodeUrl(searchParams.view as string)}
|
||||
file={viewFile}
|
||||
onClose={() => {
|
||||
if (router.canGoBack()) {
|
||||
router.back();
|
||||
|
@ -11,14 +11,17 @@ import { HStack } from "@ui/Stack";
|
||||
import Box from "@ui/Box";
|
||||
import Apps from "../components/pages/home/Apps";
|
||||
import { Stack } from "expo-router";
|
||||
import { useIsFocused } from "@/hooks/useIsFocused";
|
||||
|
||||
const HomePage = () => {
|
||||
const { isLoggedIn } = useAuth();
|
||||
const isFocused = useIsFocused();
|
||||
|
||||
const { data: system } = useQuery({
|
||||
queryKey: ["system"],
|
||||
queryFn: () => api.system.$get().then((r) => r.json()),
|
||||
refetchInterval: 1000,
|
||||
enabled: isLoggedIn,
|
||||
enabled: isLoggedIn && isFocused,
|
||||
});
|
||||
|
||||
if (!isLoggedIn) {
|
||||
|
293
src/components/containers/AudioPlayer.tsx
Normal file
293
src/components/containers/AudioPlayer.tsx
Normal file
@ -0,0 +1,293 @@
|
||||
import { AVPlaybackStatusSuccess, Audio } from "expo-av";
|
||||
import { 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";
|
||||
|
||||
type Props = {
|
||||
path: string;
|
||||
uri: string;
|
||||
};
|
||||
|
||||
const AudioPlayer = ({ path, uri }: Props) => {
|
||||
const { files, setViewFile } = useFilesContext();
|
||||
const soundRef = useRef<Audio.Sound | null>(null);
|
||||
const [curFileIdx, setFileIdx] = useState(-1);
|
||||
const [status, setStatus] = useState<AVPlaybackStatusSuccess | null>(null);
|
||||
const [mediaTags, setMediaTags] = useState<MediaTags | null>(null);
|
||||
const filename = getFilename(decodeURIComponent(uri));
|
||||
|
||||
const playlist = useMemo(() => {
|
||||
return files.filter((f) => getFileType(f.name) === "audio");
|
||||
}, [files]);
|
||||
|
||||
const playIdx = (idx: number) => {
|
||||
if (!playlist.length || idx < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const file = playlist[idx % playlist.length];
|
||||
if (file) {
|
||||
setViewFile(file);
|
||||
}
|
||||
};
|
||||
|
||||
const playNext = (increment = 1) => {
|
||||
if (!playlist.length || curFileIdx < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
playIdx(curFileIdx + increment);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!playlist?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileIdx = playlist.findIndex((file) => path === file.path);
|
||||
setFileIdx(fileIdx);
|
||||
|
||||
const onNext = () => playIdx(fileIdx + 1);
|
||||
|
||||
async function play() {
|
||||
try {
|
||||
const { sound } = await Audio.Sound.createAsync({ uri });
|
||||
soundRef.current = sound;
|
||||
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);
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
|
||||
play();
|
||||
|
||||
return () => {
|
||||
soundRef.current?.unloadAsync();
|
||||
soundRef.current = null;
|
||||
};
|
||||
}, [uri, path, playlist]);
|
||||
|
||||
return (
|
||||
<HStack className="flex-1 items-stretch">
|
||||
<Box className="flex-1 relative overflow-hidden">
|
||||
<Box className="absolute -inset-10 -z-[1]">
|
||||
<Image
|
||||
source={mediaTags?.picture ? { uri: mediaTags?.picture } : bgImage}
|
||||
style={cn("absolute -inset-5 w-full h-full")}
|
||||
resizeMode="cover"
|
||||
blurRadius={10}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box className="absolute inset-0 z-[1] bg-black bg-opacity-50 flex flex-col items-center justify-center p-4 md:p-8">
|
||||
{mediaTags?.picture ? (
|
||||
<Image
|
||||
source={{ uri: mediaTags.picture }}
|
||||
style={cn("w-full flex-1 max-h-[256px] mb-8")}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Text className="text-white text-lg sm:text-xl">Now Playing</Text>
|
||||
<Text
|
||||
className="text-white text-xl md:text-3xl mt-4"
|
||||
numberOfLines={1}
|
||||
>
|
||||
{mediaTags?.tags?.title || filename}
|
||||
</Text>
|
||||
{mediaTags?.tags?.artist ? (
|
||||
<Text className="text-white mt-2" numberOfLines={1}>
|
||||
{mediaTags.tags.artist}
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
<Slider
|
||||
minimumValue={0}
|
||||
maximumValue={100}
|
||||
value={
|
||||
((status?.positionMillis || 0) / (status?.durationMillis || 1)) *
|
||||
100
|
||||
}
|
||||
containerStyle={cn("w-full my-4 md:my-8")}
|
||||
onValueChange={async (value) => {
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
|
||||
<HStack className="gap-4">
|
||||
<Button
|
||||
icon={<Ionicons name="chevron-back" />}
|
||||
iconClassName="text-[32px] md:text-[40px]"
|
||||
className="w-16 h-16 md:w-20 md:h-20 rounded-full"
|
||||
onPress={() => playNext(-1)}
|
||||
/>
|
||||
<Button
|
||||
icon={<Ionicons name={status?.isPlaying ? "pause" : "play"} />}
|
||||
iconClassName="text-[40px] md:text-[48px]"
|
||||
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();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
icon={<Ionicons name="chevron-forward" />}
|
||||
iconClassName="text-[32px] md:text-[40px]"
|
||||
className="w-16 h-16 md:w-20 md:h-20 rounded-full"
|
||||
onPress={() => playNext()}
|
||||
/>
|
||||
</HStack>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Playlist playlist={playlist} currentIdx={curFileIdx} playIdx={playIdx} />
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
type PlaylistProps = {
|
||||
playlist: FileItem[];
|
||||
currentIdx: number;
|
||||
playIdx: (idx: number) => void;
|
||||
};
|
||||
|
||||
const Playlist = ({ playlist, currentIdx, playIdx }: PlaylistProps) => {
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const list = useMemo(() => {
|
||||
if (!search?.length) {
|
||||
return playlist;
|
||||
}
|
||||
return playlist.filter((i) =>
|
||||
i.name.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
}, [search, playlist]);
|
||||
|
||||
return (
|
||||
<Box className="hidden md:flex w-1/3 max-w-[400px] bg-black">
|
||||
<HStack className="pl-6 pr-2 items-center">
|
||||
<Text className="text-2xl text-white py-4 flex-1" numberOfLines={1}>
|
||||
Playlist
|
||||
</Text>
|
||||
<Input
|
||||
value={search}
|
||||
onChangeText={setSearch}
|
||||
placeholder="Search"
|
||||
placeholderTextColor="white"
|
||||
inputClassName="text-white"
|
||||
/>
|
||||
</HStack>
|
||||
|
||||
<FlatList
|
||||
data={list}
|
||||
keyExtractor={(i) => i.path}
|
||||
renderItem={({ item, index }) => (
|
||||
<PlaylistItem
|
||||
file={item}
|
||||
isCurrent={index === currentIdx}
|
||||
onPress={() => playIdx(index)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
type PlaylistItemProps = {
|
||||
file: FileItem;
|
||||
isCurrent: boolean;
|
||||
onPress: () => void;
|
||||
};
|
||||
|
||||
const PlaylistItem = ({ file, isCurrent, onPress }: PlaylistItemProps) => {
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
style={cn(
|
||||
"py-4 px-5 border-b border-gray-800",
|
||||
isCurrent && "bg-[#323232]"
|
||||
)}
|
||||
>
|
||||
<Text className="text-white" numberOfLines={1}>
|
||||
{file.name}
|
||||
</Text>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
export default AudioPlayer;
|
@ -9,17 +9,18 @@ import Modal from "react-native-modal";
|
||||
import { Video, ResizeMode } from "expo-av";
|
||||
import { getFileUrl, openFile } from "@/app/apps/files/utils";
|
||||
import { Image } from "react-native";
|
||||
import AudioPlayer from "@ui/AudioPlayer";
|
||||
import AudioPlayer from "@/components/containers/AudioPlayer";
|
||||
import { FileItem } from "@/types/files";
|
||||
|
||||
type Props = {
|
||||
path?: string | null;
|
||||
file?: FileItem | null;
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
const FileViewer = ({ path }: Pick<Props, "path">) => {
|
||||
const FileViewer = ({ file }: Pick<Props, "file">) => {
|
||||
const videoPlayerRef = useRef<Video>(null!);
|
||||
const fileType = getFileType(path);
|
||||
const uri = getFileUrl(path);
|
||||
const fileType = getFileType(file.path);
|
||||
const uri = getFileUrl(file.path);
|
||||
|
||||
if (fileType === "video") {
|
||||
return (
|
||||
@ -36,7 +37,7 @@ const FileViewer = ({ path }: Pick<Props, "path">) => {
|
||||
}
|
||||
|
||||
if (fileType === "audio") {
|
||||
return <AudioPlayer path={path} uri={uri} />;
|
||||
return <AudioPlayer path={file.path} uri={uri} />;
|
||||
}
|
||||
|
||||
if (fileType === "image") {
|
||||
@ -51,18 +52,22 @@ const FileViewer = ({ path }: Pick<Props, "path">) => {
|
||||
|
||||
return (
|
||||
<Box className="w-full flex-1 flex flex-col items-center justify-center">
|
||||
<Button onPress={() => openFile(path)}>Open File</Button>
|
||||
<Button onPress={() => openFile(file.path)}>Open File</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const FileInlineViewer = ({ path, onClose }: Props) => {
|
||||
const filename = path?.split("/").pop();
|
||||
const FileInlineViewer = ({ file, onClose }: Props) => {
|
||||
const filename = file?.path?.split("/").pop();
|
||||
|
||||
return (
|
||||
<Modal isVisible={!!path} onBackButtonPress={onClose} style={cn("m-0")}>
|
||||
<Modal
|
||||
isVisible={!!file?.path}
|
||||
onBackButtonPress={onClose}
|
||||
style={cn("m-0")}
|
||||
>
|
||||
<Box className="flex-1 w-full bg-gray-950">
|
||||
<HStack className="gap-2 p-2 bg-gray-900 relative z-10">
|
||||
<HStack className="gap-2 p-2 bg-black border-b border-gray-800 relative z-10">
|
||||
<Button
|
||||
icon={<Ionicons name="arrow-back" />}
|
||||
iconClassName="text-white"
|
||||
@ -77,18 +82,18 @@ const FileInlineViewer = ({ path, onClose }: Props) => {
|
||||
icon={<Ionicons name="download-outline" />}
|
||||
iconClassName="text-white text-xl"
|
||||
className="px-3"
|
||||
onPress={() => openFile(path, true)}
|
||||
onPress={() => openFile(file?.path, true)}
|
||||
variant="ghost"
|
||||
/>
|
||||
<Button
|
||||
icon={<Ionicons name="open-outline" />}
|
||||
iconClassName="text-white text-xl"
|
||||
className="px-3"
|
||||
onPress={() => openFile(path)}
|
||||
onPress={() => openFile(file?.path)}
|
||||
/>
|
||||
</HStack>
|
||||
|
||||
<FileViewer path={path} />
|
||||
{file ? <FileViewer file={file} /> : null}
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
|
@ -3,10 +3,14 @@ import { createContext, useContext } from "react";
|
||||
|
||||
type FilesContextType = {
|
||||
files: FileItem[];
|
||||
viewFile: FileItem | null;
|
||||
setViewFile: (file: FileItem | null) => void;
|
||||
};
|
||||
|
||||
export const FilesContext = createContext<FilesContextType>({
|
||||
files: [],
|
||||
viewFile: null,
|
||||
setViewFile: () => null,
|
||||
});
|
||||
|
||||
export const useFilesContext = () => useContext(FilesContext);
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { useIsFocused } from "@/hooks/useIsFocused";
|
||||
import api from "@/lib/api";
|
||||
import Box from "@ui/Box";
|
||||
import Button from "@ui/Button";
|
||||
@ -8,6 +9,7 @@ import { useQuery } from "react-query";
|
||||
|
||||
const ProcessList = () => {
|
||||
const [sort, setSort] = useState<string>("mem");
|
||||
const isFocused = useIsFocused();
|
||||
|
||||
const { data } = useQuery({
|
||||
queryKey: ["process", sort],
|
||||
@ -18,6 +20,7 @@ const ProcessList = () => {
|
||||
},
|
||||
select: (i) => i.list,
|
||||
refetchInterval: 1000,
|
||||
enabled: isFocused,
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -1,196 +0,0 @@
|
||||
import { AVPlaybackStatusSuccess, Audio } from "expo-av";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import jsmediatags from "jsmediatags/build2/jsmediatags";
|
||||
import { MediaTags } from "@/types/mediaTags";
|
||||
import Box from "./Box";
|
||||
import Text from "./Text";
|
||||
import { base64encode, cn, encodeUrl, getFilename } from "@/lib/utils";
|
||||
import { Image } from "react-native";
|
||||
import { useFilesContext } from "../pages/files/FilesContext";
|
||||
import { HStack } from "./Stack";
|
||||
import Button from "./Button";
|
||||
import { Ionicons } from "./Icons";
|
||||
import { router } from "expo-router";
|
||||
import { Slider } from "@miblanchard/react-native-slider";
|
||||
import bgImage from "@/assets/images/audioplayer-bg.jpeg";
|
||||
|
||||
type Props = {
|
||||
path: string;
|
||||
uri: string;
|
||||
};
|
||||
|
||||
const AudioPlayer = ({ path, uri }: Props) => {
|
||||
const { files } = useFilesContext();
|
||||
const soundRef = useRef<Audio.Sound | null>(null);
|
||||
const [curFileIdx, setFileIdx] = useState(-1);
|
||||
const [status, setStatus] = useState<AVPlaybackStatusSuccess | null>(null);
|
||||
const [mediaTags, setMediaTags] = useState<MediaTags | null>(null);
|
||||
const filename = getFilename(decodeURIComponent(uri));
|
||||
|
||||
const playNext = (inc = 1) => {
|
||||
if (!files.length || curFileIdx < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileIdx = (curFileIdx + inc) % files.length;
|
||||
const file = files[fileIdx];
|
||||
// setPlayback({ uri: getFileUrl(file), path: file.path });
|
||||
router.setParams({ view: encodeUrl(file.path) });
|
||||
};
|
||||
|
||||
const onPlaybackEnd = () => {
|
||||
playNext();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!files?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
async function play() {
|
||||
try {
|
||||
const { sound } = await Audio.Sound.createAsync({ uri });
|
||||
soundRef.current = sound;
|
||||
sound.setOnPlaybackStatusUpdate((st: AVPlaybackStatusSuccess) => {
|
||||
setStatus(st as any);
|
||||
if (st.didJustFinish) {
|
||||
onPlaybackEnd();
|
||||
}
|
||||
});
|
||||
|
||||
await sound.playAsync();
|
||||
|
||||
if (soundRef.current !== sound) {
|
||||
await sound.unloadAsync();
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException) {
|
||||
if (err.name === "NotSupportedError") {
|
||||
setTimeout(onPlaybackEnd, 3000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fileIdx = files.findIndex((file) => path === file.path);
|
||||
setFileIdx(fileIdx);
|
||||
|
||||
play();
|
||||
|
||||
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);
|
||||
},
|
||||
});
|
||||
|
||||
return () => {
|
||||
soundRef.current?.unloadAsync();
|
||||
soundRef.current = null;
|
||||
};
|
||||
}, [uri, path, files]);
|
||||
|
||||
return (
|
||||
<Box className="flex-1 relative">
|
||||
<Box className="absolute -inset-10 -z-[1]">
|
||||
<Image
|
||||
source={mediaTags?.picture ? { uri: mediaTags?.picture } : bgImage}
|
||||
style={cn("absolute -inset-5 w-full h-full")}
|
||||
resizeMode="cover"
|
||||
blurRadius={10}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box className="absolute inset-0 z-[1] bg-black bg-opacity-50 flex flex-col items-center justify-center p-4 md:p-8">
|
||||
{mediaTags?.picture ? (
|
||||
<Image
|
||||
source={{ uri: mediaTags.picture }}
|
||||
style={cn("w-full flex-1 max-h-[256px] mb-8")}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Text className="text-white text-lg sm:text-xl">Now Playing</Text>
|
||||
<Text className="text-white text-xl md:text-3xl mt-4" numberOfLines={1}>
|
||||
{mediaTags?.tags?.title || filename}
|
||||
</Text>
|
||||
{mediaTags?.tags?.artist ? (
|
||||
<Text className="text-white mt-2" numberOfLines={1}>
|
||||
{mediaTags.tags.artist}
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
<Slider
|
||||
minimumValue={0}
|
||||
maximumValue={100}
|
||||
value={
|
||||
((status?.positionMillis || 0) / (status?.durationMillis || 1)) *
|
||||
100
|
||||
}
|
||||
containerStyle={cn("w-full my-4 md:my-8")}
|
||||
onValueChange={async (value) => {
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
|
||||
<HStack className="gap-4">
|
||||
<Button
|
||||
icon={<Ionicons name="chevron-back" />}
|
||||
iconClassName="text-[32px] md:text-[40px]"
|
||||
className="w-16 h-16 md:w-20 md:h-20 rounded-full"
|
||||
onPress={() => playNext(-1)}
|
||||
/>
|
||||
<Button
|
||||
icon={<Ionicons name={status?.isPlaying ? "pause" : "play"} />}
|
||||
iconClassName="text-[40px] md:text-[48px]"
|
||||
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();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
icon={<Ionicons name="chevron-forward" />}
|
||||
iconClassName="text-[32px] md:text-[40px]"
|
||||
className="w-16 h-16 md:w-20 md:h-20 rounded-full"
|
||||
onPress={() => playNext()}
|
||||
/>
|
||||
</HStack>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default AudioPlayer;
|
15
src/hooks/useIsFocused.ts
Normal file
15
src/hooks/useIsFocused.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { useFocusEffect } from "expo-router";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
export const useIsFocused = () => {
|
||||
const [isFocused, setFocused] = useState(false);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
setFocused(true);
|
||||
return () => setFocused(false);
|
||||
}, [])
|
||||
);
|
||||
|
||||
return isFocused;
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user