mirror of
https://github.com/khairul169/home-lab.git
synced 2025-04-28 16:49:36 +07:00
feat: add inline file viewer, add audio player
This commit is contained in:
parent
a371a9e569
commit
7ca128a02c
@ -28,9 +28,7 @@ const filesDirList = process.env.FILE_DIRS
|
|||||||
const route = new Hono()
|
const route = new Hono()
|
||||||
.get("/", zValidator("query", getFilesSchema), async (c) => {
|
.get("/", zValidator("query", getFilesSchema), async (c) => {
|
||||||
const input: z.infer<typeof getFilesSchema> = c.req.query();
|
const input: z.infer<typeof getFilesSchema> = c.req.query();
|
||||||
const pathname = (input.path || "").split("/");
|
const { baseName, path, pathname } = getFilePath(input.path);
|
||||||
const path = pathname.slice(2).join("/");
|
|
||||||
const baseName = pathname[1];
|
|
||||||
|
|
||||||
if (!baseName?.length) {
|
if (!baseName?.length) {
|
||||||
return c.json(
|
return c.json(
|
||||||
@ -42,20 +40,14 @@ const route = new Hono()
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseDir = filesDirList.find((i) => i.name === baseName)?.path;
|
|
||||||
if (!baseDir) {
|
|
||||||
return c.json([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const cwd = baseDir + "/" + path;
|
const entities = await fs.readdir(path, { withFileTypes: true });
|
||||||
const entities = await fs.readdir(cwd, { withFileTypes: true });
|
|
||||||
|
|
||||||
const files = entities
|
const files = entities
|
||||||
.filter((e) => !e.name.startsWith("."))
|
.filter((e) => !e.name.startsWith("."))
|
||||||
.map((e) => ({
|
.map((e) => ({
|
||||||
name: e.name,
|
name: e.name,
|
||||||
path: "/" + [baseName, path, e.name].filter(Boolean).join("/"),
|
path: [pathname, e.name].join("/"),
|
||||||
isDirectory: e.isDirectory(),
|
isDirectory: e.isDirectory(),
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
@ -90,16 +82,11 @@ const route = new Hono()
|
|||||||
throw new HTTPException(400, { message: "Files is empty!" });
|
throw new HTTPException(400, { message: "Files is empty!" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const pathSlices = data.path.split("/");
|
const { baseDir, path: targetDir } = getFilePath(data.path);
|
||||||
const baseName = pathSlices[1] || null;
|
|
||||||
const path = pathSlices.slice(2).join("/");
|
|
||||||
const baseDir = filesDirList.find((i) => i.name === baseName)?.path;
|
|
||||||
if (!baseDir?.length) {
|
if (!baseDir?.length) {
|
||||||
throw new HTTPException(400, { message: "Path not found!" });
|
throw new HTTPException(400, { message: "Path not found!" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetDir = [baseDir, path].join("/");
|
|
||||||
|
|
||||||
// files.forEach((file) => {
|
// files.forEach((file) => {
|
||||||
// const filepath = targetDir + "/" + file.name;
|
// const filepath = targetDir + "/" + file.name;
|
||||||
// if (existsSync(filepath)) {
|
// if (existsSync(filepath)) {
|
||||||
@ -124,15 +111,16 @@ const route = new Hono()
|
|||||||
const pathSlice = pathname.slice(pathname.indexOf("download") + 1);
|
const pathSlice = pathname.slice(pathname.indexOf("download") + 1);
|
||||||
const baseName = pathSlice[0];
|
const baseName = pathSlice[0];
|
||||||
const path = "/" + pathSlice.slice(1).join("/");
|
const path = "/" + pathSlice.slice(1).join("/");
|
||||||
|
const filename = path.substring(1);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!baseName?.length) {
|
if (!baseName?.length) {
|
||||||
throw new Error();
|
throw new Error("baseName is empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseDir = filesDirList.find((i) => i.name === baseName)?.path;
|
const baseDir = filesDirList.find((i) => i.name === baseName)?.path;
|
||||||
if (!baseDir) {
|
if (!baseDir) {
|
||||||
throw new Error();
|
throw new Error("baseDir not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
const filepath = baseDir + path;
|
const filepath = baseDir + path;
|
||||||
@ -141,7 +129,10 @@ const route = new Hono()
|
|||||||
|
|
||||||
if (dlFile) {
|
if (dlFile) {
|
||||||
c.header("Content-Type", "application/octet-stream");
|
c.header("Content-Type", "application/octet-stream");
|
||||||
c.header("Content-Disposition", `attachment; filename="${path}"`);
|
c.header(
|
||||||
|
"Content-Disposition",
|
||||||
|
`attachment; filename="${encodeURIComponent(filename)}"`
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
c.header("Content-Type", getMimeType(filepath));
|
c.header("Content-Type", getMimeType(filepath));
|
||||||
}
|
}
|
||||||
@ -177,23 +168,24 @@ const route = new Hono()
|
|||||||
|
|
||||||
return c.body(stream, 206);
|
return c.body(stream, 206);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
// console.log("err", err);
|
||||||
throw new HTTPException(404, { message: "Not Found!" });
|
throw new HTTPException(404, { message: "Not Found!" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function getFilePath(path: string) {
|
function getFilePath(path?: string) {
|
||||||
const pathSlices = path.split("/");
|
const pathSlices =
|
||||||
|
path
|
||||||
|
?.replace(/\/{2,}/g, "/")
|
||||||
|
.replace(/\/$/, "")
|
||||||
|
.split("/") || [];
|
||||||
const baseName = pathSlices[1] || null;
|
const baseName = pathSlices[1] || null;
|
||||||
const filePath = pathSlices.slice(2).join("/");
|
const filePath = pathSlices.slice(2).join("/");
|
||||||
|
|
||||||
const baseDir = filesDirList.find((i) => i.name === baseName)?.path;
|
const baseDir = filesDirList.find((i) => i.name === baseName)?.path;
|
||||||
if (!baseDir?.length) {
|
|
||||||
throw new HTTPException(400, { message: "Path not found!" });
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
path: [baseDir, filePath].join("/"),
|
path: [baseDir || "", filePath].join("/").replace(/\/$/, ""),
|
||||||
pathname: ["", baseName, filePath].join("/"),
|
pathname: ["", baseName, filePath].join("/").replace(/\/$/, ""),
|
||||||
baseName,
|
baseName,
|
||||||
baseDir,
|
baseDir,
|
||||||
filePath,
|
filePath,
|
||||||
|
5
global.d.ts
vendored
5
global.d.ts
vendored
@ -2,3 +2,8 @@ declare module "*.svg" {
|
|||||||
const content: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
|
const content: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
|
||||||
export default content;
|
export default content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module "*.jpg";
|
||||||
|
declare module "*.jpeg";
|
||||||
|
declare module "*.png";
|
||||||
|
declare module "*.webp";
|
||||||
|
@ -13,21 +13,26 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/metro-runtime": "~3.1.3",
|
"@expo/metro-runtime": "~3.1.3",
|
||||||
"@hookform/resolvers": "^3.3.4",
|
"@hookform/resolvers": "^3.3.4",
|
||||||
|
"@miblanchard/react-native-slider": "^2.3.1",
|
||||||
"@react-native-async-storage/async-storage": "1.21.0",
|
"@react-native-async-storage/async-storage": "1.21.0",
|
||||||
"@types/react": "~18.2.45",
|
"@types/react": "~18.2.45",
|
||||||
|
"base-64": "^1.0.0",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"expo": "~50.0.11",
|
"expo": "~50.0.11",
|
||||||
|
"expo-av": "~13.10.5",
|
||||||
"expo-constants": "~15.4.5",
|
"expo-constants": "~15.4.5",
|
||||||
"expo-linking": "~6.2.2",
|
"expo-linking": "~6.2.2",
|
||||||
"expo-router": "~3.4.8",
|
"expo-router": "~3.4.8",
|
||||||
"expo-status-bar": "~1.11.1",
|
"expo-status-bar": "~1.11.1",
|
||||||
"hono": "^4.1.0",
|
"hono": "^4.1.0",
|
||||||
|
"jsmediatags": "^3.9.7",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-hook-form": "^7.51.0",
|
"react-hook-form": "^7.51.0",
|
||||||
"react-native": "0.73.4",
|
"react-native": "0.73.4",
|
||||||
"react-native-circular-progress": "^1.3.9",
|
"react-native-circular-progress": "^1.3.9",
|
||||||
|
"react-native-fs": "^2.20.0",
|
||||||
"react-native-modal": "^13.0.1",
|
"react-native-modal": "^13.0.1",
|
||||||
"react-native-safe-area-context": "4.8.2",
|
"react-native-safe-area-context": "4.8.2",
|
||||||
"react-native-screens": "~3.29.0",
|
"react-native-screens": "~3.29.0",
|
||||||
@ -37,6 +42,7 @@
|
|||||||
"react-query": "^3.39.3",
|
"react-query": "^3.39.3",
|
||||||
"twrnc": "^4.1.0",
|
"twrnc": "^4.1.0",
|
||||||
"typescript": "^5.3.0",
|
"typescript": "^5.3.0",
|
||||||
|
"utf8": "^3.0.0",
|
||||||
"xterm": "^5.3.0",
|
"xterm": "^5.3.0",
|
||||||
"xterm-addon-attach": "^0.9.0",
|
"xterm-addon-attach": "^0.9.0",
|
||||||
"xterm-addon-fit": "^0.8.0",
|
"xterm-addon-fit": "^0.8.0",
|
||||||
|
@ -4,21 +4,24 @@ import api from "@/lib/api";
|
|||||||
import { useAuth } from "@/stores/authStore";
|
import { useAuth } from "@/stores/authStore";
|
||||||
import BackButton from "@ui/BackButton";
|
import BackButton from "@ui/BackButton";
|
||||||
import Input from "@ui/Input";
|
import Input from "@ui/Input";
|
||||||
import { Stack } from "expo-router";
|
import { Stack, router, useLocalSearchParams } from "expo-router";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useMutation, useQuery } from "react-query";
|
import { useMutation, useQuery } from "react-query";
|
||||||
import { openFile } from "./utils";
|
|
||||||
import FileDrop from "@/components/pages/files/FileDrop";
|
import FileDrop from "@/components/pages/files/FileDrop";
|
||||||
import { showToast } from "@/stores/toastStore";
|
import { showToast } from "@/stores/toastStore";
|
||||||
import { HStack } from "@ui/Stack";
|
import { HStack } from "@ui/Stack";
|
||||||
import Button from "@ui/Button";
|
import Button from "@ui/Button";
|
||||||
import { Ionicons } from "@ui/Icons";
|
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";
|
||||||
|
|
||||||
const FilesPage = () => {
|
const FilesPage = () => {
|
||||||
const { isLoggedIn } = useAuth();
|
const { isLoggedIn } = useAuth();
|
||||||
const [params, setParams] = useAsyncStorage("files", {
|
const [params, setParams] = useAsyncStorage("files", {
|
||||||
path: "",
|
path: "",
|
||||||
});
|
});
|
||||||
|
const searchParams = useLocalSearchParams();
|
||||||
const parentPath =
|
const parentPath =
|
||||||
params.path.length > 0
|
params.path.length > 0
|
||||||
? params.path.split("/").slice(0, -1).join("/")
|
? params.path.split("/").slice(0, -1).join("/")
|
||||||
@ -56,8 +59,12 @@ const FilesPage = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!isLoggedIn) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<FilesContext.Provider value={{ files: data }}>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{ headerLeft: () => <BackButton />, title: "Files" }}
|
options={{ headerLeft: () => <BackButton />, title: "Files" }}
|
||||||
/>
|
/>
|
||||||
@ -71,6 +78,13 @@ const FilesPage = () => {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onPress={() => setParams({ ...params, path: parentPath })}
|
onPress={() => setParams({ ...params, path: parentPath })}
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
icon={<Ionicons name="home-outline" />}
|
||||||
|
className="px-3 border-gray-300"
|
||||||
|
labelClasses="text-gray-500"
|
||||||
|
variant="outline"
|
||||||
|
onPress={() => setParams({ ...params, path: "" })}
|
||||||
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder="/"
|
placeholder="/"
|
||||||
value={params.path}
|
value={params.path}
|
||||||
@ -86,11 +100,22 @@ const FilesPage = () => {
|
|||||||
if (file.isDirectory) {
|
if (file.isDirectory) {
|
||||||
return setParams({ ...params, path: file.path });
|
return setParams({ ...params, path: file.path });
|
||||||
}
|
}
|
||||||
openFile(file);
|
router.push("/apps/files?view=" + encodeUrl(file.path));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FileDrop>
|
</FileDrop>
|
||||||
</>
|
|
||||||
|
<FileInlineViewer
|
||||||
|
path={decodeUrl(searchParams.view as string)}
|
||||||
|
onClose={() => {
|
||||||
|
if (router.canGoBack()) {
|
||||||
|
router.back();
|
||||||
|
} else {
|
||||||
|
router.replace("/apps/files");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FilesContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2,14 +2,15 @@ import { API_BASEURL } from "@/lib/constants";
|
|||||||
import authStore from "@/stores/authStore";
|
import authStore from "@/stores/authStore";
|
||||||
import { FileItem } from "@/types/files";
|
import { FileItem } from "@/types/files";
|
||||||
|
|
||||||
export function openFile(file: FileItem, dl = false) {
|
export function openFile(file: FileItem | string, dl = false) {
|
||||||
const url = getFileUrl(file, dl);
|
const url = getFileUrl(file, dl);
|
||||||
window.open(url, "_blank");
|
window.open(url, "_blank");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFileUrl(file: FileItem, dl = false) {
|
export function getFileUrl(file: FileItem | string, dl = false) {
|
||||||
const url = new URL(API_BASEURL + "/files/download" + file.path);
|
const filepath = typeof file === "string" ? file : file.path;
|
||||||
|
const url = new URL(API_BASEURL + "/files/download" + filepath);
|
||||||
url.searchParams.set("token", authStore.getState().token);
|
url.searchParams.set("token", authStore.getState().token);
|
||||||
dl && url.searchParams.set("dlmode", "true");
|
dl && url.searchParams.set("dl", "true");
|
||||||
return url.toString();
|
return url.toString();
|
||||||
}
|
}
|
||||||
|
BIN
src/assets/images/audioplayer-bg.jpeg
Normal file
BIN
src/assets/images/audioplayer-bg.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 81 KiB |
97
src/components/pages/files/FileInlineViewer.tsx
Normal file
97
src/components/pages/files/FileInlineViewer.tsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { cn, getFileType } from "@/lib/utils";
|
||||||
|
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, { useRef } from "react";
|
||||||
|
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";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
path?: string | null;
|
||||||
|
onClose?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FileViewer = ({ path }: Pick<Props, "path">) => {
|
||||||
|
const videoPlayerRef = useRef<Video>(null!);
|
||||||
|
const fileType = getFileType(path);
|
||||||
|
const uri = getFileUrl(path);
|
||||||
|
|
||||||
|
if (fileType === "video") {
|
||||||
|
return (
|
||||||
|
<Video
|
||||||
|
ref={videoPlayerRef}
|
||||||
|
style={cn("w-full flex-1 overflow-hidden relative")}
|
||||||
|
videoStyle={cn("absolute top-0 left-0 w-full h-full")}
|
||||||
|
source={{ uri }}
|
||||||
|
useNativeControls
|
||||||
|
resizeMode={ResizeMode.CONTAIN}
|
||||||
|
shouldPlay
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileType === "audio") {
|
||||||
|
return <AudioPlayer path={path} uri={uri} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileType === "image") {
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
source={{ uri }}
|
||||||
|
style={cn("w-full flex-1")}
|
||||||
|
resizeMode="contain"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className="w-full flex-1 flex flex-col items-center justify-center">
|
||||||
|
<Button onPress={() => openFile(path)}>Open File</Button>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const FileInlineViewer = ({ path, onClose }: Props) => {
|
||||||
|
const filename = path?.split("/").pop();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isVisible={!!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">
|
||||||
|
<Button
|
||||||
|
icon={<Ionicons name="arrow-back" />}
|
||||||
|
iconClassName="text-white"
|
||||||
|
onPress={onClose}
|
||||||
|
variant="ghost"
|
||||||
|
/>
|
||||||
|
<Text className="text-white flex-1" numberOfLines={1}>
|
||||||
|
{filename}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
icon={<Ionicons name="download-outline" />}
|
||||||
|
iconClassName="text-white text-xl"
|
||||||
|
className="px-3"
|
||||||
|
onPress={() => openFile(path, true)}
|
||||||
|
variant="ghost"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
icon={<Ionicons name="open-outline" />}
|
||||||
|
iconClassName="text-white text-xl"
|
||||||
|
className="px-3"
|
||||||
|
onPress={() => openFile(path)}
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<FileViewer path={path} />
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FileInlineViewer;
|
@ -87,4 +87,4 @@ const FileItemList = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FileList;
|
export default React.memo(FileList);
|
||||||
|
@ -8,6 +8,7 @@ import { cn } from "@/lib/utils";
|
|||||||
import ActionSheet from "@ui/ActionSheet";
|
import ActionSheet from "@ui/ActionSheet";
|
||||||
import { HStack } from "@ui/Stack";
|
import { HStack } from "@ui/Stack";
|
||||||
import Button from "@ui/Button";
|
import Button from "@ui/Button";
|
||||||
|
import { openFile } from "@/app/apps/files/utils";
|
||||||
|
|
||||||
type Store = {
|
type Store = {
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
@ -27,6 +28,11 @@ const FileMenu = () => {
|
|||||||
const { isVisible, file } = useStore(store);
|
const { isVisible, file } = useStore(store);
|
||||||
const onClose = () => store.setState({ isVisible: false });
|
const onClose = () => store.setState({ isVisible: false });
|
||||||
|
|
||||||
|
const onDownload = () => {
|
||||||
|
openFile(file, true);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ActionSheet isVisible={isVisible} onClose={onClose}>
|
<ActionSheet isVisible={isVisible} onClose={onClose}>
|
||||||
<Text className="text-lg md:text-xl" numberOfLines={1}>
|
<Text className="text-lg md:text-xl" numberOfLines={1}>
|
||||||
@ -37,10 +43,13 @@ const FileMenu = () => {
|
|||||||
<List.Item icon={<Ionicons name="pencil" />}>Rename</List.Item>
|
<List.Item icon={<Ionicons name="pencil" />}>Rename</List.Item>
|
||||||
<List.Item icon={<Ionicons name="copy" />}>Copy</List.Item>
|
<List.Item icon={<Ionicons name="copy" />}>Copy</List.Item>
|
||||||
<List.Item icon={<Ionicons name="cut-outline" />}>Move</List.Item>
|
<List.Item icon={<Ionicons name="cut-outline" />}>Move</List.Item>
|
||||||
|
<List.Item icon={<Ionicons name="download" />} onPress={onDownload}>
|
||||||
|
Download
|
||||||
|
</List.Item>
|
||||||
<List.Item icon={<Ionicons name="trash" />}>Delete</List.Item>
|
<List.Item icon={<Ionicons name="trash" />}>Delete</List.Item>
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
<HStack className="justify-end mt-6">
|
<HStack className="justify-end mt-6 hidden md:flex">
|
||||||
<Button variant="ghost" onPress={onClose}>
|
<Button variant="ghost" onPress={onClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
12
src/components/pages/files/FilesContext.tsx
Normal file
12
src/components/pages/files/FilesContext.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { FileItem } from "@/types/files";
|
||||||
|
import { createContext, useContext } from "react";
|
||||||
|
|
||||||
|
type FilesContextType = {
|
||||||
|
files: FileItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FilesContext = createContext<FilesContextType>({
|
||||||
|
files: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useFilesContext = () => useContext(FilesContext);
|
196
src/components/ui/AudioPlayer.tsx
Normal file
196
src/components/ui/AudioPlayer.tsx
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
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;
|
@ -18,11 +18,15 @@ type ListItemProps = {
|
|||||||
className?: any;
|
className?: any;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
|
onPress?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ListItem = ({ className, icon, children }: ListItemProps) => {
|
const ListItem = ({ className, icon, children, onPress }: ListItemProps) => {
|
||||||
return (
|
return (
|
||||||
<Pressable style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })}>
|
<Pressable
|
||||||
|
style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })}
|
||||||
|
onPress={onPress}
|
||||||
|
>
|
||||||
<HStack className={cn("py-2 border-b border-gray-200", className)}>
|
<HStack className={cn("py-2 border-b border-gray-200", className)}>
|
||||||
{icon ? (
|
{icon ? (
|
||||||
<Slot.Text style={cn("text-gray-800 text-xl w-8")}>{icon}</Slot.Text>
|
<Slot.Text style={cn("text-gray-800 text-xl w-8")}>{icon}</Slot.Text>
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { ClassInput, create as createTwrnc } from "twrnc";
|
import { ClassInput, create as createTwrnc } from "twrnc";
|
||||||
|
import base64 from "base-64";
|
||||||
|
import utf8 from "utf8";
|
||||||
|
|
||||||
export const tw = createTwrnc(require(`../../tailwind.config.js`));
|
export const tw = createTwrnc(require(`../../tailwind.config.js`));
|
||||||
|
|
||||||
@ -9,3 +11,50 @@ export const cn = (...args: ClassInput[]) => {
|
|||||||
|
|
||||||
return tw.style(...args);
|
return tw.style(...args);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const base64encode = (str?: string | null) => {
|
||||||
|
return str ? base64.encode(str) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const base64decode = (str?: string | null) => {
|
||||||
|
return str ? base64.decode(str) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const encodeUrl = (str?: string | null) => {
|
||||||
|
return str ? encodeURIComponent(base64encode(utf8.encode(str))) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decodeUrl = (str?: string | null) => {
|
||||||
|
return str ? decodeURIComponent(utf8.decode(base64decode(str))) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFileType = (path?: string | null) => {
|
||||||
|
const ext = path?.split(".").pop();
|
||||||
|
const videoExts = "mp4,mkv,webm,avi,mov";
|
||||||
|
if (videoExts.split(",").includes(ext)) {
|
||||||
|
return "video";
|
||||||
|
}
|
||||||
|
|
||||||
|
const imgExts = "jpeg,jpg,png,gif,webp,avif,svg,bmp,ico,tif,tiff";
|
||||||
|
if (imgExts.split(",").includes(ext)) {
|
||||||
|
return "image";
|
||||||
|
}
|
||||||
|
|
||||||
|
const docExts = "pdf,doc,docx,xls,xlsx,ppt,pptx";
|
||||||
|
if (docExts.split(",").includes(ext)) {
|
||||||
|
return "document";
|
||||||
|
}
|
||||||
|
|
||||||
|
const audioExts = "mp3,ogg,wav,flac,aac,amr";
|
||||||
|
if (audioExts.split(",").includes(ext)) {
|
||||||
|
return "audio";
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFilename = (path?: string | null) => {
|
||||||
|
let fname = path.split("/").pop()?.split(".").slice(0, -1).join(".");
|
||||||
|
fname = fname.substring(0, fname.indexOf("?"));
|
||||||
|
return fname;
|
||||||
|
};
|
||||||
|
83
src/types/mediaTags.ts
Normal file
83
src/types/mediaTags.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
export type MediaTags = {
|
||||||
|
type: string;
|
||||||
|
version: string;
|
||||||
|
major: number;
|
||||||
|
revision: number;
|
||||||
|
flags: Flags;
|
||||||
|
size: number;
|
||||||
|
tags: Tags;
|
||||||
|
picture?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Flags = {
|
||||||
|
unsynchronisation: boolean;
|
||||||
|
extended_header: boolean;
|
||||||
|
experimental_indicator: boolean;
|
||||||
|
footer_present: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Tags = {
|
||||||
|
title: string;
|
||||||
|
artist: string;
|
||||||
|
album: string;
|
||||||
|
year: string;
|
||||||
|
comment: Comment;
|
||||||
|
track: string;
|
||||||
|
genre: string;
|
||||||
|
// picture: Picture;
|
||||||
|
TALB: Talb;
|
||||||
|
TPE1: Talb;
|
||||||
|
COMM: Comm;
|
||||||
|
TCON: Talb;
|
||||||
|
TIT2: Talb;
|
||||||
|
TRCK: Talb;
|
||||||
|
TYER: Talb;
|
||||||
|
TXXX: Txxx;
|
||||||
|
APIC: APIC;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type APIC = {
|
||||||
|
id: string;
|
||||||
|
size: number;
|
||||||
|
description: string;
|
||||||
|
data: Picture;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Picture = {
|
||||||
|
format: string;
|
||||||
|
type: string;
|
||||||
|
description: string;
|
||||||
|
data: number[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Comm = {
|
||||||
|
id: string;
|
||||||
|
size: number;
|
||||||
|
description: string;
|
||||||
|
data: Comment;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Comment = {
|
||||||
|
language: string;
|
||||||
|
short_description: string;
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Talb = {
|
||||||
|
id: string;
|
||||||
|
size: number;
|
||||||
|
description: string;
|
||||||
|
data: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Txxx = {
|
||||||
|
id: string;
|
||||||
|
size: number;
|
||||||
|
description: string;
|
||||||
|
data: Data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Data = {
|
||||||
|
user_description: string;
|
||||||
|
data: string;
|
||||||
|
};
|
45
yarn.lock
45
yarn.lock
@ -1699,6 +1699,11 @@
|
|||||||
"@jridgewell/resolve-uri" "^3.1.0"
|
"@jridgewell/resolve-uri" "^3.1.0"
|
||||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
"@jridgewell/sourcemap-codec" "^1.4.14"
|
||||||
|
|
||||||
|
"@miblanchard/react-native-slider@^2.3.1":
|
||||||
|
version "2.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@miblanchard/react-native-slider/-/react-native-slider-2.3.1.tgz#79e0f1f9b1ce43ef25ee51ee9256c012e5dfa412"
|
||||||
|
integrity sha512-J/hZDBWmXq8fJeOnTVHqIUVDHshqMSpJVxJ4WqwuCBKl5Rke9OBYXIdkSlgi75OgtScAr8FKK5KNkDKHUf6JIg==
|
||||||
|
|
||||||
"@nodelib/fs.scandir@2.1.5":
|
"@nodelib/fs.scandir@2.1.5":
|
||||||
version "2.1.5"
|
version "2.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
|
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
|
||||||
@ -2790,6 +2795,16 @@ balanced-match@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||||
|
|
||||||
|
base-64@^0.1.0:
|
||||||
|
version "0.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb"
|
||||||
|
integrity sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==
|
||||||
|
|
||||||
|
base-64@^1.0.0:
|
||||||
|
version "1.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/base-64/-/base-64-1.0.0.tgz#09d0f2084e32a3fd08c2475b973788eee6ae8f4a"
|
||||||
|
integrity sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==
|
||||||
|
|
||||||
base64-js@^1.2.3, base64-js@^1.3.1, base64-js@^1.5.1:
|
base64-js@^1.2.3, base64-js@^1.3.1, base64-js@^1.5.1:
|
||||||
version "1.5.1"
|
version "1.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
|
||||||
@ -3831,6 +3846,11 @@ expo-asset@~9.0.2:
|
|||||||
invariant "^2.2.4"
|
invariant "^2.2.4"
|
||||||
md5-file "^3.2.3"
|
md5-file "^3.2.3"
|
||||||
|
|
||||||
|
expo-av@~13.10.5:
|
||||||
|
version "13.10.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/expo-av/-/expo-av-13.10.5.tgz#cdce4db8c0f896be88cc881994704a251a1e63ff"
|
||||||
|
integrity sha512-w45oCoe+8PunDeM0rh/Ut6UaGh7OjEJOCjAiQy3nCxpA8FaXB17KaqpsvkAXIMvceHYWndH8+D29esUTS6wEsA==
|
||||||
|
|
||||||
expo-constants@~15.4.0, expo-constants@~15.4.3, expo-constants@~15.4.5:
|
expo-constants@~15.4.0, expo-constants@~15.4.3, expo-constants@~15.4.5:
|
||||||
version "15.4.5"
|
version "15.4.5"
|
||||||
resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-15.4.5.tgz#81756a4c4e1c020f840a419cd86a124a6d1fb35b"
|
resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-15.4.5.tgz#81756a4c4e1c020f840a419cd86a124a6d1fb35b"
|
||||||
@ -4924,6 +4944,13 @@ jsesc@~0.5.0:
|
|||||||
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
|
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
|
||||||
integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==
|
integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==
|
||||||
|
|
||||||
|
jsmediatags@^3.9.7:
|
||||||
|
version "3.9.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/jsmediatags/-/jsmediatags-3.9.7.tgz#808c6713b5ccb9712a4dc4b2149a0cb253c641a2"
|
||||||
|
integrity sha512-xCAO8C3li3t5hYkXqn8iv8zQQUB4T1QqRN2aSONHMls21ICdEvXi4xtb6W70/fAFYSDwMHd32hIqvo4YuXoNcQ==
|
||||||
|
dependencies:
|
||||||
|
xhr2 "^0.1.4"
|
||||||
|
|
||||||
json-parse-better-errors@^1.0.1:
|
json-parse-better-errors@^1.0.1:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
|
resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
|
||||||
@ -6382,6 +6409,14 @@ react-native-circular-progress@^1.3.9:
|
|||||||
dependencies:
|
dependencies:
|
||||||
prop-types "^15.8.1"
|
prop-types "^15.8.1"
|
||||||
|
|
||||||
|
react-native-fs@^2.20.0:
|
||||||
|
version "2.20.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-native-fs/-/react-native-fs-2.20.0.tgz#05a9362b473bfc0910772c0acbb73a78dbc810f6"
|
||||||
|
integrity sha512-VkTBzs7fIDUiy/XajOSNk0XazFE9l+QlMAce7lGuebZcag5CnjszB+u4BdqzwaQOdcYb5wsJIsqq4kxInIRpJQ==
|
||||||
|
dependencies:
|
||||||
|
base-64 "^0.1.0"
|
||||||
|
utf8 "^3.0.0"
|
||||||
|
|
||||||
react-native-modal@^13.0.1:
|
react-native-modal@^13.0.1:
|
||||||
version "13.0.1"
|
version "13.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/react-native-modal/-/react-native-modal-13.0.1.tgz#691f1e646abb96fa82c1788bf18a16d585da37cd"
|
resolved "https://registry.yarnpkg.com/react-native-modal/-/react-native-modal-13.0.1.tgz#691f1e646abb96fa82c1788bf18a16d585da37cd"
|
||||||
@ -7611,6 +7646,11 @@ use-sync-external-store@1.2.0:
|
|||||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"
|
||||||
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==
|
||||||
|
|
||||||
|
utf8@^3.0.0:
|
||||||
|
version "3.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/utf8/-/utf8-3.0.0.tgz#f052eed1364d696e769ef058b183df88c87f69d1"
|
||||||
|
integrity sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==
|
||||||
|
|
||||||
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
|
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||||
@ -7844,6 +7884,11 @@ xcode@^3.0.1:
|
|||||||
simple-plist "^1.1.0"
|
simple-plist "^1.1.0"
|
||||||
uuid "^7.0.3"
|
uuid "^7.0.3"
|
||||||
|
|
||||||
|
xhr2@^0.1.4:
|
||||||
|
version "0.1.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/xhr2/-/xhr2-0.1.4.tgz#7f87658847716db5026323812f818cadab387a5f"
|
||||||
|
integrity sha512-3QGhDryRzTbIDj+waTRvMBe8SyPhW79kz3YnNb+HQt/6LPYQT3zT3Jt0Y8pBofZqQX26x8Ecfv0FXR72uH5VpA==
|
||||||
|
|
||||||
xml2js@0.6.0:
|
xml2js@0.6.0:
|
||||||
version "0.6.0"
|
version "0.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.6.0.tgz#07afc447a97d2bd6507a1f76eeadddb09f7a8282"
|
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.6.0.tgz#07afc447a97d2bd6507a1f76eeadddb09f7a8282"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user