feat: add file manager app

This commit is contained in:
2024-03-16 16:03:15 +07:00
parent 9eaf90ee2f
commit b1216df09e
18 changed files with 434 additions and 13 deletions
+71
View File
@@ -0,0 +1,71 @@
import { FlatList, Pressable } from "react-native";
import React, { useMemo } 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;
};
type FileListProps = {
files?: FileItem[];
onSelect?: (file: FileItem) => void;
canGoBack?: boolean;
};
const FileList = ({ files, onSelect, canGoBack }: FileListProps) => {
const fileList = useMemo(() => {
if (canGoBack) {
return [{ name: "..", path: "..", isDirectory: true }, ...(files || [])];
}
return files || [];
}, [files, canGoBack]);
return (
<FlatList
contentContainerStyle={cn("bg-white")}
data={fileList || []}
renderItem={({ item }) => (
<FileItem file={item} onPress={() => onSelect?.(item)} />
)}
keyExtractor={(item) => item.path}
/>
);
};
const FileItem = ({
file,
onPress,
}: {
file: FileItem;
onPress?: () => void;
}) => {
return (
<HStack className="bg-white border-b border-gray-200 items-center">
<Pressable
style={({ pressed }) =>
cn(
"flex-1 px-4 py-3 flex flex-row gap-4 items-center",
pressed && "bg-gray-100"
)
}
onPress={onPress}
>
<Ionicons
name={file.isDirectory ? "folder" : "document"}
style={cn(
"text-2xl",
file.isDirectory ? "text-blue-400" : "text-gray-500"
)}
/>
<Text numberOfLines={1}>{file.name}</Text>
</Pressable>
</HStack>
);
};
export default FileList;
+68
View File
@@ -0,0 +1,68 @@
import React, { ComponentProps } from "react";
import Text from "@ui/Text";
import Box from "@ui/Box";
import { Ionicons } from "@ui/Icons";
import { HStack } from "@ui/Stack";
import Button from "@ui/Button";
import { useNavigation } from "expo-router";
import { showDialog } from "@/stores/dialogStore";
import { wakePcUp } from "@/app/apps/lib";
type Props = ComponentProps<typeof Box>;
const Apps = (props: Props) => {
const navigation = useNavigation();
const appList = [
{
name: "Files",
icon: <Ionicons name="folder" />,
path: "files/index",
},
{
name: "Terminal",
icon: <Ionicons name="terminal" />,
path: "terminal",
},
{
name: "Turn on PC",
icon: <Ionicons name="desktop" />,
action: () =>
showDialog(
"Turn on PC",
"Are you sure wanna turn on the PC?",
wakePcUp
),
},
];
return (
<Box {...props}>
<Text className="text-lg md:text-2xl font-medium">Apps</Text>
<HStack className="mt-4 flex-wrap gap-4 md:gap-8">
{appList.map((app, idx) => (
<Button
key={idx}
className="flex-col flex-1 basis-1/3 md:max-w-[160px] h-32 bg-white"
labelClasses="text-sm text-center text-gray-900"
iconClassName="text-2xl mb-1 text-primary"
variant="outline"
icon={app.icon}
label={app.name}
onPress={() => {
if (app.path) {
navigation.navigate(("apps/" + app.path) as never);
}
if (app.action) {
app.action();
}
}}
/>
))}
</HStack>
</Box>
);
};
export default React.memo(Apps);
+101
View File
@@ -0,0 +1,101 @@
import React, { useState } from "react";
import { AnimatedCircularProgress } from "react-native-circular-progress";
import { InferResponseType } from "hono/client";
import api from "@/lib/api";
import Text from "@ui/Text";
import { HStack, VStack } from "@ui/Stack";
import Box from "@ui/Box";
import ProcessList from "./ProcessList";
import Button from "@ui/Button";
import { Ionicons } from "@ui/Icons";
type Props = {
data: InferResponseType<typeof api.system.$get>;
};
const Performance = ({ data: system }: Props) => {
const [showProcess, setShowProcess] = useState(false);
return (
<>
<Text className="text-lg font-medium mt-8">Performance</Text>
<Box className="px-4 py-6 mt-2 bg-white border border-gray-100 rounded-lg relative">
<HStack className="justify-evenly">
<VStack className="items-center">
<AnimatedCircularProgress
size={120}
width={15}
backgroundWidth={5}
fill={system?.perf.cpu.load || 0}
tintColor="#6366F1"
backgroundColor="#3d5875"
arcSweepAngle={240}
rotation={240}
lineCap="round"
>
{() => (
<Text>
<Text className="text-2xl mr-0.5">
{Math.round(system?.perf.cpu.load || 0)}
</Text>
%
</Text>
)}
</AnimatedCircularProgress>
<Text className="-mt-8 text-lg">CPU</Text>
{system ? (
<Text className="text-xs">
{`${system.perf.cpu.speed.toFixed(1)} GHz / ${
system.perf.cpu.temp || 0
}°C`}
</Text>
) : null}
</VStack>
<VStack className="items-center">
<AnimatedCircularProgress
size={120}
width={15}
backgroundWidth={5}
fill={system?.perf.mem.percent || 0}
tintColor="#6366F1"
backgroundColor="#3d5875"
arcSweepAngle={240}
rotation={240}
lineCap="round"
>
{() => (
<Text>
<Text className="text-2xl mr-0.5">
{Math.round(system?.perf.mem.percent || 0)}
</Text>
%
</Text>
)}
</AnimatedCircularProgress>
<Text className="-mt-8 text-lg">Mem</Text>
<Text className="text-xs">{system?.perf.mem.used}</Text>
</VStack>
</HStack>
<Button
icon={
<Ionicons
name="chevron-forward"
style={{
transform: showProcess ? [{ rotate: "90deg" }] : undefined,
}}
/>
}
className="absolute right-0 top-1"
variant="ghost"
onPress={() => setShowProcess(!showProcess)}
/>
{showProcess && <ProcessList />}
</Box>
</>
);
};
export default Performance;
+54
View File
@@ -0,0 +1,54 @@
import api from "@/lib/api";
import Box from "@ui/Box";
import Button from "@ui/Button";
import { HStack, VStack } from "@ui/Stack";
import Text from "@ui/Text";
import React, { useState } from "react";
import { useQuery } from "react-query";
const ProcessList = () => {
const [sort, setSort] = useState<string>("mem");
const { data } = useQuery({
queryKey: ["process", sort],
queryFn: async () => {
return api.process
.$get({ query: { sort, limit: 5 } })
.then((r) => r.json());
},
select: (i) => i.list,
refetchInterval: 1000,
});
return (
<Box className="mt-4">
<HStack className="gap-2 flex-wrap">
<Button
label="Mem"
variant={sort === "mem" ? "default" : "outline"}
size="sm"
onPress={() => setSort("mem")}
/>
<Button
label="CPU"
variant={sort === "cpu" ? "default" : "outline"}
size="sm"
onPress={() => setSort("cpu")}
/>
</HStack>
<VStack className="gap-2 mt-3">
{data?.map((item, idx) => (
<HStack key={idx} className="pb-2 border-b border-gray-200">
<Text className="flex-1" numberOfLines={1}>
{item.cmd}
</Text>
<Text>{sort === "mem" ? item.memUsage : item.cpuPercent}</Text>
</HStack>
))}
</VStack>
</Box>
);
};
export default ProcessList;
+49
View File
@@ -0,0 +1,49 @@
import React from "react";
import { InferResponseType } from "hono/client";
import api from "@/lib/api";
import Text from "@ui/Text";
import Box from "@ui/Box";
import DriveIcon from "@/assets/icons/harddrive.svg";
import { HStack, VStack } from "@ui/Stack";
type Props = {
data: InferResponseType<typeof api.system.$get>;
};
const Storage = ({ data }: Props) => {
return (
<>
<Text className="text-lg font-medium mt-8">Storage</Text>
<HStack className="px-1 py-4 mt-2 bg-white border border-gray-100 rounded-lg gap-3 flex-wrap">
{data?.storage.map((item) => (
<Box key={item.mount} className="flex-1 basis-full sm:basis-[40%]">
<HStack className="flex items-center justify-center gap-2">
<DriveIcon style={{ width: 72, height: 72 }} />
<VStack className="flex-1 gap-1">
<Text className="text-primary font-bold">{item.mount}</Text>
<Text>{`Total: ${item.total}`}</Text>
<Text>{`Free: ${item.free}`}</Text>
</VStack>
</HStack>
<Box className="rounded-full h-2 mx-4 bg-gray-200 overflow-hidden">
<Box
className={[
"rounded-full h-2",
item.percent > 90
? "bg-red-500"
: item.percent > 75
? "bg-yellow-500"
: "bg-primary-400",
]}
style={{ width: `${item.percent}%` }}
></Box>
</Box>
</Box>
))}
</HStack>
</>
);
};
export default Storage;
+24
View File
@@ -0,0 +1,24 @@
import React from "react";
import Box from "@ui/Box";
import Text from "@ui/Text";
import dayjs from "dayjs";
import { InferResponseType } from "hono/client";
import api from "@/lib/api";
type Props = {
data: InferResponseType<typeof api.system.$get>;
};
const Summary = ({ data }: Props) => {
return (
<Box className="px-4 mt-4 py-6 bg-white border border-gray-100 rounded-lg">
<Text className="text-5xl">{dayjs(data?.date).format("HH:mm")}</Text>
<Text className="mt-2">
{dayjs(data?.date).format("dddd, DD MMM YYYY")}
</Text>
<Text className="flex-1">{`Uptime: ${data?.uptime || "-"}`}</Text>
</Box>
);
};
export default Summary;
+32
View File
@@ -0,0 +1,32 @@
import { useNavigation } from "expo-router";
import React from "react";
import { Ionicons } from "./Icons";
import Button from "./Button";
type BackButtonProps = {
prev?: string;
};
const BackButton = ({ prev }: BackButtonProps) => {
const navigation = useNavigation();
const onPress = () => {
if (navigation.canGoBack()) {
navigation.goBack();
} else {
navigation.navigate((prev || "index") as never);
}
};
return (
<Button
icon={<Ionicons name="arrow-back" />}
variant="ghost"
className="h-14 w-14"
iconClassName="text-black text-2xl"
onPress={onPress}
/>
);
};
export default BackButton;