mirror of
https://github.com/khairul169/vaulterm.git
synced 2026-09-15 17:03:30 +07:00
feat: update ui
This commit is contained in:
@@ -22,11 +22,10 @@ type IncusSessionProps = {
|
||||
};
|
||||
};
|
||||
|
||||
export type InteractiveSessionProps = { params: { hostId: string } } & (
|
||||
| SSHSessionProps
|
||||
| PVESessionProps
|
||||
| IncusSessionProps
|
||||
);
|
||||
export type InteractiveSessionProps = {
|
||||
label: string;
|
||||
params: { hostId: string };
|
||||
} & (SSHSessionProps | PVESessionProps | IncusSessionProps);
|
||||
|
||||
const InteractiveSession = ({ type, params }: InteractiveSessionProps) => {
|
||||
const query = new URLSearchParams(params);
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
import React, { ComponentPropsWithoutRef } from "react";
|
||||
import XTermJs, { XTermRef } from "./xtermjs";
|
||||
import Ionicons from "@expo/vector-icons/Ionicons";
|
||||
import {
|
||||
Pressable,
|
||||
ScrollView,
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextStyle,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { ScrollView, Text, TextStyle, View } from "tamagui";
|
||||
import Pressable from "../ui/pressable";
|
||||
import Icons from "../ui/icons";
|
||||
|
||||
const Keys = {
|
||||
ArrowLeft: "\x1b[D",
|
||||
@@ -45,7 +39,7 @@ const Terminal = ({ client = "xtermjs", style, ...props }: TerminalProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[styles.container, style]} {...props}>
|
||||
<View flex={1} bg="$background" {...props}>
|
||||
{client === "xtermjs" && (
|
||||
<XTermJs
|
||||
ref={xtermRef}
|
||||
@@ -56,32 +50,32 @@ const Terminal = ({ client = "xtermjs", style, ...props }: TerminalProps) => {
|
||||
|
||||
<ScrollView
|
||||
horizontal
|
||||
style={{ flexGrow: 0 }}
|
||||
contentContainerStyle={styles.buttons}
|
||||
flexGrow={0}
|
||||
contentContainerStyle={{ flexDirection: "row" }}
|
||||
>
|
||||
<TerminalButton
|
||||
title={<Ionicons name="swap-horizontal" color="white" size={16} />}
|
||||
onPress={() => send(Keys.Tab)}
|
||||
title={<Icons name="swap-horizontal" size={16} />}
|
||||
// onPress={() => send(Keys.Tab)}
|
||||
/>
|
||||
<TerminalButton title="ESC" onPress={() => send(Keys.Escape)} />
|
||||
<TerminalButton
|
||||
title={<Ionicons name="home" color="white" size={16} />}
|
||||
title={<Icons name="home" size={16} />}
|
||||
onPress={() => send(Keys.Home)}
|
||||
/>
|
||||
<TerminalButton
|
||||
title={<Ionicons name="arrow-back" color="white" size={18} />}
|
||||
title={<Icons name="arrow-left" size={18} />}
|
||||
onPress={() => send(Keys.ArrowLeft)}
|
||||
/>
|
||||
<TerminalButton
|
||||
title={<Ionicons name="arrow-up" color="white" size={18} />}
|
||||
title={<Icons name="arrow-up" size={18} />}
|
||||
onPress={() => send(Keys.ArrowUp)}
|
||||
/>
|
||||
<TerminalButton
|
||||
title={<Ionicons name="arrow-down" color="white" size={18} />}
|
||||
title={<Icons name="arrow-down" size={18} />}
|
||||
onPress={() => send(Keys.ArrowDown)}
|
||||
/>
|
||||
<TerminalButton
|
||||
title={<Ionicons name="arrow-forward" color="white" size={18} />}
|
||||
title={<Icons name="arrow-right" size={18} />}
|
||||
onPress={() => send(Keys.ArrowRight)}
|
||||
/>
|
||||
<TerminalButton title="Enter" onPress={() => send(Keys.Enter)} />
|
||||
@@ -108,45 +102,11 @@ const TerminalButton = ({
|
||||
...props
|
||||
}: ComponentPropsWithoutRef<typeof Pressable> & {
|
||||
title: string | React.ReactNode;
|
||||
textStyle?: StyleProp<TextStyle>;
|
||||
textStyle?: TextStyle;
|
||||
}) => (
|
||||
<Pressable
|
||||
style={({ pressed }) => [styles.btn, pressed && styles.btnPressed]}
|
||||
{...props}
|
||||
>
|
||||
{typeof title === "string" ? (
|
||||
<Text style={[styles.btnText, textStyle]}>{title}</Text>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
<Pressable px="$4" py="$3" $hover={{ bg: "$blue3" }} {...props}>
|
||||
{typeof title === "string" ? <Text {...textStyle}>{title}</Text> : title}
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: "#232323",
|
||||
},
|
||||
buttons: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "stretch",
|
||||
backgroundColor: "#232323",
|
||||
},
|
||||
btn: {
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
btnPressed: {
|
||||
backgroundColor: "#3a3a3a",
|
||||
},
|
||||
btnText: {
|
||||
color: "white",
|
||||
fontSize: 16,
|
||||
},
|
||||
});
|
||||
|
||||
export default Terminal;
|
||||
|
||||
@@ -20,6 +20,29 @@ type XTermJsProps = {
|
||||
wsUrl: string;
|
||||
};
|
||||
|
||||
// vscode-snazzy https://github.com/Tyriar/vscode-snazzy
|
||||
const snazzyTheme = {
|
||||
foreground: "#eff0eb",
|
||||
background: "#282a36",
|
||||
selection: "#97979b33",
|
||||
black: "#282a36",
|
||||
brightBlack: "#686868",
|
||||
red: "#ff5c57",
|
||||
brightRed: "#ff5c57",
|
||||
green: "#5af78e",
|
||||
brightGreen: "#5af78e",
|
||||
yellow: "#f3f99d",
|
||||
brightYellow: "#f3f99d",
|
||||
blue: "#57c7ff",
|
||||
brightBlue: "#57c7ff",
|
||||
magenta: "#ff6ac1",
|
||||
brightMagenta: "#ff6ac1",
|
||||
cyan: "#9aedfe",
|
||||
brightCyan: "#9aedfe",
|
||||
white: "#f1f1f0",
|
||||
brightWhite: "#eff0eb",
|
||||
};
|
||||
|
||||
export interface XTermRef extends DOMImperativeFactory {
|
||||
send: (...args: JSONValue[]) => void;
|
||||
}
|
||||
@@ -35,7 +58,11 @@ const XTermJs = forwardRef<XTermRef, XTermJsProps>((props, ref) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const xterm = new XTerm();
|
||||
const xterm = new XTerm({
|
||||
fontFamily: '"Cascadia Code", Menlo, monospace',
|
||||
theme: snazzyTheme,
|
||||
cursorBlink: true,
|
||||
});
|
||||
xterm.open(container);
|
||||
|
||||
const fitAddon = new FitAddon();
|
||||
@@ -131,6 +158,8 @@ const XTermJs = forwardRef<XTermRef, XTermJsProps>((props, ref) => {
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={{
|
||||
background: snazzyTheme.background,
|
||||
padding: 12,
|
||||
flex: !IS_DOM ? 1 : undefined,
|
||||
width: "100%",
|
||||
height: IS_DOM ? "100vh" : undefined,
|
||||
|
||||
@@ -1,26 +1,51 @@
|
||||
import { useDebounceCallback } from "@/hooks/useDebounce";
|
||||
import React, { ComponentPropsWithoutRef, useEffect, useRef } 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, ...props }: PagerViewProps) => {
|
||||
const PagerView = ({
|
||||
page,
|
||||
onChangePage,
|
||||
EmptyComponent,
|
||||
children,
|
||||
...props
|
||||
}: PagerViewProps) => {
|
||||
const ref = useRef<RNPagerView>(null);
|
||||
|
||||
const [onPageSelect, clearPageSelectDebounce] = useDebounceCallback(
|
||||
(page) => onChangePage?.(page),
|
||||
100
|
||||
);
|
||||
|
||||
const [setPage] = useDebounceCallback((page) => {
|
||||
ref.current?.setPage(page);
|
||||
clearPageSelectDebounce();
|
||||
}, 300);
|
||||
|
||||
useEffect(() => {
|
||||
if (page != null) {
|
||||
ref.current?.setPage(page);
|
||||
const npage = EmptyComponent != null ? page + 1 : page;
|
||||
setPage(npage);
|
||||
}
|
||||
}, [page]);
|
||||
}, [page, EmptyComponent]);
|
||||
|
||||
return (
|
||||
<RNPagerView
|
||||
ref={ref}
|
||||
{...props}
|
||||
onPageSelected={(e) => onChangePage?.(e.nativeEvent.position)}
|
||||
/>
|
||||
onPageSelected={(e) => {
|
||||
const pos = e.nativeEvent.position;
|
||||
onPageSelect(EmptyComponent ? pos - 1 : pos);
|
||||
}}
|
||||
>
|
||||
{EmptyComponent ? <EmptyComponent key="-1" /> : null}
|
||||
{children}
|
||||
</RNPagerView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { View } from "react-native";
|
||||
import { PagerViewProps } from "./pager-view";
|
||||
|
||||
const PagerView = ({
|
||||
className,
|
||||
EmptyComponent,
|
||||
children,
|
||||
page,
|
||||
initialPage,
|
||||
@@ -33,7 +33,16 @@ const PagerView = ({
|
||||
});
|
||||
}, [curPage, children]);
|
||||
|
||||
return content;
|
||||
const pageElement = useMemo(() => {
|
||||
return Array.isArray(children) ? children[curPage] : null;
|
||||
}, [curPage, children]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!pageElement && EmptyComponent ? <EmptyComponent key="-1" /> : null}
|
||||
{content}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PagerView;
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
import { Pressable as BasePressable } from "react-native";
|
||||
import { GetProps, styled, ViewStyle } from "tamagui";
|
||||
import { useRef } from "react";
|
||||
import {
|
||||
TapGestureHandler,
|
||||
State as GestureState,
|
||||
} from "react-native-gesture-handler";
|
||||
import { Button, GetProps, styled, View, ViewStyle } from "tamagui";
|
||||
|
||||
const StyledPressable = styled(Button, {
|
||||
unstyled: true,
|
||||
backgroundColor: "$colorTransparent",
|
||||
borderWidth: 0,
|
||||
cursor: "pointer",
|
||||
});
|
||||
|
||||
const StyledPressable = styled(BasePressable);
|
||||
export type PressableProps = GetProps<typeof StyledPressable> & {
|
||||
$hover?: ViewStyle;
|
||||
$pressed?: ViewStyle;
|
||||
@@ -13,7 +23,49 @@ const Pressable = ({
|
||||
...props
|
||||
}: PressableProps) => {
|
||||
return (
|
||||
<StyledPressable pressStyle={$pressed} hoverStyle={$hover} {...props} />
|
||||
<StyledPressable
|
||||
hoverStyle={$hover}
|
||||
pressStyle={$pressed}
|
||||
{...(props as any)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type MultiTapPressableProps = GetProps<typeof View> & {
|
||||
numberOfTaps: number;
|
||||
onTap?: () => void;
|
||||
onMultiTap?: () => void;
|
||||
};
|
||||
|
||||
export const MultiTapPressable = ({
|
||||
numberOfTaps,
|
||||
onTap,
|
||||
onMultiTap,
|
||||
...props
|
||||
}: MultiTapPressableProps) => {
|
||||
const tapRef = useRef<any>();
|
||||
|
||||
return (
|
||||
<TapGestureHandler
|
||||
onHandlerStateChange={(e) => {
|
||||
if (e.nativeEvent.state === GestureState.ACTIVE) {
|
||||
onTap?.();
|
||||
}
|
||||
}}
|
||||
waitFor={tapRef}
|
||||
>
|
||||
<TapGestureHandler
|
||||
onHandlerStateChange={(e) => {
|
||||
if (e.nativeEvent.state === GestureState.ACTIVE) {
|
||||
onMultiTap?.();
|
||||
}
|
||||
}}
|
||||
numberOfTaps={numberOfTaps}
|
||||
ref={tapRef}
|
||||
>
|
||||
<View pressStyle={{ opacity: 0.5 }} {...props} />
|
||||
</TapGestureHandler>
|
||||
</TapGestureHandler>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from "react";
|
||||
import { GetProps, Input, View, ViewStyle } from "tamagui";
|
||||
import Icons from "./icons";
|
||||
|
||||
type SearchInputProps = GetProps<typeof Input> & {
|
||||
_container?: ViewStyle;
|
||||
};
|
||||
|
||||
const SearchInput = ({ _container, ...props }: SearchInputProps) => {
|
||||
return (
|
||||
<View position="relative" {..._container}>
|
||||
<Icons
|
||||
name="magnify"
|
||||
size={20}
|
||||
position="absolute"
|
||||
top={11}
|
||||
left="$3"
|
||||
zIndex={1}
|
||||
/>
|
||||
<Input pl="$7" placeholder="Search..." {...props} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchInput;
|
||||
Reference in New Issue
Block a user