feat: add terminal & wake pc

This commit is contained in:
2024-03-16 10:14:17 +07:00
parent 1e8252d9da
commit 5e8a3c88a2
21 changed files with 782 additions and 21 deletions
+7
View File
@@ -7,8 +7,10 @@ import { cn, tw } from "@/lib/utils";
import { useDeviceContext } from "twrnc";
import { StatusBar } from "expo-status-bar";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import Toast from "react-native-toast-notifications";
import { useStore } from "zustand";
import authStore from "@/stores/authStore";
import { toastStore } from "@/stores/toastStore";
const RootLayout = () => {
const insets = useSafeAreaInsets();
@@ -39,6 +41,11 @@ const RootLayout = () => {
<View style={cn("flex-1 bg-[#f2f7fb]", { paddingTop: insets.top })}>
<Slot />
</View>
<Toast
ref={(ref) => {
toastStore.setState(ref);
}}
/>
</QueryClientProvider>
);
};
+13
View File
@@ -0,0 +1,13 @@
// src/app/apps/lib.ts
import api from "@/lib/api";
import { showToast } from "@/stores/toastStore";
export const wakePcUp = async () => {
try {
await api.apps.wakepc.$post();
showToast("Waking up PC...");
} catch (err) {
showToast("Cannot wake up the PC!", { type: "danger" });
}
};
+93
View File
@@ -0,0 +1,93 @@
import { Platform } from "react-native";
import { Terminal } from "xterm";
import { FitAddon } from "xterm-addon-fit";
import { AttachAddon } from "xterm-addon-attach";
import Box from "@ui/Box";
import Text from "@ui/Text";
import React, { useEffect, useRef } from "react";
import "xterm/css/xterm.css";
import { API_BASEURL } from "@/lib/constants";
import authStore, { useAuth } from "@/stores/authStore";
const isWeb = Platform.OS === "web";
const TerminalPage = () => {
const { token } = useAuth();
const terminalRef = useRef<any>();
const fitRef = useRef<FitAddon | null>(null);
useEffect(() => {
if (!isWeb || !token) {
return;
}
const term = new Terminal({ theme: { background: "#1d1e2b" } });
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
const baseUrl = API_BASEURL.replace("https://", "wss://").replace(
"http://",
"ws://"
);
const socket = new WebSocket(baseUrl + "/terminal?token=" + token);
const attachAddon = new AttachAddon(socket);
// Attach the socket to term
term.loadAddon(attachAddon);
term.open(terminalRef.current);
fitAddon.fit();
fitRef.current = fitAddon;
const sendResizeSignal = (data: { cols: number; rows: number }) => {
socket.send("resize:" + [data.cols, data.rows].join(","));
};
term.onResize(sendResizeSignal);
setTimeout(() => {
sendResizeSignal({ cols: term.cols, rows: term.rows });
}, 1000);
return () => {
attachAddon.dispose();
fitAddon.dispose();
term.dispose();
fitRef.current = null;
};
}, [token]);
useEffect(() => {
if (!isWeb) {
return;
}
const onResize = () => {
if (fitRef.current) {
fitRef.current.fit();
}
};
window.addEventListener("resize", onResize);
return () => {
window.removeEventListener("resize", onResize);
};
}, []);
if (!isWeb) {
return (
<Box className="p-8">
<Text className="text-center">Only web platform suppported.</Text>
</Box>
);
}
return (
<div
ref={terminalRef}
style={{ height: "100vh", background: "#1d1e2b", padding: 16 }}
/>
);
};
export default TerminalPage;
+57
View File
@@ -0,0 +1,57 @@
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 { wakePcUp } from "@/app/apps/lib";
type Props = ComponentProps<typeof Box>;
const Apps = (props: Props) => {
const navigation = useNavigation();
const appList = [
{
name: "Terminal",
icon: <Ionicons name="terminal" />,
path: "terminal",
},
{
name: "PC Control",
icon: <Ionicons name="desktop" />,
action: 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);
+1 -1
View File
@@ -11,7 +11,7 @@ type Props = {
const Summary = ({ data }: Props) => {
return (
<Box className="px-4 py-6 mt-4 bg-white border border-gray-100 rounded-lg">
<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")}
+14 -6
View File
@@ -7,6 +7,9 @@ import Summary from "./_sections/Summary";
import Storage from "./_sections/Storage";
import Container from "@ui/Container";
import { useAuth } from "@/stores/authStore";
import { HStack } from "@ui/Stack";
import Box from "@ui/Box";
import Apps from "./_sections/Apps";
const HomePage = () => {
const { isLoggedIn } = useAuth();
@@ -22,12 +25,17 @@ const HomePage = () => {
}
return (
<Container scrollable className="px-4 py-8 md:py-16">
<Text className="text-2xl font-medium">Home Lab</Text>
<Summary data={system} />
<Performance data={system} />
<Storage data={system} />
<Container scrollable className="px-4 md:px-8 max-w-none py-8">
<HStack className="items-start gap-8">
<Box className="flex-1 md:max-w-lg">
<Text className="text-2xl font-medium">Home Lab</Text>
<Summary data={system} />
<Apps className="md:hidden mt-6" />
<Performance data={system} />
<Storage data={system} />
</Box>
<Apps className="hidden md:flex md:flex-col md:flex-1" />
</HStack>
</Container>
);
};
+11 -5
View File
@@ -60,6 +60,7 @@ interface ButtonProps
labelClasses?: string;
className?: string;
icon?: React.ReactNode;
iconClassName?: string;
children?: string;
}
function Button({
@@ -69,12 +70,15 @@ function Button({
variant,
size,
icon,
iconClassName,
children,
...props
}: ButtonProps) {
const textStyles = cn(
buttonTextVariants({ variant, size, className: labelClasses })
);
const textStyles = buttonTextVariants({
variant,
size,
className: labelClasses,
});
return (
<Pressable
@@ -86,10 +90,12 @@ function Button({
}
{...props}
>
{icon ? <Slot.View style={textStyles}>{icon}</Slot.View> : null}
{icon ? (
<Slot.View style={cn(textStyles, iconClassName)}>{icon}</Slot.View>
) : null}
{label || children ? (
<Text style={textStyles}>{label || children}</Text>
<Text style={cn(textStyles)}>{label || children}</Text>
) : null}
</Pressable>
);
+14
View File
@@ -0,0 +1,14 @@
import { createStore } from "zustand";
import { Toast, ToastOptions } from "react-native-toast-notifications";
export const toastStore = createStore<typeof Toast | null>(() => null);
export const showToast = (
message: string | JSX.Element,
toastOptions?: ToastOptions
) => {
const toast = toastStore.getState();
if (toast) {
toast.show(message, toastOptions);
}
};