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: {
typedRoutes: true,
},
owner: "khairul169",
extra: {
eas: {
projectId: "3e0112c1-f0ed-423c-b5cf-95633f23f6dc",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,9 +11,6 @@
"distribution": "internal",
"android": {
"buildType": "apk"
},
"env": {
"EXPO_PUBLIC_API_URL": "https://vaulterm-dev.rul.sh"
}
},
"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>(
callback: T,
@ -24,5 +24,5 @@ export const useDebounceCallback = <T extends (...args: any[]) => any>(
[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-navigation/drawer": "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",
"@xterm/addon-attach": "^0.11.0",
"@xterm/addon-fit": "^0.10.0",
@ -53,7 +53,7 @@
"react-native-screens": "4.0.0",
"react-native-web": "~0.19.13",
"react-native-webview": "^13.12.2",
"tamagui": "^1.116.14",
"tamagui": "^1.116.15",
"zod": "3.23.8",
"zustand": "^5.0.1"
},

View File

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

View File

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

View File

@ -2,6 +2,7 @@ import React from "react";
import { useTermSession } from "@/stores/terminal-sessions";
import { Button, ScrollView, View } from "tamagui";
import Icons from "@/components/ui/icons";
import { router } from "expo-router";
const SessionTabs = () => {
const { sessions, curSession, setSession, remove } = useTermSession();
@ -50,7 +51,7 @@ const SessionTabs = () => {
))}
<Button
onPress={() => setSession(-1)}
onPress={() => router.push("/hosts")}
size="$2.5"
bg="$colorTransparent"
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 PagerView from "@/components/ui/pager-view";
import PagerView, { PagerViewRef } from "@/components/ui/pager-view";
import { useTermSession } from "@/stores/terminal-sessions";
import { Button, useMedia } from "tamagui";
import SessionTabs from "./components/session-tabs";
@ -8,12 +8,40 @@ import HostList from "../hosts/components/host-list";
import Drawer from "expo-router/drawer";
import { router } from "expo-router";
import Icons from "@/components/ui/icons";
import { useDebounceCallback } from "@/hooks/useDebounce";
const TerminalPage = () => {
const pagerViewRef = useRef<PagerViewRef>(null!);
const { sessions, curSession, setSession } = useTermSession();
const session = sessions[curSession];
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 (
<>
<Drawer.Screen
@ -30,17 +58,7 @@ const TerminalPage = () => {
/>
{sessions.length > 0 && media.gtSm ? <SessionTabs /> : null}
<PagerView
style={{ flex: 1 }}
page={curSession}
onChangePage={setSession}
EmptyComponent={() => <HostList allowEdit={false} />}
>
{sessions.map((session) => (
<InteractiveSession key={session.id} {...session} />
))}
</PagerView>
{!sessions.length ? <HostList allowEdit={false} /> : pagerView}
</>
);
};

View File

@ -63,7 +63,7 @@ const SessionsPage = () => {
icon={<Icons name="connection" size={16} />}
onPress={() => {
router.back();
setTimeout(() => setSession(idx), 20);
setSession(idx);
}}
>
{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";
// you usually export this from a tamagui.config.ts file