This commit is contained in:
EthanShoeDev
2025-09-15 16:39:18 -04:00
parent d2695577ca
commit aa5c0bfc7a
8 changed files with 250 additions and 217 deletions

View File

@@ -7,6 +7,7 @@
"mhutchie.git-graph", "mhutchie.git-graph",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"yoavbls.pretty-ts-errors", "yoavbls.pretty-ts-errors",
"ctf0.duplicated-code-new" "ctf0.duplicated-code-new",
"github.vscode-github-actions"
] ]
} }

View File

@@ -6,11 +6,11 @@
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "expo start", "start": "expo start",
"android": "expo run:android", "android": "expo run:android --port 8082",
"ios": "expo run:ios --port 8081",
"android:release": "expo run:android --variant release", "android:release": "expo run:android --variant release",
"build:signed:aab": "tsx scripts/signed-build.ts", "build:signed:aab": "tsx scripts/signed-build.ts",
"build:signed:apk": "tsx scripts/signed-build.ts --format apk", "build:signed:apk": "tsx scripts/signed-build.ts --format apk",
"ios": "expo run:ios",
"web": "expo start --web", "web": "expo start --web",
"prebuild": "expo prebuild", "prebuild": "expo prebuild",
"prebuild:clean": "expo prebuild --clean", "prebuild:clean": "expo prebuild --clean",

View File

@@ -1,19 +1,21 @@
import { NativeTabs, Icon, Label } from 'expo-router/unstable-native-tabs'; import { NativeTabs, Icon, Label } from 'expo-router/unstable-native-tabs';
import React from 'react'; import React from 'react';
import { useTheme } from '@/lib/theme';
export default function TabsLayout() { export default function TabsLayout() {
const theme = useTheme();
return ( return (
<NativeTabs> <NativeTabs backgroundColor={theme.colors.surface}>
<NativeTabs.Trigger name="index"> <NativeTabs.Trigger name="index">
<Label>Host</Label> <Label>Host</Label>
<Icon sf="house.fill" drawable="custom_android_drawable" /> <Icon sf="house.fill" drawable="ic_menu_myplaces" />
</NativeTabs.Trigger> </NativeTabs.Trigger>
<NativeTabs.Trigger name="shell"> <NativeTabs.Trigger name="shell">
<Icon sf="gear" drawable="custom_settings_drawable" /> <Icon sf="gear" drawable="ic_menu_compass" />
<Label>Shell</Label> <Label>Shell</Label>
</NativeTabs.Trigger> </NativeTabs.Trigger>
<NativeTabs.Trigger name="settings"> <NativeTabs.Trigger name="settings">
<Icon sf="gear" drawable="custom_settings_drawable" /> <Icon sf="gear" drawable="ic_menu_preferences" />
<Label>Settings</Label> <Label>Settings</Label>
</NativeTabs.Trigger> </NativeTabs.Trigger>
</NativeTabs> </NativeTabs>

View File

@@ -22,7 +22,7 @@ import {
connectionDetailsSchema, connectionDetailsSchema,
secretsManager, secretsManager,
} from '@/lib/secrets-manager'; } from '@/lib/secrets-manager';
import { useTheme } from '@/theme'; import { useTheme ,type AppTheme } from '@/lib/theme';
export default function TabsIndex() { export default function TabsIndex() {
return <Host />; return <Host />;
@@ -40,6 +40,7 @@ const defaultValues: ConnectionDetails = {
function Host() { function Host() {
const theme = useTheme(); const theme = useTheme();
const styles = React.useMemo(() => makeStyles(theme), [theme]);
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const sshConnMutation = useSshConnMutation(); const sshConnMutation = useSshConnMutation();
const connectionForm = useAppForm({ const connectionForm = useAppForm({
@@ -192,6 +193,8 @@ function Host() {
} }
function KeyIdPickerField() { function KeyIdPickerField() {
const theme = useTheme();
const styles = React.useMemo(() => makeStyles(theme), [theme]);
const field = useFieldContext<string>(); const field = useFieldContext<string>();
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
@@ -228,7 +231,7 @@ function KeyIdPickerField() {
setOpen(true); setOpen(true);
}} }}
> >
<Text style={{ color: '#E5E7EB' }}>{display}</Text> <Text style={{ color: theme.colors.textPrimary }}>{display}</Text>
</Pressable> </Pressable>
{!selected && ( {!selected && (
<Text style={styles.mutedText}> <Text style={styles.mutedText}>
@@ -270,6 +273,8 @@ function KeyIdPickerField() {
function PreviousConnectionsSection(props: { function PreviousConnectionsSection(props: {
onSelect: (connection: ConnectionDetails) => void; onSelect: (connection: ConnectionDetails) => void;
}) { }) {
const theme = useTheme();
const styles = React.useMemo(() => makeStyles(theme), [theme]);
const listConnectionsQuery = useQuery(secretsManager.connections.query.list); const listConnectionsQuery = useQuery(secretsManager.connections.query.list);
return ( return (
@@ -300,6 +305,8 @@ function ConnectionRow(props: {
id: string; id: string;
onSelect: (connection: ConnectionDetails) => void; onSelect: (connection: ConnectionDetails) => void;
}) { }) {
const theme = useTheme();
const styles = React.useMemo(() => makeStyles(theme), [theme]);
const detailsQuery = useQuery(secretsManager.connections.query.get(props.id)); const detailsQuery = useQuery(secretsManager.connections.query.get(props.id));
const details = detailsQuery.data?.value; const details = detailsQuery.data?.value;
@@ -324,196 +331,198 @@ function ConnectionRow(props: {
); );
} }
const styles = StyleSheet.create({ function makeStyles(theme: AppTheme) {
container: { return StyleSheet.create({
flex: 1, container: {
padding: 24, flex: 1,
backgroundColor: '#0B1324', padding: 24,
justifyContent: 'center', backgroundColor: theme.colors.background,
}, justifyContent: 'center',
scrollContent: { },
paddingBottom: 32, scrollContent: {
}, paddingBottom: 32,
header: { },
marginBottom: 16, header: {
alignItems: 'center', marginBottom: 16,
}, alignItems: 'center',
appName: { },
fontSize: 28, appName: {
fontWeight: '800', fontSize: 28,
color: '#E5E7EB', fontWeight: '800',
letterSpacing: 1, color: theme.colors.textPrimary,
}, letterSpacing: 1,
appTagline: { },
marginTop: 4, appTagline: {
fontSize: 13, marginTop: 4,
color: '#9AA0A6', fontSize: 13,
}, color: theme.colors.muted,
card: { },
backgroundColor: '#111B34', card: {
borderRadius: 20, backgroundColor: theme.colors.surface,
padding: 24, borderRadius: 20,
marginHorizontal: 4, padding: 24,
shadowColor: '#000', marginHorizontal: 4,
shadowOpacity: 0.3, shadowColor: theme.colors.shadow,
shadowRadius: 16, shadowOpacity: 0.3,
shadowOffset: { width: 0, height: 4 }, shadowRadius: 16,
elevation: 8, shadowOffset: { width: 0, height: 4 },
borderWidth: 1, elevation: 8,
borderColor: '#1E293B', borderWidth: 1,
}, borderColor: theme.colors.borderStrong,
title: { },
fontSize: 24, title: {
fontWeight: '700', fontSize: 24,
color: '#E5E7EB', fontWeight: '700',
marginBottom: 6, color: theme.colors.textPrimary,
letterSpacing: 0.5, marginBottom: 6,
}, letterSpacing: 0.5,
subtitle: { },
fontSize: 15, subtitle: {
color: '#9AA0A6', fontSize: 15,
marginBottom: 24, color: theme.colors.muted,
lineHeight: 20, marginBottom: 24,
}, lineHeight: 20,
inputGroup: { },
marginBottom: 12, inputGroup: {
}, marginBottom: 12,
label: { },
marginBottom: 6, label: {
fontSize: 14, marginBottom: 6,
color: '#C6CBD3', fontSize: 14,
fontWeight: '600', color: theme.colors.textSecondary,
}, fontWeight: '600',
input: { },
borderWidth: 1, input: {
borderColor: '#2A3655', borderWidth: 1,
backgroundColor: '#0E172B', borderColor: theme.colors.border,
color: '#E5E7EB', backgroundColor: theme.colors.inputBackground,
borderRadius: 10, color: theme.colors.textPrimary,
paddingHorizontal: 12, borderRadius: 10,
paddingVertical: 12, paddingHorizontal: 12,
fontSize: 16, paddingVertical: 12,
}, fontSize: 16,
errorText: { },
marginTop: 6, errorText: {
color: '#FCA5A5', marginTop: 6,
fontSize: 12, color: theme.colors.danger,
}, fontSize: 12,
actions: { },
marginTop: 20, actions: {
}, marginTop: 20,
mutedText: { },
color: '#9AA0A6', mutedText: {
fontSize: 14, color: theme.colors.muted,
}, fontSize: 14,
submitButton: { },
backgroundColor: '#2563EB', submitButton: {
borderRadius: 12, backgroundColor: theme.colors.primary,
paddingVertical: 16, borderRadius: 12,
alignItems: 'center', paddingVertical: 16,
shadowColor: '#2563EB', alignItems: 'center',
shadowOpacity: 0.3, shadowColor: theme.colors.primary,
shadowRadius: 8, shadowOpacity: 0.3,
shadowOffset: { width: 0, height: 2 }, shadowRadius: 8,
elevation: 4, shadowOffset: { width: 0, height: 2 },
}, elevation: 4,
submitButtonText: { },
color: '#FFFFFF', submitButtonText: {
fontWeight: '700', color: theme.colors.buttonTextOnPrimary,
fontSize: 16, fontWeight: '700',
letterSpacing: 0.5, fontSize: 16,
}, letterSpacing: 0.5,
buttonDisabled: { },
backgroundColor: '#3B82F6', buttonDisabled: {
opacity: 0.6, backgroundColor: theme.colors.primaryDisabled,
}, opacity: 0.6,
secondaryButton: { },
backgroundColor: 'transparent', secondaryButton: {
borderWidth: 1, backgroundColor: theme.colors.transparent,
borderColor: '#2A3655', borderWidth: 1,
borderRadius: 12, borderColor: theme.colors.border,
paddingVertical: 14, borderRadius: 12,
alignItems: 'center', paddingVertical: 14,
marginTop: 12, alignItems: 'center',
}, marginTop: 12,
secondaryButtonText: { },
color: '#C6CBD3', secondaryButtonText: {
fontWeight: '600', color: theme.colors.textSecondary,
fontSize: 14, fontWeight: '600',
letterSpacing: 0.3, fontSize: 14,
}, letterSpacing: 0.3,
listSection: { },
marginTop: 20, listSection: {
}, marginTop: 20,
listTitle: { },
fontSize: 16, listTitle: {
fontWeight: '700', fontSize: 16,
color: '#E5E7EB', fontWeight: '700',
marginBottom: 8, color: theme.colors.textPrimary,
}, marginBottom: 8,
listContainer: { },
// Intentionally empty for RN compatibility listContainer: {
}, // Intentionally empty for RN compatibility
row: { },
flexDirection: 'row', row: {
alignItems: 'center', flexDirection: 'row',
justifyContent: 'space-between', alignItems: 'center',
backgroundColor: '#0E172B', justifyContent: 'space-between',
borderWidth: 1, backgroundColor: theme.colors.inputBackground,
borderColor: '#2A3655', borderWidth: 1,
borderRadius: 12, borderColor: theme.colors.border,
paddingHorizontal: 12, borderRadius: 12,
paddingVertical: 12, paddingHorizontal: 12,
marginBottom: 8, paddingVertical: 12,
}, marginBottom: 8,
rowTextContainer: { },
flex: 1, rowTextContainer: {
marginRight: 12, flex: 1,
}, marginRight: 12,
rowTitle: { },
color: '#E5E7EB', rowTitle: {
fontSize: 15, color: theme.colors.textPrimary,
fontWeight: '600', fontSize: 15,
}, fontWeight: '600',
rowSubtitle: { },
color: '#9AA0A6', rowSubtitle: {
marginTop: 2, color: theme.colors.muted,
fontSize: 12, marginTop: 2,
}, fontSize: 12,
rowChevron: { },
color: '#9AA0A6', rowChevron: {
fontSize: 22, color: theme.colors.muted,
paddingHorizontal: 4, fontSize: 22,
}, paddingHorizontal: 4,
modalOverlay: { },
flex: 1, modalOverlay: {
backgroundColor: 'rgba(0,0,0,0.4)', flex: 1,
justifyContent: 'flex-end', backgroundColor: theme.colors.overlay,
}, justifyContent: 'flex-end',
modalSheet: { },
backgroundColor: '#0B1324', modalSheet: {
borderTopLeftRadius: 16, backgroundColor: theme.colors.background,
borderTopRightRadius: 16, borderTopLeftRadius: 16,
padding: 16, borderTopRightRadius: 16,
borderColor: '#1E293B', padding: 16,
borderWidth: 1, borderColor: theme.colors.borderStrong,
maxHeight: '85%', borderWidth: 1,
}, maxHeight: '85%',
modalHeader: { },
flexDirection: 'row', modalHeader: {
justifyContent: 'space-between', flexDirection: 'row',
alignItems: 'center', justifyContent: 'space-between',
marginBottom: 8, alignItems: 'center',
}, marginBottom: 8,
modalCloseButton: { },
paddingHorizontal: 8, modalCloseButton: {
paddingVertical: 6, paddingHorizontal: 8,
borderRadius: 8, paddingVertical: 6,
borderWidth: 1, borderRadius: 8,
borderColor: '#2A3655', borderWidth: 1,
}, borderColor: theme.colors.border,
modalCloseText: { },
color: '#C6CBD3', modalCloseText: {
fontWeight: '600', color: theme.colors.textSecondary,
}, fontWeight: '600',
}); },
});
}

View File

@@ -1,11 +1,5 @@
import { RnRussh } from '@fressh/react-native-uniffi-russh'; import { RnRussh } from '@fressh/react-native-uniffi-russh';
import { import { Stack, useLocalSearchParams, useRouter } from 'expo-router';
Link,
Stack,
useLocalSearchParams,
useNavigation,
useRouter,
} from 'expo-router';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { import {
Platform, Platform,
@@ -17,7 +11,7 @@ import {
View, View,
} from 'react-native'; } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context'; import { SafeAreaView } from 'react-native-safe-area-context';
import { useTheme } from '@/theme'; import { useTheme } from '@/lib/theme';
export default function TabsShellDetail() { export default function TabsShellDetail() {
return <ShellDetail />; return <ShellDetail />;

View File

@@ -8,15 +8,22 @@ import { useQuery } from '@tanstack/react-query';
import { Link } from 'expo-router'; import { Link } from 'expo-router';
import React from 'react'; import React from 'react';
import { StyleSheet, Text, View } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { listSshShellsQueryOptions } from '@/lib/query-fns'; import { listSshShellsQueryOptions } from '@/lib/query-fns';
import { useTheme ,type AppTheme } from '@/lib/theme';
export default function TabsShellList() { export default function TabsShellList() {
return <ShellList />; const theme = useTheme();
return (
<SafeAreaView style={{ flex: 1, backgroundColor: theme.colors.background }}>
<ShellContent />
</SafeAreaView>
);
} }
type ShellWithConnection = SshShellSession & { connection: SshConnection }; type ShellWithConnection = SshShellSession & { connection: SshConnection };
function ShellList() { function ShellContent() {
const connectionsWithShells = useQuery(listSshShellsQueryOptions); const connectionsWithShells = useQuery(listSshShellsQueryOptions);
if (!connectionsWithShells.data) { if (!connectionsWithShells.data) {
@@ -26,6 +33,8 @@ function ShellList() {
} }
function LoadingState() { function LoadingState() {
const theme = useTheme();
const styles = React.useMemo(() => makeStyles(theme), [theme]);
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Text style={styles.text}>Loading...</Text> <Text style={styles.text}>Loading...</Text>
@@ -67,6 +76,8 @@ function LoadedState({
} }
function EmptyState() { function EmptyState() {
const theme = useTheme();
const styles = React.useMemo(() => makeStyles(theme), [theme]);
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Text style={styles.text}>No active shells. Connect from Host tab.</Text> <Text style={styles.text}>No active shells. Connect from Host tab.</Text>
@@ -76,6 +87,8 @@ function EmptyState() {
} }
function ShellCard({ shell }: { shell: ShellWithConnection }) { function ShellCard({ shell }: { shell: ShellWithConnection }) {
const theme = useTheme();
const styles = React.useMemo(() => makeStyles(theme), [theme]);
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Text style={styles.text}>{shell.connectionId}</Text> <Text style={styles.text}>{shell.connectionId}</Text>
@@ -93,7 +106,9 @@ function ShellCard({ shell }: { shell: ShellWithConnection }) {
); );
} }
const styles = StyleSheet.create({ function makeStyles(theme: AppTheme) {
container: { flex: 1, alignItems: 'center', justifyContent: 'center' }, return StyleSheet.create({
text: { color: 'black', marginBottom: 8 }, container: { flex: 1, alignItems: 'center', justifyContent: 'center' },
}); text: { color: theme.colors.textPrimary, marginBottom: 8 },
});
}

View File

@@ -2,8 +2,8 @@ import { QueryClientProvider } from '@tanstack/react-query';
import { isLiquidGlassAvailable } from 'expo-glass-effect'; import { isLiquidGlassAvailable } from 'expo-glass-effect';
import { Stack } from 'expo-router'; import { Stack } from 'expo-router';
import React from 'react'; import React from 'react';
import { ThemeProvider } from '../lib/theme';
import { queryClient } from '../lib/utils'; import { queryClient } from '../lib/utils';
import { ThemeProvider } from '../theme';
console.log('Fressh App Init', { console.log('Fressh App Init', {
isLiquidGlassAvailable: isLiquidGlassAvailable(), isLiquidGlassAvailable: isLiquidGlassAvailable(),

View File

@@ -6,12 +6,18 @@ export type AppTheme = {
surface: string; surface: string;
terminalBackground: string; terminalBackground: string;
border: string; border: string;
borderStrong: string;
textPrimary: string; textPrimary: string;
textSecondary: string; textSecondary: string;
muted: string; muted: string;
primary: string; primary: string;
buttonTextOnPrimary: string; buttonTextOnPrimary: string;
inputBackground: string; inputBackground: string;
danger: string;
overlay: string;
transparent: string;
shadow: string;
primaryDisabled: string;
}; };
}; };
@@ -21,12 +27,18 @@ export const darkTheme: AppTheme = {
surface: '#111B34', surface: '#111B34',
terminalBackground: '#0E172B', terminalBackground: '#0E172B',
border: '#2A3655', border: '#2A3655',
borderStrong: '#1E293B',
textPrimary: '#E5E7EB', textPrimary: '#E5E7EB',
textSecondary: '#C6CBD3', textSecondary: '#C6CBD3',
muted: '#9AA0A6', muted: '#9AA0A6',
primary: '#2563EB', primary: '#2563EB',
buttonTextOnPrimary: '#FFFFFF', buttonTextOnPrimary: '#FFFFFF',
inputBackground: '#0E172B', inputBackground: '#0E172B',
danger: '#FCA5A5',
overlay: 'rgba(0,0,0,0.4)',
transparent: 'transparent',
shadow: '#000000',
primaryDisabled: '#3B82F6',
}, },
}; };