mirror of
				https://github.com/khairul169/home-lab.git
				synced 2025-11-04 13:41:07 +07:00 
			
		
		
		
	feat: add file operation
This commit is contained in:
		
							parent
							
								
									b7c1ceb2b3
								
							
						
					
					
						commit
						a371a9e569
					
				@ -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;
 | 
			
		||||
 | 
			
		||||
@ -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 (
 | 
			
		||||
    <>
 | 
			
		||||
      <Stack.Screen
 | 
			
		||||
        options={{ headerLeft: () => <BackButton />, title: "Files" }}
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <Box className="px-2 py-2 bg-white">
 | 
			
		||||
      <HStack className="px-2 py-2 bg-white gap-2">
 | 
			
		||||
        <Button
 | 
			
		||||
          icon={<Ionicons name="chevron-back" />}
 | 
			
		||||
          disabled={parentPath == null}
 | 
			
		||||
          className="px-3 border-gray-300"
 | 
			
		||||
          labelClasses="text-gray-500"
 | 
			
		||||
          variant="outline"
 | 
			
		||||
          onPress={() => setParams({ ...params, path: parentPath })}
 | 
			
		||||
        />
 | 
			
		||||
        <Input
 | 
			
		||||
          placeholder="/"
 | 
			
		||||
          value={params.path}
 | 
			
		||||
          onChangeText={(path) => setParams({ path })}
 | 
			
		||||
          className="flex-1"
 | 
			
		||||
        />
 | 
			
		||||
      </Box>
 | 
			
		||||
      </HStack>
 | 
			
		||||
 | 
			
		||||
      <FileList
 | 
			
		||||
        files={data}
 | 
			
		||||
        onSelect={(file) => {
 | 
			
		||||
          if (file.path === "..") {
 | 
			
		||||
            return setParams({ ...params, path: parentPath });
 | 
			
		||||
          }
 | 
			
		||||
          if (file.isDirectory) {
 | 
			
		||||
            return setParams({ ...params, path: file.path });
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          openFile(file);
 | 
			
		||||
        }}
 | 
			
		||||
        canGoBack={parentPath != null}
 | 
			
		||||
      />
 | 
			
		||||
      <FileDrop onFileDrop={onFileDrop} isDisabled={upload.isLoading}>
 | 
			
		||||
        <FileList
 | 
			
		||||
          files={data}
 | 
			
		||||
          onSelect={(file) => {
 | 
			
		||||
            if (file.isDirectory) {
 | 
			
		||||
              return setParams({ ...params, path: file.path });
 | 
			
		||||
            }
 | 
			
		||||
            openFile(file);
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      </FileDrop>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { FileItem } from "@/components/pages/files/FileList";
 | 
			
		||||
import { API_BASEURL } from "@/lib/constants";
 | 
			
		||||
import authStore from "@/stores/authStore";
 | 
			
		||||
import { FileItem } from "@/types/files";
 | 
			
		||||
 | 
			
		||||
export function openFile(file: FileItem, dl = false) {
 | 
			
		||||
  const url = getFileUrl(file, dl);
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										97
									
								
								src/components/pages/files/FileDrop.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								src/components/pages/files/FileDrop.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,97 @@
 | 
			
		||||
// @ts-ignore
 | 
			
		||||
import { cn } from "@/lib/utils";
 | 
			
		||||
import Box from "@ui/Box";
 | 
			
		||||
import { Ionicons } from "@ui/Icons";
 | 
			
		||||
import Text from "@ui/Text";
 | 
			
		||||
import React, { useRef, useState } from "react";
 | 
			
		||||
import { Platform } from "react-native";
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
  children: React.ReactNode;
 | 
			
		||||
  isDisabled?: boolean;
 | 
			
		||||
  onFileDrop?: (files: File[]) => void;
 | 
			
		||||
  onDrop?: React.DragEventHandler<HTMLDivElement>;
 | 
			
		||||
  onDragOver?: React.DragEventHandler<HTMLDivElement>;
 | 
			
		||||
  onDragLeave?: React.DragEventHandler<HTMLDivElement>;
 | 
			
		||||
  className?: string;
 | 
			
		||||
};
 | 
			
		||||
const isWeb = Platform.OS === "web";
 | 
			
		||||
 | 
			
		||||
const FileDrop = ({ className, children, isDisabled, ...props }: Props) => {
 | 
			
		||||
  const dragContainerRef = useRef<any>(null);
 | 
			
		||||
  const overlayRef = useRef<any>(null);
 | 
			
		||||
  const [isDragging, setDragging] = useState(false);
 | 
			
		||||
 | 
			
		||||
  if (!isWeb) {
 | 
			
		||||
    return children;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      style={cn("flex-1 relative flex overflow-hidden")}
 | 
			
		||||
      ref={dragContainerRef}
 | 
			
		||||
      onDrop={(e) => {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        if (!isDragging || isDisabled) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setDragging(false);
 | 
			
		||||
        props.onDrop && props.onDrop(e);
 | 
			
		||||
 | 
			
		||||
        if (props.onFileDrop) {
 | 
			
		||||
          const files = Array.from(e.dataTransfer.items)
 | 
			
		||||
            .filter((i) => i.kind === "file")
 | 
			
		||||
            .map((i) => i.getAsFile());
 | 
			
		||||
          props.onFileDrop(files);
 | 
			
		||||
        }
 | 
			
		||||
      }}
 | 
			
		||||
      onDragOver={(e) => {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        if (isDragging || isDisabled) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // ignore if not a file
 | 
			
		||||
        if (
 | 
			
		||||
          !e.dataTransfer.items ||
 | 
			
		||||
          !e.dataTransfer.items.length ||
 | 
			
		||||
          e.dataTransfer.items[0].kind !== "file"
 | 
			
		||||
        ) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setDragging(true);
 | 
			
		||||
        props.onDragOver && props.onDragOver(e);
 | 
			
		||||
      }}
 | 
			
		||||
      onDragLeave={(e) => {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        if (!isDragging || e.target !== overlayRef.current) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setDragging(false);
 | 
			
		||||
        props.onDragLeave && props.onDragLeave(e);
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      {children}
 | 
			
		||||
 | 
			
		||||
      {isDragging && (
 | 
			
		||||
        <Box
 | 
			
		||||
          ref={overlayRef}
 | 
			
		||||
          className="flex flex-col items-center justify-center absolute top-0 left-0 w-full h-full bg-black/10 z-10"
 | 
			
		||||
        >
 | 
			
		||||
          <Box
 | 
			
		||||
            className="bg-white p-8 rounded-xl flex flex-col items-center gap-2"
 | 
			
		||||
            style={{ pointerEvents: "none" }}
 | 
			
		||||
          >
 | 
			
		||||
            <Ionicons name="cloud-upload" style={{ fontSize: 48 }} />
 | 
			
		||||
            <Text className="text-primary">Drop files here</Text>
 | 
			
		||||
          </Box>
 | 
			
		||||
        </Box>
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default React.memo(FileDrop);
 | 
			
		||||
@ -1,48 +1,54 @@
 | 
			
		||||
import { FlatList, Pressable } from "react-native";
 | 
			
		||||
import React, { useMemo } from "react";
 | 
			
		||||
import { FlatList } from "react-native";
 | 
			
		||||
import React from "react";
 | 
			
		||||
import Text from "@ui/Text";
 | 
			
		||||
import { HStack } from "@ui/Stack";
 | 
			
		||||
import { cn } from "@/lib/utils";
 | 
			
		||||
import { Ionicons } from "@ui/Icons";
 | 
			
		||||
 | 
			
		||||
export type FileItem = {
 | 
			
		||||
  name: string;
 | 
			
		||||
  path: string;
 | 
			
		||||
  isDirectory: boolean;
 | 
			
		||||
};
 | 
			
		||||
import Button from "@ui/Button";
 | 
			
		||||
import Pressable from "@ui/Pressable";
 | 
			
		||||
import { FileItem } from "@/types/files";
 | 
			
		||||
import FileMenu, { openFileMenu } from "./FileMenu";
 | 
			
		||||
 | 
			
		||||
type FileListProps = {
 | 
			
		||||
  files?: FileItem[];
 | 
			
		||||
  onSelect?: (file: FileItem) => void;
 | 
			
		||||
  canGoBack?: boolean;
 | 
			
		||||
  // onMenu?: (file: FileItem) => void;
 | 
			
		||||
  onLongPress?: (file: FileItem) => void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const FileList = ({ files, onSelect, canGoBack }: FileListProps) => {
 | 
			
		||||
  const fileList = useMemo(() => {
 | 
			
		||||
    if (canGoBack) {
 | 
			
		||||
      return [{ name: "..", path: "..", isDirectory: true }, ...(files || [])];
 | 
			
		||||
    }
 | 
			
		||||
    return files || [];
 | 
			
		||||
  }, [files, canGoBack]);
 | 
			
		||||
 | 
			
		||||
const FileList = ({ files, onSelect, onLongPress }: FileListProps) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <FlatList
 | 
			
		||||
      contentContainerStyle={cn("bg-white")}
 | 
			
		||||
      data={fileList || []}
 | 
			
		||||
      renderItem={({ item }) => (
 | 
			
		||||
        <FileItem file={item} onPress={() => onSelect?.(item)} />
 | 
			
		||||
      )}
 | 
			
		||||
      keyExtractor={(item) => item.path}
 | 
			
		||||
    />
 | 
			
		||||
    <>
 | 
			
		||||
      <FlatList
 | 
			
		||||
        style={cn("flex-1")}
 | 
			
		||||
        contentContainerStyle={cn("bg-white")}
 | 
			
		||||
        data={files || []}
 | 
			
		||||
        renderItem={({ item }) => (
 | 
			
		||||
          <FileItemList
 | 
			
		||||
            file={item}
 | 
			
		||||
            onPress={() => onSelect?.(item)}
 | 
			
		||||
            onLongPress={() => onLongPress?.(item)}
 | 
			
		||||
            onMenuPress={() => openFileMenu(item)}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
        keyExtractor={(item) => item.path}
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <FileMenu />
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const FileItem = ({
 | 
			
		||||
const FileItemList = ({
 | 
			
		||||
  file,
 | 
			
		||||
  onPress,
 | 
			
		||||
  onLongPress,
 | 
			
		||||
  onMenuPress,
 | 
			
		||||
}: {
 | 
			
		||||
  file: FileItem;
 | 
			
		||||
  onPress?: () => void;
 | 
			
		||||
  onLongPress?: () => void;
 | 
			
		||||
  onMenuPress?: () => void;
 | 
			
		||||
}) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <HStack className="bg-white border-b border-gray-200 items-center">
 | 
			
		||||
@ -54,6 +60,13 @@ const FileItem = ({
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
        onPress={onPress}
 | 
			
		||||
        onLongPress={onLongPress}
 | 
			
		||||
        onContextMenu={(e) => {
 | 
			
		||||
          if (onMenuPress) {
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
            onMenuPress();
 | 
			
		||||
          }
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Ionicons
 | 
			
		||||
          name={file.isDirectory ? "folder" : "document"}
 | 
			
		||||
@ -64,6 +77,12 @@ const FileItem = ({
 | 
			
		||||
        />
 | 
			
		||||
        <Text numberOfLines={1}>{file.name}</Text>
 | 
			
		||||
      </Pressable>
 | 
			
		||||
      <Button
 | 
			
		||||
        icon={<Ionicons name="ellipsis-vertical" />}
 | 
			
		||||
        variant="ghost"
 | 
			
		||||
        className="h-full px-4"
 | 
			
		||||
        onPress={onMenuPress}
 | 
			
		||||
      />
 | 
			
		||||
    </HStack>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										52
									
								
								src/components/pages/files/FileMenu.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/components/pages/files/FileMenu.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,52 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { createStore, useStore } from "zustand";
 | 
			
		||||
import { FileItem } from "@/types/files";
 | 
			
		||||
import Text from "@ui/Text";
 | 
			
		||||
import List from "@ui/List";
 | 
			
		||||
import { Ionicons } from "@ui/Icons";
 | 
			
		||||
import { cn } from "@/lib/utils";
 | 
			
		||||
import ActionSheet from "@ui/ActionSheet";
 | 
			
		||||
import { HStack } from "@ui/Stack";
 | 
			
		||||
import Button from "@ui/Button";
 | 
			
		||||
 | 
			
		||||
type Store = {
 | 
			
		||||
  isVisible: boolean;
 | 
			
		||||
  file?: FileItem | null;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const store = createStore<Store>(() => ({
 | 
			
		||||
  isVisible: false,
 | 
			
		||||
  file: null,
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
export const openFileMenu = (file: FileItem) => {
 | 
			
		||||
  store.setState({ isVisible: true, file });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const FileMenu = () => {
 | 
			
		||||
  const { isVisible, file } = useStore(store);
 | 
			
		||||
  const onClose = () => store.setState({ isVisible: false });
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ActionSheet isVisible={isVisible} onClose={onClose}>
 | 
			
		||||
      <Text className="text-lg md:text-xl" numberOfLines={1}>
 | 
			
		||||
        {file?.name}
 | 
			
		||||
      </Text>
 | 
			
		||||
 | 
			
		||||
      <List className="mt-4">
 | 
			
		||||
        <List.Item icon={<Ionicons name="pencil" />}>Rename</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="trash" />}>Delete</List.Item>
 | 
			
		||||
      </List>
 | 
			
		||||
 | 
			
		||||
      <HStack className="justify-end mt-6">
 | 
			
		||||
        <Button variant="ghost" onPress={onClose}>
 | 
			
		||||
          Cancel
 | 
			
		||||
        </Button>
 | 
			
		||||
      </HStack>
 | 
			
		||||
    </ActionSheet>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default FileMenu;
 | 
			
		||||
							
								
								
									
										26
									
								
								src/components/ui/ActionSheet.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/components/ui/ActionSheet.tsx
									
									
									
									
									
										Normal file
									
								
							@ -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<ComponentProps<typeof Modal>> & {
 | 
			
		||||
  onClose?: () => void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const ActionSheet = ({ onClose, children, ...props }: ActionSheetProps) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Modal
 | 
			
		||||
      style={cn("justify-end md:justify-center m-0 md:m-4")}
 | 
			
		||||
      onBackButtonPress={onClose}
 | 
			
		||||
      onBackdropPress={onClose}
 | 
			
		||||
      backdropOpacity={0.3}
 | 
			
		||||
      {...props}
 | 
			
		||||
    >
 | 
			
		||||
      <Container className="bg-white p-4 md:p-8 rounded-t-xl md:rounded-xl">
 | 
			
		||||
        {children}
 | 
			
		||||
      </Container>
 | 
			
		||||
    </Modal>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default ActionSheet;
 | 
			
		||||
@ -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<typeof View>;
 | 
			
		||||
 | 
			
		||||
const Box = ({ className, style, ...props }: Props) => {
 | 
			
		||||
const Box = forwardRef(({ className, style, ...props }: Props, ref: any) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <View style={{ ...cn(className), ...((style || {}) as any) }} {...props} />
 | 
			
		||||
    <View
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      style={{ ...cn(className), ...((style || {}) as any) }}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default Box;
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										38
									
								
								src/components/ui/List.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/components/ui/List.tsx
									
									
									
									
									
										Normal file
									
								
							@ -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 <VStack className={cn(className)}>{children}</VStack>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type ListItemProps = {
 | 
			
		||||
  className?: any;
 | 
			
		||||
  children: React.ReactNode;
 | 
			
		||||
  icon?: React.ReactNode;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const ListItem = ({ className, icon, children }: ListItemProps) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Pressable style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })}>
 | 
			
		||||
      <HStack className={cn("py-2 border-b border-gray-200", className)}>
 | 
			
		||||
        {icon ? (
 | 
			
		||||
          <Slot.Text style={cn("text-gray-800 text-xl w-8")}>{icon}</Slot.Text>
 | 
			
		||||
        ) : null}
 | 
			
		||||
 | 
			
		||||
        <Text>{children}</Text>
 | 
			
		||||
      </HStack>
 | 
			
		||||
    </Pressable>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
List.Item = ListItem;
 | 
			
		||||
 | 
			
		||||
export default List;
 | 
			
		||||
							
								
								
									
										12
									
								
								src/components/ui/Pressable.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/components/ui/Pressable.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
			
		||||
import { ComponentProps, forwardRef } from "react";
 | 
			
		||||
import { Pressable as BasePressable } from "react-native";
 | 
			
		||||
 | 
			
		||||
type Props = ComponentProps<typeof BasePressable> & {
 | 
			
		||||
  onContextMenu?: (event: PointerEvent) => void;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const Pressable = forwardRef((props: Props, ref: any) => {
 | 
			
		||||
  return <BasePressable ref={ref} {...props} />;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default Pressable;
 | 
			
		||||
							
								
								
									
										5
									
								
								src/types/files.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/types/files.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
export type FileItem = {
 | 
			
		||||
  name: string;
 | 
			
		||||
  path: string;
 | 
			
		||||
  isDirectory: boolean;
 | 
			
		||||
};
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user