diff --git a/backend/routes/files.ts b/backend/routes/files.ts index 6d48c78..25df8b7 100644 --- a/backend/routes/files.ts +++ b/backend/routes/files.ts @@ -13,6 +13,11 @@ const getFilesSchema = z .partial() .optional(); +const uploadSchema = z.object({ + path: z.string().min(1), + size: z.string().min(1), +}); + const filesDirList = process.env.FILE_DIRS ? process.env.FILE_DIRS.split(";").map((i) => ({ name: i.split("/").at(-1), @@ -68,6 +73,50 @@ const route = new Hono() return c.json([]); }) + .post("/upload", async (c) => { + const input: any = (await c.req.parseBody()) as never; + const data = await uploadSchema.parseAsync(input); + + const size = parseInt(input.size); + if (Number.isNaN(size) || !size) { + throw new HTTPException(400, { message: "Size is empty!" }); + } + + const files: File[] = [...Array(size)] + .map((_, idx) => input[`files.${idx}`]) + .filter((i) => !!i); + + if (!files.length) { + 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; + 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)) { + // throw new HTTPException(400, { message: "File already exists!" }); + // } + // }); + + await Promise.all( + files.map(async (file) => { + const filepath = targetDir + "/" + file.name; + const buffer = await file.arrayBuffer(); + await fs.writeFile(filepath, new Uint8Array(buffer)); + }) + ); + + return c.json({ success: true }); + }) .get("/download/*", async (c) => { const dlFile = c.req.query("dl") === "true"; const url = new URL(c.req.url, `http://${c.req.header("host")}`); @@ -132,4 +181,23 @@ const route = new Hono() } }); +function getFilePath(path: string) { + const pathSlices = path.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("/"), + baseName, + baseDir, + filePath, + }; +} + export default route; diff --git a/src/app/apps/files/index.tsx b/src/app/apps/files/index.tsx index 5c821b7..5ce94c1 100644 --- a/src/app/apps/files/index.tsx +++ b/src/app/apps/files/index.tsx @@ -1,14 +1,18 @@ -import FileList, { FileItem } from "@/components/pages/files/FileList"; +import FileList from "@/components/pages/files/FileList"; import { useAsyncStorage } from "@/hooks/useAsyncStorage"; import api from "@/lib/api"; import { useAuth } from "@/stores/authStore"; import BackButton from "@ui/BackButton"; -import Box from "@ui/Box"; import Input from "@ui/Input"; import { Stack } from "expo-router"; import React from "react"; -import { useQuery } from "react-query"; +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"; const FilesPage = () => { const { isLoggedIn } = useAuth(); @@ -20,40 +24,72 @@ const FilesPage = () => { ? params.path.split("/").slice(0, -1).join("/") : null; - const { data } = useQuery({ + const { data, refetch } = useQuery({ queryKey: ["app/files", params], queryFn: () => api.files.$get({ query: params }).then((r) => r.json()), enabled: isLoggedIn, }); + const upload = useMutation({ + mutationFn: async (files: File[]) => { + const form: any = { + path: params.path, + size: files.length, + }; + + files.forEach((file, idx) => { + form[`files.${idx}`] = file; + }); + + const res = await api.files.upload.$post({ form }); + return res.json(); + }, + onSuccess: () => { + showToast("Upload success!"); + refetch(); + }, + }); + + const onFileDrop = (files: File[]) => { + if (!upload.isLoading) { + upload.mutate(files); + } + }; + return ( <> , title: "Files" }} /> - + + + + + ); +}; + +export default FileMenu; diff --git a/src/components/ui/ActionSheet.tsx b/src/components/ui/ActionSheet.tsx new file mode 100644 index 0000000..edb2e5b --- /dev/null +++ b/src/components/ui/ActionSheet.tsx @@ -0,0 +1,26 @@ +import React, { ComponentProps } from "react"; +import Modal from "react-native-modal"; +import { cn } from "@/lib/utils"; +import Container from "./Container"; + +type ActionSheetProps = Partial> & { + onClose?: () => void; +}; + +const ActionSheet = ({ onClose, children, ...props }: ActionSheetProps) => { + return ( + + + {children} + + + ); +}; + +export default ActionSheet; diff --git a/src/components/ui/Box.tsx b/src/components/ui/Box.tsx index 5524e56..d254c83 100644 --- a/src/components/ui/Box.tsx +++ b/src/components/ui/Box.tsx @@ -1,13 +1,18 @@ import { cn } from "@/lib/utils"; import { ComponentPropsWithClassName } from "@/types/components"; +import { forwardRef } from "react"; import { View } from "react-native"; type Props = ComponentPropsWithClassName; -const Box = ({ className, style, ...props }: Props) => { +const Box = forwardRef(({ className, style, ...props }: Props, ref: any) => { return ( - + ); -}; +}); export default Box; diff --git a/src/components/ui/List.tsx b/src/components/ui/List.tsx new file mode 100644 index 0000000..cd65c9b --- /dev/null +++ b/src/components/ui/List.tsx @@ -0,0 +1,38 @@ +import React from "react"; +import { HStack, VStack } from "./Stack"; +import { cn } from "@/lib/utils"; +import Pressable from "./Pressable"; +import Text from "./Text"; +import Slot from "./Slot"; + +type Props = { + className?: any; + children: React.ReactNode; +}; + +const List = ({ className, children }: Props) => { + return {children}; +}; + +type ListItemProps = { + className?: any; + children: React.ReactNode; + icon?: React.ReactNode; +}; + +const ListItem = ({ className, icon, children }: ListItemProps) => { + return ( + ({ opacity: pressed ? 0.7 : 1 })}> + + {icon ? ( + {icon} + ) : null} + + {children} + + + ); +}; +List.Item = ListItem; + +export default List; diff --git a/src/components/ui/Pressable.tsx b/src/components/ui/Pressable.tsx new file mode 100644 index 0000000..a599aaa --- /dev/null +++ b/src/components/ui/Pressable.tsx @@ -0,0 +1,12 @@ +import { ComponentProps, forwardRef } from "react"; +import { Pressable as BasePressable } from "react-native"; + +type Props = ComponentProps & { + onContextMenu?: (event: PointerEvent) => void; +}; + +const Pressable = forwardRef((props: Props, ref: any) => { + return ; +}); + +export default Pressable; diff --git a/src/types/files.ts b/src/types/files.ts new file mode 100644 index 0000000..fbdeb23 --- /dev/null +++ b/src/types/files.ts @@ -0,0 +1,5 @@ +export type FileItem = { + name: string; + path: string; + isDirectory: boolean; +};