feat: update ui for android

This commit is contained in:
Khairul Hidayat 2024-11-15 09:39:35 +00:00
parent d03c11fdb1
commit f2c0ab0945
21 changed files with 1006 additions and 801 deletions

View File

@ -34,7 +34,6 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
experiments: { experiments: {
typedRoutes: true, typedRoutes: true,
}, },
owner: "khairul169",
extra: { extra: {
eas: { eas: {
projectId: "3e0112c1-f0ed-423c-b5cf-95633f23f6dc", projectId: "3e0112c1-f0ed-423c-b5cf-95633f23f6dc",

View File

@ -20,9 +20,12 @@ export default function Layout() {
drawerContent={DrawerContent} drawerContent={DrawerContent}
screenOptions={{ screenOptions={{
drawerType: media.sm ? "front" : "permanent", drawerType: media.sm ? "front" : "permanent",
drawerStyle: { width: 250 }, drawerStyle: {
width: !media.sm ? 250 : "80%",
padding: 0,
},
headerLeft: media.sm ? undefined : () => null, headerLeft: media.sm ? undefined : () => null,
headerStyle: {elevation: 0, borderBottomWidth: 0} headerStyle: { elevation: 0, borderBottomWidth: 0 },
}} }}
> >
<Drawer.Screen <Drawer.Screen

View File

@ -3,7 +3,7 @@ import tamaguiConfig from "@/tamagui.config";
import { import {
DarkTheme, DarkTheme,
DefaultTheme, DefaultTheme,
ThemeProvider, ThemeProvider as NavThemeProvider,
} from "@react-navigation/native"; } from "@react-navigation/native";
import { TamaguiProvider, Theme } from "@tamagui/core"; import { TamaguiProvider, Theme } from "@tamagui/core";
import useThemeStore from "@/stores/theme"; import useThemeStore from "@/stores/theme";
@ -15,9 +15,19 @@ import { useServer } from "@/stores/app";
import queryClient from "@/lib/queryClient"; import queryClient from "@/lib/queryClient";
import DialogMessageProvider from "@/components/containers/dialog-message"; import DialogMessageProvider from "@/components/containers/dialog-message";
type Props = PropsWithChildren; const Providers = ({ children }: PropsWithChildren) => {
return (
<QueryClientProvider client={queryClient}>
<AuthProvider />
<ThemeProvider>
{children}
<DialogMessageProvider />
</ThemeProvider>
</QueryClientProvider>
);
};
const Providers = ({ children }: Props) => { const ThemeProvider = ({ children }: PropsWithChildren) => {
const colorScheme = useThemeStore((i) => i.theme); const colorScheme = useThemeStore((i) => i.theme);
const theme = useMemo(() => { const theme = useMemo(() => {
@ -40,17 +50,13 @@ const Providers = ({ children }: Props) => {
}, [theme, colorScheme]); }, [theme, colorScheme]);
return ( return (
<QueryClientProvider client={queryClient}> <NavThemeProvider value={navTheme}>
<AuthProvider /> <TamaguiProvider config={tamaguiConfig} defaultTheme={colorScheme}>
<ThemeProvider value={navTheme}> <Theme name="blue">
<TamaguiProvider config={tamaguiConfig} defaultTheme={colorScheme}> <PortalProvider shouldAddRootHost>{children}</PortalProvider>
<Theme name="blue"> </Theme>
<PortalProvider shouldAddRootHost>{children}</PortalProvider> </TamaguiProvider>
<DialogMessageProvider /> </NavThemeProvider>
</Theme>
</TamaguiProvider>
</ThemeProvider>
</QueryClientProvider>
); );
}; };

View File

@ -4,7 +4,7 @@ import {
DrawerContentScrollView, DrawerContentScrollView,
DrawerNavigationOptions as NavProps, DrawerNavigationOptions as NavProps,
} from "@react-navigation/drawer"; } from "@react-navigation/drawer";
import { Button, View } from "tamagui"; import { Button, Text, View } from "tamagui";
import { import {
CommonActions, CommonActions,
DrawerActions, DrawerActions,
@ -24,19 +24,25 @@ const Drawer = (props: DrawerContentComponentProps) => {
return ( return (
<View pt={insets.top} flex={1}> <View pt={insets.top} flex={1}>
<View p="$4"> <View py="$4" px="$2">
<UserMenuButton /> <UserMenuButton />
</View> </View>
<DrawerContentScrollView <DrawerContentScrollView
contentContainerStyle={{ padding: 18, paddingTop: 0 }} contentContainerStyle={{
paddingTop: 0,
paddingLeft: 0,
paddingStart: 0,
paddingRight: 18,
paddingBottom: 18,
}}
{...props} {...props}
> >
<DrawerItemList {...props} /> <DrawerItemList {...props} />
</DrawerContentScrollView> </DrawerContentScrollView>
<View px="$4" py="$2"> <View px="$4" py="$2">
<ThemeSwitcher /> <ThemeSwitcher $xs={{ alignSelf: "flex-start" }} />
</View> </View>
</View> </View>
); );
@ -85,7 +91,11 @@ const DrawerItemList = ({
onPress={onPress} onPress={onPress}
icon={drawerIcon?.({ size: 16, color: "$color", focused }) as never} icon={drawerIcon?.({ size: 16, color: "$color", focused }) as never}
size="$4" size="$4"
$xs={{ size: "$5", borderRadius: 999, borderWidth: 0 }} $xs={{ size: "$5" }}
borderRadius={0}
borderTopRightRadius="$10"
borderBottomRightRadius="$10"
borderWidth={0}
> >
{drawerLabel !== undefined {drawerLabel !== undefined
? drawerLabel ? drawerLabel

View File

@ -61,6 +61,7 @@ const ServerStatsBar = ({ url }: Props) => {
return ( return (
<ScrollView <ScrollView
horizontal horizontal
flexGrow={0}
contentContainerStyle={{ contentContainerStyle={{
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",

View File

@ -55,6 +55,7 @@ const Terminal = ({ client = "xtermjs", style, ...props }: TerminalProps) => {
horizontal horizontal
flexGrow={0} flexGrow={0}
contentContainerStyle={{ flexDirection: "row" }} contentContainerStyle={{ flexDirection: "row" }}
$gtSm={{ display: "none" }}
> >
<TerminalButton <TerminalButton
title={<Icons name="swap-horizontal" size={16} />} title={<Icons name="swap-horizontal" size={16} />}

View File

@ -12,21 +12,28 @@ const ThemeSwitcher = ({ iconSize = 18, ...props }: Props) => {
const id = useId(); const id = useId();
return ( return (
<XStack alignItems="center" gap="$2"> <XStack
<Ionicons alignItems="center"
name={theme === "light" ? "moon-outline" : "sunny-outline"} gap="$4"
size={iconSize} w="auto"
/> justifyContent="space-between"
<Label htmlFor={id} flex={1} cursor="pointer"> {...props}
Dark Mode >
</Label> <XStack alignItems="center" gap="$2">
<Ionicons
name={theme === "light" ? "moon-outline" : "sunny-outline"}
size={iconSize}
/>
<Label htmlFor={id} cursor="pointer">
Dark Mode
</Label>
</XStack>
<Switch <Switch
id={id} id={id}
onPress={toggle} onPress={toggle}
checked={theme === "dark"} checked={theme === "dark"}
size="$2" size="$2"
cursor="pointer" cursor="pointer"
{...props}
> >
<Switch.Thumb animation="quicker" /> <Switch.Thumb animation="quicker" />
</Switch> </Switch>

View File

@ -7,7 +7,6 @@ import {
Text, Text,
useMedia, useMedia,
View, View,
YGroup,
} from "tamagui"; } from "tamagui";
import MenuButton from "../ui/menu-button"; import MenuButton from "../ui/menu-button";
import Icons from "../ui/icons"; import Icons from "../ui/icons";
@ -29,8 +28,11 @@ const UserMenuButton = () => {
trigger={ trigger={
<Button <Button
bg="$colorTransparent" bg="$colorTransparent"
borderWidth={0}
justifyContent="flex-start" justifyContent="flex-start"
p={0} borderRadius="$10"
py={0}
px="$2"
gap="$1" gap="$1"
> >
<Avatar circular size="$3"> <Avatar circular size="$3">
@ -42,7 +44,7 @@ const UserMenuButton = () => {
{team ? `${team.icon} ${team.name}` : "Personal"} {team ? `${team.icon} ${team.name}` : "Personal"}
</Text> </Text>
</View> </View>
<Icons name="chevron-down" size={16} /> <Icons name="chevron-down" size={16} mr="$2" />
</Button> </Button>
} }
> >
@ -59,6 +61,7 @@ const UserMenuButton = () => {
title="Logout" title="Logout"
/> />
</MenuButton> </MenuButton>
<TeamForm /> <TeamForm />
</> </>
); );

View File

@ -25,7 +25,7 @@ const MenuButtonFrame = ({
<Popover size="$1" {...props}> <Popover size="$1" {...props}>
<Popover.Trigger asChild={asChild}>{trigger}</Popover.Trigger> <Popover.Trigger asChild={asChild}>{trigger}</Popover.Trigger>
<Adapt when="sm" platform="touch"> {/* <Adapt when="sm" platform="touch">
<Popover.Sheet modal dismissOnSnapToBottom snapPointsMode="fit"> <Popover.Sheet modal dismissOnSnapToBottom snapPointsMode="fit">
<Popover.Sheet.Overlay <Popover.Sheet.Overlay
animation="quickest" animation="quickest"
@ -33,11 +33,10 @@ const MenuButtonFrame = ({
exitStyle={{ opacity: 0 }} exitStyle={{ opacity: 0 }}
/> />
<Popover.Sheet.Frame padding="$4"> <Popover.Sheet.Frame padding="$4">
{/* <Adapt.Contents /> */} <Adapt.Contents />
{children}
</Popover.Sheet.Frame> </Popover.Sheet.Frame>
</Popover.Sheet> </Popover.Sheet>
</Adapt> </Adapt> */}
<Popover.Content <Popover.Content
bordered bordered
@ -53,12 +52,8 @@ const MenuButtonFrame = ({
}; };
const MenuButtonItem = (props: GetProps<typeof ListItem>) => { const MenuButtonItem = (props: GetProps<typeof ListItem>) => {
if (Platform.OS === "android" || Platform.OS === "ios") {
return <ListItem hoverTheme pressTheme {...props} />;
}
return ( return (
<Popover.Close asChild> <Popover.Close flexDirection="row" asChild>
<ListItem hoverTheme pressTheme {...props} /> <ListItem hoverTheme pressTheme {...props} />
</Popover.Close> </Popover.Close>
); );

View File

@ -1,52 +1,30 @@
import { useDebounceCallback } from "@/hooks/useDebounce"; import React, { ComponentPropsWithoutRef, forwardRef } from "react";
import React, { ComponentPropsWithoutRef, useEffect, useRef } from "react";
import RNPagerView from "react-native-pager-view"; import RNPagerView from "react-native-pager-view";
export type PagerViewProps = ComponentPropsWithoutRef<typeof RNPagerView> & { export type PagerViewProps = ComponentPropsWithoutRef<typeof RNPagerView> & {
page?: number;
onChangePage?: (page: number) => void; onChangePage?: (page: number) => void;
EmptyComponent?: () => JSX.Element;
}; };
const PagerView = ({ export type PagerViewRef = {
page, setPage: (page: number) => void;
onChangePage, setPageWithoutAnimation: (page: number) => void;
EmptyComponent,
children,
...props
}: PagerViewProps) => {
const ref = useRef<RNPagerView>(null);
const [onPageSelect, clearPageSelectDebounce] = useDebounceCallback(
(page) => onChangePage?.(page),
300
);
const [setPage] = useDebounceCallback((page) => {
ref.current?.setPage(page);
clearPageSelectDebounce();
}, 100);
useEffect(() => {
if (page != null) {
const npage = EmptyComponent != null ? page + 1 : page;
setPage(npage);
}
}, [page, EmptyComponent]);
return (
<RNPagerView
ref={ref}
{...props}
onPageSelected={(e) => {
const pos = e.nativeEvent.position;
onPageSelect(EmptyComponent ? pos - 1 : pos);
}}
>
{EmptyComponent ? <EmptyComponent key="-1" /> : null}
{children}
</RNPagerView>
);
}; };
const PagerView = forwardRef<PagerViewRef, PagerViewProps>(
({ onChangePage, children, ...props }, ref) => {
return (
<RNPagerView
ref={ref as never}
{...props}
onPageSelected={(e) => {
const pos = e.nativeEvent.position;
onChangePage?.(pos);
}}
>
{children}
</RNPagerView>
);
}
);
export default PagerView; export default PagerView;

View File

@ -1,48 +1,40 @@
import React, { useEffect, useMemo, useState } from "react"; import React, {
forwardRef,
useImperativeHandle,
useMemo,
useState,
} from "react";
import { View } from "react-native"; import { View } from "react-native";
import { PagerViewProps } from "./pager-view"; import { PagerViewProps, PagerViewRef } from "./pager-view";
const PagerView = ({ const PagerView = forwardRef<PagerViewRef, PagerViewProps>(
EmptyComponent, ({ children, initialPage }, ref) => {
children, const [curPage, setPage] = useState<number>(initialPage || 0);
page,
initialPage,
}: PagerViewProps) => {
const [curPage, setPage] = useState<number>(page || initialPage || 0);
useEffect(() => { useImperativeHandle(ref, () => ({
if (page != null) { setPage,
setPage(page); setPageWithoutAnimation: setPage,
} }));
}, [page]);
const content = useMemo(() => { const content = useMemo(() => {
if (!Array.isArray(children)) { if (!Array.isArray(children)) {
return null; return null;
} }
return children.map((element, index) => { return children.map((element, index) => {
return ( return (
<View <View
key={element.key || index} key={element.key || index}
style={{ display: index === curPage ? "flex" : "none", flex: 1 }} style={{ display: index === curPage ? "flex" : "none", flex: 1 }}
> >
{element} {element}
</View> </View>
); );
}); });
}, [curPage, children]); }, [curPage, children]);
const pageElement = useMemo(() => { return content;
return Array.isArray(children) ? children[curPage] : null; }
}, [curPage, children]); );
return (
<>
{!pageElement && EmptyComponent ? <EmptyComponent key="-1" /> : null}
{content}
</>
);
};
export default PagerView; export default PagerView;

View File

@ -11,9 +11,6 @@
"distribution": "internal", "distribution": "internal",
"android": { "android": {
"buildType": "apk" "buildType": "apk"
},
"env": {
"EXPO_PUBLIC_API_URL": "https://vaulterm-dev.rul.sh"
} }
}, },
"production": {} "production": {}

View File

@ -1,4 +1,4 @@
import { useCallback, useMemo, useRef } from "react"; import { useCallback, useRef } from "react";
export const useDebounceCallback = <T extends (...args: any[]) => any>( export const useDebounceCallback = <T extends (...args: any[]) => any>(
callback: T, callback: T,
@ -24,5 +24,5 @@ export const useDebounceCallback = <T extends (...args: any[]) => any>(
[delay, clear] [delay, clear]
); );
return [fn, clear] as const; return fn;
}; };

View File

@ -24,7 +24,7 @@
"@react-native-async-storage/async-storage": "1.23.1", "@react-native-async-storage/async-storage": "1.23.1",
"@react-navigation/drawer": "7.0.0", "@react-navigation/drawer": "7.0.0",
"@react-navigation/native": "7.0.0", "@react-navigation/native": "7.0.0",
"@tamagui/config": "^1.116.14", "@tamagui/config": "^1.116.15",
"@tanstack/react-query": "^5.59.20", "@tanstack/react-query": "^5.59.20",
"@xterm/addon-attach": "^0.11.0", "@xterm/addon-attach": "^0.11.0",
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.10.0",
@ -53,7 +53,7 @@
"react-native-screens": "4.0.0", "react-native-screens": "4.0.0",
"react-native-web": "~0.19.13", "react-native-web": "~0.19.13",
"react-native-webview": "^13.12.2", "react-native-webview": "^13.12.2",
"tamagui": "^1.116.14", "tamagui": "^1.116.15",
"zod": "3.23.8", "zod": "3.23.8",
"zustand": "^5.0.1" "zustand": "^5.0.1"
}, },

View File

@ -33,9 +33,7 @@ export default function LoginPage() {
}, },
title: "Login", title: "Login",
headerTitle: "", headerTitle: "",
headerRight: () => ( headerRight: () => <ThemeSwitcher $gtSm={{ mr: "$3" }} />,
<ThemeSwitcher bg="$colorTransparent" $gtSm={{ mr: "$3" }} />
),
}} }}
/> />

View File

@ -46,9 +46,7 @@ export default function ServerPage() {
marginHorizontal: "auto", marginHorizontal: "auto",
}, },
title: "Vaulterm", title: "Vaulterm",
headerRight: () => ( headerRight: () => <ThemeSwitcher $gtSm={{ mr: "$3" }} />,
<ThemeSwitcher bg="$colorTransparent" $gtSm={{ mr: "$3" }} />
),
}} }}
/> />
@ -66,7 +64,13 @@ export default function ServerPage() {
<ErrorAlert error={serverConnect.error} /> <ErrorAlert error={serverConnect.error} />
<FormField vertical label="URL"> <FormField vertical label="URL">
<InputField form={form} name="url" placeholder="https://" /> <InputField
form={form}
name="url"
autoCapitalize="none"
keyboardType="url"
placeholder="https://"
/>
</FormField> </FormField>
<Button onPress={onSubmit} isLoading={serverConnect.isPending}> <Button onPress={onSubmit} isLoading={serverConnect.isPending}>

View File

@ -2,6 +2,7 @@ import React from "react";
import { useTermSession } from "@/stores/terminal-sessions"; import { useTermSession } from "@/stores/terminal-sessions";
import { Button, ScrollView, View } from "tamagui"; import { Button, ScrollView, View } from "tamagui";
import Icons from "@/components/ui/icons"; import Icons from "@/components/ui/icons";
import { router } from "expo-router";
const SessionTabs = () => { const SessionTabs = () => {
const { sessions, curSession, setSession, remove } = useTermSession(); const { sessions, curSession, setSession, remove } = useTermSession();
@ -50,7 +51,7 @@ const SessionTabs = () => {
))} ))}
<Button <Button
onPress={() => setSession(-1)} onPress={() => router.push("/hosts")}
size="$2.5" size="$2.5"
bg="$colorTransparent" bg="$colorTransparent"
circular circular

View File

@ -1,6 +1,6 @@
import React from "react"; import React, { useEffect, useMemo, useRef } from "react";
import InteractiveSession from "@/components/containers/interactive-session"; import InteractiveSession from "@/components/containers/interactive-session";
import PagerView from "@/components/ui/pager-view"; import PagerView, { PagerViewRef } from "@/components/ui/pager-view";
import { useTermSession } from "@/stores/terminal-sessions"; import { useTermSession } from "@/stores/terminal-sessions";
import { Button, useMedia } from "tamagui"; import { Button, useMedia } from "tamagui";
import SessionTabs from "./components/session-tabs"; import SessionTabs from "./components/session-tabs";
@ -8,12 +8,40 @@ import HostList from "../hosts/components/host-list";
import Drawer from "expo-router/drawer"; import Drawer from "expo-router/drawer";
import { router } from "expo-router"; import { router } from "expo-router";
import Icons from "@/components/ui/icons"; import Icons from "@/components/ui/icons";
import { useDebounceCallback } from "@/hooks/useDebounce";
const TerminalPage = () => { const TerminalPage = () => {
const pagerViewRef = useRef<PagerViewRef>(null!);
const { sessions, curSession, setSession } = useTermSession(); const { sessions, curSession, setSession } = useTermSession();
const session = sessions[curSession]; const session = sessions[curSession];
const media = useMedia(); const media = useMedia();
const setCurSession = useDebounceCallback((idx: number) => {
pagerViewRef.current?.setPage(idx);
}, 100);
useEffect(() => {
setCurSession(curSession);
}, [curSession]);
const pagerView = useMemo(() => {
if (!sessions.length) {
return null;
}
return (
<PagerView
ref={pagerViewRef}
style={{ flex: 1 }}
onChangePage={setSession}
initialPage={0}
>
{sessions.map((session) => (
<InteractiveSession key={session.id} {...session} />
))}
</PagerView>
);
}, [sessions]);
return ( return (
<> <>
<Drawer.Screen <Drawer.Screen
@ -30,17 +58,7 @@ const TerminalPage = () => {
/> />
{sessions.length > 0 && media.gtSm ? <SessionTabs /> : null} {sessions.length > 0 && media.gtSm ? <SessionTabs /> : null}
{!sessions.length ? <HostList allowEdit={false} /> : pagerView}
<PagerView
style={{ flex: 1 }}
page={curSession}
onChangePage={setSession}
EmptyComponent={() => <HostList allowEdit={false} />}
>
{sessions.map((session) => (
<InteractiveSession key={session.id} {...session} />
))}
</PagerView>
</> </>
); );
}; };

View File

@ -63,7 +63,7 @@ const SessionsPage = () => {
icon={<Icons name="connection" size={16} />} icon={<Icons name="connection" size={16} />}
onPress={() => { onPress={() => {
router.back(); router.back();
setTimeout(() => setSession(idx), 20); setSession(idx);
}} }}
> >
{session.label} {session.label}

1482
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
import { createTamagui } from "@tamagui/core"; import { createTamagui } from "tamagui";
import { config } from "@tamagui/config/v3"; import { config } from "@tamagui/config/v3";
// you usually export this from a tamagui.config.ts file // you usually export this from a tamagui.config.ts file