mirror of
https://github.com/khairul169/home-lab.git
synced 2026-10-02 17:13:57 +07:00
feat: add file manager app
This commit is contained in:
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user