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",
"esbenp.prettier-vscode",
"yoavbls.pretty-ts-errors",
"ctf0.duplicated-code-new"
"ctf0.duplicated-code-new",
"github.vscode-github-actions"
]
}

View File

@@ -6,11 +6,11 @@
"type": "module",
"scripts": {
"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",
"build:signed:aab": "tsx scripts/signed-build.ts",
"build:signed:apk": "tsx scripts/signed-build.ts --format apk",
"ios": "expo run:ios",
"web": "expo start --web",
"prebuild": "expo prebuild",
"prebuild:clean": "expo prebuild --clean",

View File

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

View File

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

View File

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

View File

@@ -8,15 +8,22 @@ import { useQuery } from '@tanstack/react-query';
import { Link } from 'expo-router';
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { listSshShellsQueryOptions } from '@/lib/query-fns';
import { useTheme ,type AppTheme } from '@/lib/theme';
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 };
function ShellList() {
function ShellContent() {
const connectionsWithShells = useQuery(listSshShellsQueryOptions);
if (!connectionsWithShells.data) {
@@ -26,6 +33,8 @@ function ShellList() {
}
function LoadingState() {
const theme = useTheme();
const styles = React.useMemo(() => makeStyles(theme), [theme]);
return (
<View style={styles.container}>
<Text style={styles.text}>Loading...</Text>
@@ -67,6 +76,8 @@ function LoadedState({
}
function EmptyState() {
const theme = useTheme();
const styles = React.useMemo(() => makeStyles(theme), [theme]);
return (
<View style={styles.container}>
<Text style={styles.text}>No active shells. Connect from Host tab.</Text>
@@ -76,6 +87,8 @@ function EmptyState() {
}
function ShellCard({ shell }: { shell: ShellWithConnection }) {
const theme = useTheme();
const styles = React.useMemo(() => makeStyles(theme), [theme]);
return (
<View style={styles.container}>
<Text style={styles.text}>{shell.connectionId}</Text>
@@ -93,7 +106,9 @@ function ShellCard({ shell }: { shell: ShellWithConnection }) {
);
}
const styles = StyleSheet.create({
container: { flex: 1, alignItems: 'center', justifyContent: 'center' },
text: { color: 'black', marginBottom: 8 },
});
function makeStyles(theme: AppTheme) {
return StyleSheet.create({
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 { Stack } from 'expo-router';
import React from 'react';
import { ThemeProvider } from '../lib/theme';
import { queryClient } from '../lib/utils';
import { ThemeProvider } from '../theme';
console.log('Fressh App Init', {
isLiquidGlassAvailable: isLiquidGlassAvailable(),

View File

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