Initial scaffold: Expo SDK 56, expo-router, expo-sqlite, NativeWind
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
TextInput,
|
||||
ScrollView,
|
||||
Switch,
|
||||
Pressable,
|
||||
Alert,
|
||||
} from "react-native";
|
||||
import * as SecureStore from "expo-secure-store";
|
||||
import { Colors } from "@/constants/theme";
|
||||
import { DEFAULT_TEMPLATE } from "@/lib/markdown";
|
||||
|
||||
const KEYS = {
|
||||
INPUT_MODE: "pref_input_mode",
|
||||
HOME_VIEW: "pref_home_view",
|
||||
AI_ENABLED: "pref_ai_enabled",
|
||||
AI_KEY: "pref_ai_key",
|
||||
GITEA_URL: "gitea_url",
|
||||
GITEA_TOKEN: "gitea_token",
|
||||
GITEA_OWNER: "gitea_owner",
|
||||
GITEA_REPO: "gitea_repo",
|
||||
MD_TEMPLATE: "md_template",
|
||||
} as const;
|
||||
|
||||
const LABEL = {
|
||||
color: Colors.text2,
|
||||
fontSize: 11,
|
||||
fontWeight: "700" as const,
|
||||
textTransform: "uppercase" as const,
|
||||
letterSpacing: 0.6,
|
||||
marginBottom: 6,
|
||||
} as const;
|
||||
|
||||
const SECTION_TITLE = {
|
||||
color: Colors.text1,
|
||||
fontSize: 16,
|
||||
fontWeight: "700" as const,
|
||||
marginTop: 28,
|
||||
marginBottom: 12,
|
||||
} as const;
|
||||
|
||||
const INPUT = {
|
||||
backgroundColor: Colors.surface,
|
||||
borderColor: Colors.border,
|
||||
borderWidth: 1,
|
||||
borderRadius: 8,
|
||||
color: Colors.text1,
|
||||
padding: 12,
|
||||
fontSize: 14,
|
||||
marginBottom: 14,
|
||||
fontFamily: "monospace",
|
||||
} as const;
|
||||
|
||||
export default function SettingsScreen() {
|
||||
const [inputMode, setInputMode] = useState<"voice" | "form">("form");
|
||||
const [homeView, setHomeView] = useState<"dashboard" | "capture">("dashboard");
|
||||
const [aiEnabled, setAiEnabled] = useState(false);
|
||||
const [aiKey, setAiKey] = useState("");
|
||||
const [giteaUrl, setGiteaUrl] = useState("");
|
||||
const [giteaToken, setGiteaToken] = useState("");
|
||||
const [giteaOwner, setGiteaOwner] = useState("");
|
||||
const [giteaRepo, setGiteaRepo] = useState("");
|
||||
const [mdTemplate, setMdTemplate] = useState(DEFAULT_TEMPLATE);
|
||||
const [saved, setSaved] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const [im, hv, ai, key, gu, gt, go, gr, tpl] = await Promise.all([
|
||||
SecureStore.getItemAsync(KEYS.INPUT_MODE),
|
||||
SecureStore.getItemAsync(KEYS.HOME_VIEW),
|
||||
SecureStore.getItemAsync(KEYS.AI_ENABLED),
|
||||
SecureStore.getItemAsync(KEYS.AI_KEY),
|
||||
SecureStore.getItemAsync(KEYS.GITEA_URL),
|
||||
SecureStore.getItemAsync(KEYS.GITEA_TOKEN),
|
||||
SecureStore.getItemAsync(KEYS.GITEA_OWNER),
|
||||
SecureStore.getItemAsync(KEYS.GITEA_REPO),
|
||||
SecureStore.getItemAsync(KEYS.MD_TEMPLATE),
|
||||
]);
|
||||
if (im) setInputMode(im as "voice" | "form");
|
||||
if (hv) setHomeView(hv as "dashboard" | "capture");
|
||||
if (ai) setAiEnabled(ai === "true");
|
||||
if (key) setAiKey(key);
|
||||
if (gu) setGiteaUrl(gu);
|
||||
if (gt) setGiteaToken(gt);
|
||||
if (go) setGiteaOwner(go);
|
||||
if (gr) setGiteaRepo(gr);
|
||||
if (tpl) setMdTemplate(tpl);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
async function handleSave() {
|
||||
await Promise.all([
|
||||
SecureStore.setItemAsync(KEYS.INPUT_MODE, inputMode),
|
||||
SecureStore.setItemAsync(KEYS.HOME_VIEW, homeView),
|
||||
SecureStore.setItemAsync(KEYS.AI_ENABLED, String(aiEnabled)),
|
||||
SecureStore.setItemAsync(KEYS.AI_KEY, aiKey),
|
||||
SecureStore.setItemAsync(KEYS.GITEA_URL, giteaUrl),
|
||||
SecureStore.setItemAsync(KEYS.GITEA_TOKEN, giteaToken),
|
||||
SecureStore.setItemAsync(KEYS.GITEA_OWNER, giteaOwner),
|
||||
SecureStore.setItemAsync(KEYS.GITEA_REPO, giteaRepo),
|
||||
SecureStore.setItemAsync(KEYS.MD_TEMPLATE, mdTemplate),
|
||||
]);
|
||||
setSaved(true);
|
||||
setTimeout(() => setSaved(false), 2000);
|
||||
}
|
||||
|
||||
function ToggleRow({
|
||||
label,
|
||||
value,
|
||||
onValueChange,
|
||||
}: {
|
||||
label: string;
|
||||
value: boolean;
|
||||
onValueChange: (v: boolean) => void;
|
||||
}) {
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
backgroundColor: Colors.surface,
|
||||
borderRadius: 8,
|
||||
padding: 14,
|
||||
marginBottom: 10,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: Colors.text1, fontSize: 15 }}>{label}</Text>
|
||||
<Switch
|
||||
value={value}
|
||||
onValueChange={onValueChange}
|
||||
trackColor={{ true: Colors.primary, false: Colors.border }}
|
||||
thumbColor="#fff"
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function SegmentRow({
|
||||
label,
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
label: string;
|
||||
options: { key: string; label: string }[];
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<View style={{ marginBottom: 16 }}>
|
||||
<Text style={LABEL}>{label}</Text>
|
||||
<View style={{ flexDirection: "row", gap: 8 }}>
|
||||
{options.map((opt) => (
|
||||
<Pressable
|
||||
key={opt.key}
|
||||
onPress={() => onChange(opt.key)}
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor:
|
||||
value === opt.key ? Colors.primary : Colors.surface,
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
alignItems: "center",
|
||||
borderWidth: 1,
|
||||
borderColor:
|
||||
value === opt.key ? Colors.primary : Colors.border,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: value === opt.key ? "#fff" : Colors.text2,
|
||||
fontWeight: "600",
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
{opt.label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={{ flex: 1, backgroundColor: Colors.bg }}
|
||||
contentContainerStyle={{ padding: 16, paddingBottom: 48 }}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<Text style={SECTION_TITLE}>Preferences</Text>
|
||||
|
||||
<SegmentRow
|
||||
label="Default Input"
|
||||
value={inputMode}
|
||||
onChange={(v) => setInputMode(v as "voice" | "form")}
|
||||
options={[
|
||||
{ key: "form", label: "Form" },
|
||||
{ key: "voice", label: "Voice" },
|
||||
]}
|
||||
/>
|
||||
|
||||
<SegmentRow
|
||||
label="Home View"
|
||||
value={homeView}
|
||||
onChange={(v) => setHomeView(v as "dashboard" | "capture")}
|
||||
options={[
|
||||
{ key: "dashboard", label: "Dashboard" },
|
||||
{ key: "capture", label: "Quick Capture" },
|
||||
]}
|
||||
/>
|
||||
|
||||
<Text style={SECTION_TITLE}>AI (optional)</Text>
|
||||
<ToggleRow
|
||||
label="Enable AI assistance"
|
||||
value={aiEnabled}
|
||||
onValueChange={setAiEnabled}
|
||||
/>
|
||||
{aiEnabled && (
|
||||
<>
|
||||
<Text style={LABEL}>API Key</Text>
|
||||
<TextInput
|
||||
style={INPUT}
|
||||
value={aiKey}
|
||||
onChangeText={setAiKey}
|
||||
placeholder="sk-..."
|
||||
placeholderTextColor={Colors.textDim}
|
||||
secureTextEntry
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Text style={SECTION_TITLE}>Gitea</Text>
|
||||
<Text style={LABEL}>Instance URL</Text>
|
||||
<TextInput
|
||||
style={INPUT}
|
||||
value={giteaUrl}
|
||||
onChangeText={setGiteaUrl}
|
||||
placeholder="https://homegit.gyozamancave.fr"
|
||||
placeholderTextColor={Colors.textDim}
|
||||
autoCapitalize="none"
|
||||
keyboardType="url"
|
||||
/>
|
||||
<Text style={LABEL}>Token</Text>
|
||||
<TextInput
|
||||
style={INPUT}
|
||||
value={giteaToken}
|
||||
onChangeText={setGiteaToken}
|
||||
placeholder="Bearer token"
|
||||
placeholderTextColor={Colors.textDim}
|
||||
secureTextEntry
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
<Text style={LABEL}>Owner</Text>
|
||||
<TextInput
|
||||
style={INPUT}
|
||||
value={giteaOwner}
|
||||
onChangeText={setGiteaOwner}
|
||||
placeholder="username or org"
|
||||
placeholderTextColor={Colors.textDim}
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
<Text style={LABEL}>Repository</Text>
|
||||
<TextInput
|
||||
style={INPUT}
|
||||
value={giteaRepo}
|
||||
onChangeText={setGiteaRepo}
|
||||
placeholder="incidents"
|
||||
placeholderTextColor={Colors.textDim}
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
|
||||
<Text style={SECTION_TITLE}>Markdown Template</Text>
|
||||
<TextInput
|
||||
style={{ ...INPUT, minHeight: 200, textAlignVertical: "top" }}
|
||||
value={mdTemplate}
|
||||
onChangeText={setMdTemplate}
|
||||
multiline
|
||||
placeholder={DEFAULT_TEMPLATE}
|
||||
placeholderTextColor={Colors.textDim}
|
||||
/>
|
||||
|
||||
<Pressable
|
||||
onPress={handleSave}
|
||||
style={{
|
||||
backgroundColor: saved ? Colors.success : Colors.primary,
|
||||
borderRadius: 10,
|
||||
padding: 16,
|
||||
alignItems: "center",
|
||||
marginTop: 8,
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: "#fff", fontSize: 16, fontWeight: "700" }}>
|
||||
{saved ? "Saved ✓" : "Save Settings"}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user