From 7ca128a02ccbbcfcd499970d85b08560f52ee664 Mon Sep 17 00:00:00 2001 From: Khairul Hidayat Date: Sun, 17 Mar 2024 07:03:24 +0700 Subject: [PATCH] feat: add inline file viewer, add audio player --- backend/routes/files.ts | 48 ++--- bun.lockb | Bin 436680 -> 437048 bytes global.d.ts | 5 + package.json | 6 + src/app/apps/files/index.tsx | 35 +++- src/app/apps/files/utils.ts | 9 +- src/assets/images/audioplayer-bg.jpeg | Bin 0 -> 83041 bytes .../pages/files/FileInlineViewer.tsx | 97 +++++++++ src/components/pages/files/FileList.tsx | 2 +- src/components/pages/files/FileMenu.tsx | 11 +- src/components/pages/files/FilesContext.tsx | 12 ++ src/components/ui/AudioPlayer.tsx | 196 ++++++++++++++++++ src/components/ui/List.tsx | 8 +- src/lib/utils.ts | 49 +++++ src/types/mediaTags.ts | 83 ++++++++ yarn.lock | 45 ++++ 16 files changed, 565 insertions(+), 41 deletions(-) create mode 100644 src/assets/images/audioplayer-bg.jpeg create mode 100644 src/components/pages/files/FileInlineViewer.tsx create mode 100644 src/components/pages/files/FilesContext.tsx create mode 100644 src/components/ui/AudioPlayer.tsx create mode 100644 src/types/mediaTags.ts diff --git a/backend/routes/files.ts b/backend/routes/files.ts index 25df8b7..94f38f9 100644 --- a/backend/routes/files.ts +++ b/backend/routes/files.ts @@ -28,9 +28,7 @@ const filesDirList = process.env.FILE_DIRS const route = new Hono() .get("/", zValidator("query", getFilesSchema), async (c) => { const input: z.infer = c.req.query(); - const pathname = (input.path || "").split("/"); - const path = pathname.slice(2).join("/"); - const baseName = pathname[1]; + const { baseName, path, pathname } = getFilePath(input.path); if (!baseName?.length) { 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 { - const cwd = baseDir + "/" + path; - const entities = await fs.readdir(cwd, { withFileTypes: true }); + const entities = await fs.readdir(path, { withFileTypes: true }); const files = entities .filter((e) => !e.name.startsWith(".")) .map((e) => ({ name: e.name, - path: "/" + [baseName, path, e.name].filter(Boolean).join("/"), + path: [pathname, e.name].join("/"), isDirectory: e.isDirectory(), })) .sort((a, b) => { @@ -90,16 +82,11 @@ const route = new Hono() throw new HTTPException(400, { message: "Files is empty!" }); } - const pathSlices = data.path.split("/"); - const baseName = pathSlices[1] || null; - const path = pathSlices.slice(2).join("/"); - const baseDir = filesDirList.find((i) => i.name === baseName)?.path; + const { baseDir, path: targetDir } = getFilePath(data.path); if (!baseDir?.length) { throw new HTTPException(400, { message: "Path not found!" }); } - const targetDir = [baseDir, path].join("/"); - // files.forEach((file) => { // const filepath = targetDir + "/" + file.name; // if (existsSync(filepath)) { @@ -124,15 +111,16 @@ const route = new Hono() const pathSlice = pathname.slice(pathname.indexOf("download") + 1); const baseName = pathSlice[0]; const path = "/" + pathSlice.slice(1).join("/"); + const filename = path.substring(1); try { if (!baseName?.length) { - throw new Error(); + throw new Error("baseName is empty"); } const baseDir = filesDirList.find((i) => i.name === baseName)?.path; if (!baseDir) { - throw new Error(); + throw new Error("baseDir not found"); } const filepath = baseDir + path; @@ -141,7 +129,10 @@ const route = new Hono() if (dlFile) { c.header("Content-Type", "application/octet-stream"); - c.header("Content-Disposition", `attachment; filename="${path}"`); + c.header( + "Content-Disposition", + `attachment; filename="${encodeURIComponent(filename)}"` + ); } else { c.header("Content-Type", getMimeType(filepath)); } @@ -177,23 +168,24 @@ const route = new Hono() return c.body(stream, 206); } catch (err) { + // console.log("err", err); throw new HTTPException(404, { message: "Not Found!" }); } }); -function getFilePath(path: string) { - const pathSlices = path.split("/"); +function getFilePath(path?: string) { + const pathSlices = + path + ?.replace(/\/{2,}/g, "/") + .replace(/\/$/, "") + .split("/") || []; const baseName = pathSlices[1] || null; const filePath = pathSlices.slice(2).join("/"); - const baseDir = filesDirList.find((i) => i.name === baseName)?.path; - if (!baseDir?.length) { - throw new HTTPException(400, { message: "Path not found!" }); - } return { - path: [baseDir, filePath].join("/"), - pathname: ["", baseName, filePath].join("/"), + path: [baseDir || "", filePath].join("/").replace(/\/$/, ""), + pathname: ["", baseName, filePath].join("/").replace(/\/$/, ""), baseName, baseDir, filePath, diff --git a/bun.lockb b/bun.lockb index e89c5382ce91afab926b8f1da85bc168ffa33720..1a7792720ff230afcb87d9d66966d694ff76a1a8 100755 GIT binary patch delta 28765 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 R-65?41>&dAPgOnhe*yf|x-S3# delta 27979 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 export default content; } + +declare module "*.jpg"; +declare module "*.jpeg"; +declare module "*.png"; +declare module "*.webp"; diff --git a/package.json b/package.json index 2b3b22d..f712080 100644 --- a/package.json +++ b/package.json @@ -13,21 +13,26 @@ "dependencies": { "@expo/metro-runtime": "~3.1.3", "@hookform/resolvers": "^3.3.4", + "@miblanchard/react-native-slider": "^2.3.1", "@react-native-async-storage/async-storage": "1.21.0", "@types/react": "~18.2.45", + "base-64": "^1.0.0", "class-variance-authority": "^0.7.0", "dayjs": "^1.11.10", "expo": "~50.0.11", + "expo-av": "~13.10.5", "expo-constants": "~15.4.5", "expo-linking": "~6.2.2", "expo-router": "~3.4.8", "expo-status-bar": "~1.11.1", "hono": "^4.1.0", + "jsmediatags": "^3.9.7", "react": "18.2.0", "react-dom": "18.2.0", "react-hook-form": "^7.51.0", "react-native": "0.73.4", "react-native-circular-progress": "^1.3.9", + "react-native-fs": "^2.20.0", "react-native-modal": "^13.0.1", "react-native-safe-area-context": "4.8.2", "react-native-screens": "~3.29.0", @@ -37,6 +42,7 @@ "react-query": "^3.39.3", "twrnc": "^4.1.0", "typescript": "^5.3.0", + "utf8": "^3.0.0", "xterm": "^5.3.0", "xterm-addon-attach": "^0.9.0", "xterm-addon-fit": "^0.8.0", diff --git a/src/app/apps/files/index.tsx b/src/app/apps/files/index.tsx index 5ce94c1..f36b421 100644 --- a/src/app/apps/files/index.tsx +++ b/src/app/apps/files/index.tsx @@ -4,21 +4,24 @@ import api from "@/lib/api"; import { useAuth } from "@/stores/authStore"; import BackButton from "@ui/BackButton"; import Input from "@ui/Input"; -import { Stack } from "expo-router"; +import { Stack, router, useLocalSearchParams } from "expo-router"; import React from "react"; import { useMutation, useQuery } from "react-query"; -import { openFile } from "./utils"; import FileDrop from "@/components/pages/files/FileDrop"; import { showToast } from "@/stores/toastStore"; import { HStack } from "@ui/Stack"; import Button from "@ui/Button"; 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 { isLoggedIn } = useAuth(); const [params, setParams] = useAsyncStorage("files", { path: "", }); + const searchParams = useLocalSearchParams(); const parentPath = params.path.length > 0 ? params.path.split("/").slice(0, -1).join("/") @@ -56,8 +59,12 @@ const FilesPage = () => { } }; + if (!isLoggedIn) { + return null; + } + return ( - <> + , title: "Files" }} /> @@ -71,6 +78,13 @@ const FilesPage = () => { variant="outline" onPress={() => setParams({ ...params, path: parentPath })} /> + + + ); +}; + +const FileInlineViewer = ({ path, onClose }: Props) => { + const filename = path?.split("/").pop(); + + return ( + + + + diff --git a/src/components/pages/files/FilesContext.tsx b/src/components/pages/files/FilesContext.tsx new file mode 100644 index 0000000..b66549d --- /dev/null +++ b/src/components/pages/files/FilesContext.tsx @@ -0,0 +1,12 @@ +import { FileItem } from "@/types/files"; +import { createContext, useContext } from "react"; + +type FilesContextType = { + files: FileItem[]; +}; + +export const FilesContext = createContext({ + files: [], +}); + +export const useFilesContext = () => useContext(FilesContext); diff --git a/src/components/ui/AudioPlayer.tsx b/src/components/ui/AudioPlayer.tsx new file mode 100644 index 0000000..5502b5b --- /dev/null +++ b/src/components/ui/AudioPlayer.tsx @@ -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(null); + const [curFileIdx, setFileIdx] = useState(-1); + const [status, setStatus] = useState(null); + const [mediaTags, setMediaTags] = useState(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 ( + + + + + + + {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); + }} + /> + + +