fee283b5d6
Release APK / build (push) Has been cancelled
- Replace app icon with new Charbon·Lime brand assets (icon.png + adaptive-icon.png) - Apply design tokens to theme.ts: bg #1B2433, primary/success #88D656 (lime), text1 #EEF2F5 (cream) - Add ⚙ gear button (lime) in HomeScreen header → navigates to /settings - Expand settings: git provider selector (Gitea / GitHub / GitLab) with per-provider fields - Add lib/git.ts: unified pushIncidentToGit() for Gitea, GitHub (PUT), and GitLab APIs - Update incident detail screen to use lib/git.ts and dynamic provider routing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
458 lines
13 KiB
TypeScript
458 lines
13 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import {
|
|
View,
|
|
Text,
|
|
TextInput,
|
|
ScrollView,
|
|
Switch,
|
|
Pressable,
|
|
} from "react-native";
|
|
import * as SecureStore from "expo-secure-store";
|
|
import { Colors } from "@/constants/theme";
|
|
import { DEFAULT_TEMPLATE } from "@/lib/markdown";
|
|
import type { GitProvider } from "@/lib/git";
|
|
|
|
const KEYS = {
|
|
INPUT_MODE: "pref_input_mode",
|
|
HOME_VIEW: "pref_home_view",
|
|
AI_ENABLED: "pref_ai_enabled",
|
|
AI_KEY: "pref_ai_key",
|
|
MD_TEMPLATE: "md_template",
|
|
GIT_PROVIDER: "git_provider",
|
|
// Gitea
|
|
GITEA_URL: "gitea_url",
|
|
GITEA_TOKEN: "gitea_token",
|
|
GITEA_OWNER: "gitea_owner",
|
|
GITEA_REPO: "gitea_repo",
|
|
// GitHub
|
|
GITHUB_TOKEN: "github_token",
|
|
GITHUB_OWNER: "github_owner",
|
|
GITHUB_REPO: "github_repo",
|
|
// GitLab
|
|
GITLAB_URL: "gitlab_url",
|
|
GITLAB_TOKEN: "gitlab_token",
|
|
GITLAB_OWNER: "gitlab_owner",
|
|
GITLAB_REPO: "gitlab_repo",
|
|
} 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;
|
|
|
|
const PROVIDER_LABELS: Record<GitProvider, string> = {
|
|
gitea: "Gitea",
|
|
github: "GitHub",
|
|
gitlab: "GitLab",
|
|
};
|
|
|
|
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 [mdTemplate, setMdTemplate] = useState(DEFAULT_TEMPLATE);
|
|
const [saved, setSaved] = useState(false);
|
|
|
|
// Git provider
|
|
const [gitProvider, setGitProvider] = useState<GitProvider>("gitea");
|
|
// Gitea
|
|
const [giteaUrl, setGiteaUrl] = useState("");
|
|
const [giteaToken, setGiteaToken] = useState("");
|
|
const [giteaOwner, setGiteaOwner] = useState("");
|
|
const [giteaRepo, setGiteaRepo] = useState("");
|
|
// GitHub
|
|
const [githubToken, setGithubToken] = useState("");
|
|
const [githubOwner, setGithubOwner] = useState("");
|
|
const [githubRepo, setGithubRepo] = useState("");
|
|
// GitLab
|
|
const [gitlabUrl, setGitlabUrl] = useState("");
|
|
const [gitlabToken, setGitlabToken] = useState("");
|
|
const [gitlabOwner, setGitlabOwner] = useState("");
|
|
const [gitlabRepo, setGitlabRepo] = useState("");
|
|
|
|
useEffect(() => {
|
|
(async () => {
|
|
const [im, hv, ai, key, tpl, gp, gu, gt, go, gr, ghu, ghown, ghrepo, glu, glt, glo, glr] =
|
|
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.MD_TEMPLATE),
|
|
SecureStore.getItemAsync(KEYS.GIT_PROVIDER),
|
|
SecureStore.getItemAsync(KEYS.GITEA_URL),
|
|
SecureStore.getItemAsync(KEYS.GITEA_TOKEN),
|
|
SecureStore.getItemAsync(KEYS.GITEA_OWNER),
|
|
SecureStore.getItemAsync(KEYS.GITEA_REPO),
|
|
SecureStore.getItemAsync(KEYS.GITHUB_TOKEN),
|
|
SecureStore.getItemAsync(KEYS.GITHUB_OWNER),
|
|
SecureStore.getItemAsync(KEYS.GITHUB_REPO),
|
|
SecureStore.getItemAsync(KEYS.GITLAB_URL),
|
|
SecureStore.getItemAsync(KEYS.GITLAB_TOKEN),
|
|
SecureStore.getItemAsync(KEYS.GITLAB_OWNER),
|
|
SecureStore.getItemAsync(KEYS.GITLAB_REPO),
|
|
]);
|
|
if (im) setInputMode(im as "voice" | "form");
|
|
if (hv) setHomeView(hv as "dashboard" | "capture");
|
|
if (ai) setAiEnabled(ai === "true");
|
|
if (key) setAiKey(key);
|
|
if (tpl) setMdTemplate(tpl);
|
|
if (gp) setGitProvider(gp as GitProvider);
|
|
if (gu) setGiteaUrl(gu);
|
|
if (gt) setGiteaToken(gt);
|
|
if (go) setGiteaOwner(go);
|
|
if (gr) setGiteaRepo(gr);
|
|
if (ghu) setGithubToken(ghu);
|
|
if (ghown) setGithubOwner(ghown);
|
|
if (ghrepo) setGithubRepo(ghrepo);
|
|
if (glu) setGitlabUrl(glu);
|
|
if (glt) setGitlabToken(glt);
|
|
if (glo) setGitlabOwner(glo);
|
|
if (glr) setGitlabRepo(glr);
|
|
})();
|
|
}, []);
|
|
|
|
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.MD_TEMPLATE, mdTemplate),
|
|
SecureStore.setItemAsync(KEYS.GIT_PROVIDER, gitProvider),
|
|
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.GITHUB_TOKEN, githubToken),
|
|
SecureStore.setItemAsync(KEYS.GITHUB_OWNER, githubOwner),
|
|
SecureStore.setItemAsync(KEYS.GITHUB_REPO, githubRepo),
|
|
SecureStore.setItemAsync(KEYS.GITLAB_URL, gitlabUrl),
|
|
SecureStore.setItemAsync(KEYS.GITLAB_TOKEN, gitlabToken),
|
|
SecureStore.setItemAsync(KEYS.GITLAB_OWNER, gitlabOwner),
|
|
SecureStore.setItemAsync(KEYS.GITLAB_REPO, gitlabRepo),
|
|
]);
|
|
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 ? Colors.bg : Colors.text2,
|
|
fontWeight: "700",
|
|
fontSize: 13,
|
|
}}
|
|
>
|
|
{opt.label}
|
|
</Text>
|
|
</Pressable>
|
|
))}
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function Field({
|
|
label,
|
|
value,
|
|
onChange,
|
|
placeholder,
|
|
secure,
|
|
url,
|
|
}: {
|
|
label: string;
|
|
value: string;
|
|
onChange: (v: string) => void;
|
|
placeholder?: string;
|
|
secure?: boolean;
|
|
url?: boolean;
|
|
}) {
|
|
return (
|
|
<>
|
|
<Text style={LABEL}>{label}</Text>
|
|
<TextInput
|
|
style={INPUT}
|
|
value={value}
|
|
onChangeText={onChange}
|
|
placeholder={placeholder}
|
|
placeholderTextColor={Colors.textDim}
|
|
secureTextEntry={secure}
|
|
autoCapitalize="none"
|
|
keyboardType={url ? "url" : "default"}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
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 && (
|
|
<Field
|
|
label="API Key"
|
|
value={aiKey}
|
|
onChange={setAiKey}
|
|
placeholder="sk-..."
|
|
secure
|
|
/>
|
|
)}
|
|
|
|
<Text style={SECTION_TITLE}>Git Repository</Text>
|
|
|
|
<SegmentRow
|
|
label="Provider"
|
|
value={gitProvider}
|
|
onChange={(v) => setGitProvider(v as GitProvider)}
|
|
options={(["gitea", "github", "gitlab"] as GitProvider[]).map((p) => ({
|
|
key: p,
|
|
label: PROVIDER_LABELS[p],
|
|
}))}
|
|
/>
|
|
|
|
{gitProvider === "gitea" && (
|
|
<>
|
|
<Field
|
|
label="Instance URL"
|
|
value={giteaUrl}
|
|
onChange={setGiteaUrl}
|
|
placeholder="https://homegit.example.com"
|
|
url
|
|
/>
|
|
<Field
|
|
label="Token"
|
|
value={giteaToken}
|
|
onChange={setGiteaToken}
|
|
placeholder="Bearer token"
|
|
secure
|
|
/>
|
|
<Field
|
|
label="Owner"
|
|
value={giteaOwner}
|
|
onChange={setGiteaOwner}
|
|
placeholder="username or org"
|
|
/>
|
|
<Field
|
|
label="Repository"
|
|
value={giteaRepo}
|
|
onChange={setGiteaRepo}
|
|
placeholder="incidents"
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
{gitProvider === "github" && (
|
|
<>
|
|
<View
|
|
style={{
|
|
backgroundColor: Colors.surface,
|
|
borderRadius: 8,
|
|
padding: 12,
|
|
marginBottom: 14,
|
|
borderLeftWidth: 3,
|
|
borderLeftColor: Colors.primary,
|
|
}}
|
|
>
|
|
<Text style={{ color: Colors.text2, fontSize: 13 }}>
|
|
Uses api.github.com — no instance URL required.
|
|
</Text>
|
|
</View>
|
|
<Field
|
|
label="Personal Access Token"
|
|
value={githubToken}
|
|
onChange={setGithubToken}
|
|
placeholder="ghp_..."
|
|
secure
|
|
/>
|
|
<Field
|
|
label="Owner (username or org)"
|
|
value={githubOwner}
|
|
onChange={setGithubOwner}
|
|
placeholder="your-username"
|
|
/>
|
|
<Field
|
|
label="Repository"
|
|
value={githubRepo}
|
|
onChange={setGithubRepo}
|
|
placeholder="incidents"
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
{gitProvider === "gitlab" && (
|
|
<>
|
|
<Field
|
|
label="Instance URL"
|
|
value={gitlabUrl}
|
|
onChange={setGitlabUrl}
|
|
placeholder="https://gitlab.com"
|
|
url
|
|
/>
|
|
<Field
|
|
label="Personal Access Token"
|
|
value={gitlabToken}
|
|
onChange={setGitlabToken}
|
|
placeholder="glpat-..."
|
|
secure
|
|
/>
|
|
<Field
|
|
label="Namespace (user or group)"
|
|
value={gitlabOwner}
|
|
onChange={setGitlabOwner}
|
|
placeholder="your-username"
|
|
/>
|
|
<Field
|
|
label="Repository"
|
|
value={gitlabRepo}
|
|
onChange={setGitlabRepo}
|
|
placeholder="incidents"
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
<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: Colors.bg, fontSize: 16, fontWeight: "700" }}>
|
|
{saved ? "Saved ✓" : "Save Settings"}
|
|
</Text>
|
|
</Pressable>
|
|
</ScrollView>
|
|
);
|
|
}
|