feat: Phase 0 mobile app scaffold — Expo 55, SQLite, Feed/Import/Settings screens
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
import { useSQLiteContext } from 'expo-sqlite';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Platform, Pressable, ScrollView, StyleSheet,
|
||||
Text, TextInput, View,
|
||||
} from 'react-native';
|
||||
import { getSetting, setSetting, useSetting } from '@/db/queries';
|
||||
|
||||
export default function SettingsScreen() {
|
||||
const db = useSQLiteContext();
|
||||
|
||||
const storedUrl = useSetting('instance_url') ?? '';
|
||||
const storedHandle = useSetting('handle') ?? '';
|
||||
const storedPath = useSetting('auto_import_path') ?? '';
|
||||
|
||||
const [instanceUrl, setInstanceUrl] = useState(storedUrl);
|
||||
const [handle, setHandle] = useState(storedHandle);
|
||||
const [autoPath, setAutoPath] = useState(storedPath);
|
||||
const [saved, setSaved] = useState(false);
|
||||
|
||||
async function save() {
|
||||
await setSetting(db, 'instance_url', instanceUrl.trim());
|
||||
await setSetting(db, 'handle', handle.trim());
|
||||
if (Platform.OS === 'android') {
|
||||
await setSetting(db, 'auto_import_path', autoPath.trim());
|
||||
}
|
||||
setSaved(true);
|
||||
setTimeout(() => setSaved(false), 2000);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
||||
<Text style={styles.header}>Settings</Text>
|
||||
|
||||
<Section title="Instance (optional)">
|
||||
<Field
|
||||
label="Instance URL"
|
||||
placeholder="https://bincio.org"
|
||||
value={instanceUrl}
|
||||
onChangeText={setInstanceUrl}
|
||||
autoCapitalize="none"
|
||||
keyboardType="url"
|
||||
/>
|
||||
<Field
|
||||
label="Handle"
|
||||
placeholder="yourhandle"
|
||||
value={handle}
|
||||
onChangeText={setHandle}
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
<Text style={styles.hint}>
|
||||
Leave blank to use the app without a remote instance. When set, you can
|
||||
push activities to the instance and pull the web feed.
|
||||
</Text>
|
||||
</Section>
|
||||
|
||||
{Platform.OS === 'android' && (
|
||||
<Section title="Auto-import (Android)">
|
||||
<Field
|
||||
label="Watch directory"
|
||||
placeholder="/sdcard/Karoo/Rides"
|
||||
value={autoPath}
|
||||
onChangeText={setAutoPath}
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
<Text style={styles.hint}>
|
||||
New FIT files in this directory are imported automatically in the
|
||||
background. Leave blank to disable. Requires storage permission.
|
||||
</Text>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
<Pressable style={styles.saveButton} onPress={save}>
|
||||
<Text style={styles.saveButtonText}>
|
||||
{saved ? '✓ Saved' : 'Save'}
|
||||
</Text>
|
||||
</Pressable>
|
||||
|
||||
<Section title="About">
|
||||
<Row label="Version" value="0.1.0 (Phase 0)" />
|
||||
<Row label="Schema" value="BAS 1.0" />
|
||||
<Row label="Extraction" value="Pyodide (Phase 1)" />
|
||||
</Section>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionTitle}>{title}</Text>
|
||||
<View style={styles.sectionBody}>{children}</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function Field({
|
||||
label, placeholder, value, onChangeText, ...rest
|
||||
}: {
|
||||
label: string;
|
||||
placeholder: string;
|
||||
value: string;
|
||||
onChangeText: (v: string) => void;
|
||||
[key: string]: unknown;
|
||||
}) {
|
||||
return (
|
||||
<View style={styles.field}>
|
||||
<Text style={styles.fieldLabel}>{label}</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor="#52525b"
|
||||
value={value}
|
||||
onChangeText={onChangeText}
|
||||
{...rest}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function Row({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<View style={styles.row}>
|
||||
<Text style={styles.rowLabel}>{label}</Text>
|
||||
<Text style={styles.rowValue}>{value}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: '#09090b' },
|
||||
content: { padding: 16, paddingTop: 60, paddingBottom: 40 },
|
||||
header: { color: '#fff', fontSize: 22, fontWeight: '700', marginBottom: 24 },
|
||||
section: { marginBottom: 28 },
|
||||
sectionTitle: {
|
||||
color: '#a1a1aa', fontSize: 11, fontWeight: '600',
|
||||
letterSpacing: 0.8, marginBottom: 8,
|
||||
},
|
||||
sectionBody: {
|
||||
backgroundColor: '#18181b', borderRadius: 10,
|
||||
borderWidth: 1, borderColor: '#27272a', overflow: 'hidden',
|
||||
},
|
||||
field: { padding: 14, borderBottomWidth: 1, borderBottomColor: '#27272a' },
|
||||
fieldLabel: { color: '#71717a', fontSize: 11, marginBottom: 4 },
|
||||
input: { color: '#f4f4f5', fontSize: 15 },
|
||||
hint: { color: '#52525b', fontSize: 12, lineHeight: 16, padding: 12 },
|
||||
row: {
|
||||
flexDirection: 'row', justifyContent: 'space-between',
|
||||
paddingHorizontal: 14, paddingVertical: 12,
|
||||
borderBottomWidth: 1, borderBottomColor: '#27272a',
|
||||
},
|
||||
rowLabel: { color: '#a1a1aa', fontSize: 14 },
|
||||
rowValue: { color: '#71717a', fontSize: 14 },
|
||||
saveButton: {
|
||||
backgroundColor: '#2563eb', borderRadius: 10,
|
||||
paddingVertical: 14, alignItems: 'center', marginBottom: 28,
|
||||
},
|
||||
saveButtonText: { color: '#fff', fontWeight: '600', fontSize: 16 },
|
||||
});
|
||||
Reference in New Issue
Block a user