From fee283b5d65e523756650d1b0ca34c378bdcfeb7 Mon Sep 17 00:00:00 2001 From: billisdead Date: Tue, 23 Jun 2026 10:00:49 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20apply=20Charbon=C2=B7Lime=20design=20sy?= =?UTF-8?q?stem,=20add=20gear=20icon,=20multi-provider=20git=20settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- app.json | 5 +- app/_layout.tsx | 16 ++- app/incident/[id].tsx | 71 +++++++--- app/index.tsx | 2 +- app/settings.tsx | 290 ++++++++++++++++++++++++++++++--------- assets/adaptive-icon.png | Bin 5336 -> 10211 bytes assets/icon.png | Bin 0 -> 39115 bytes constants/theme.ts | 24 ++-- lib/git.ts | 174 +++++++++++++++++++++++ 9 files changed, 480 insertions(+), 102 deletions(-) create mode 100644 assets/icon.png create mode 100644 lib/git.ts diff --git a/app.json b/app.json index 6a0ea7c..7ee6015 100644 --- a/app.json +++ b/app.json @@ -5,12 +5,13 @@ "version": "0.1.0", "orientation": "portrait", "userInterfaceStyle": "dark", - "backgroundColor": "#0f1117", + "icon": "./assets/icon.png", + "backgroundColor": "#1B2433", "scheme": "sheethappens", "android": { "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", - "backgroundColor": "#0f1117" + "backgroundColor": "#1B2433" }, "package": "fr.gyozamancave.sheethappens", "permissions": [ diff --git a/app/_layout.tsx b/app/_layout.tsx index 2e56cf7..dedeab5 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,4 +1,5 @@ import { useEffect } from "react"; +import { Pressable, Text } from "react-native"; import { Stack, router } from "expo-router"; import { StatusBar } from "expo-status-bar"; import { GestureHandlerRootView } from "react-native-gesture-handler"; @@ -28,7 +29,20 @@ export default function RootLayout() { animation: "slide_from_right", }} > - + ( + router.push("/settings")} + style={{ paddingLeft: 12, paddingRight: 4, paddingVertical: 6 }} + > + + + ), + }} + /> diff --git a/app/incident/[id].tsx b/app/incident/[id].tsx index 185bc06..8980e14 100644 --- a/app/incident/[id].tsx +++ b/app/incident/[id].tsx @@ -12,7 +12,8 @@ import * as Clipboard from "expo-clipboard"; import * as SecureStore from "expo-secure-store"; import { getIncidentById, updateIncident } from "@/lib/db"; import { renderMarkdown } from "@/lib/markdown"; -import { pushIncidentToGitea } from "@/lib/gitea"; +import { pushIncidentToGit } from "@/lib/git"; +import type { GitProvider } from "@/lib/git"; import type { Incident } from "@/types/incident"; import { Colors } from "@/constants/theme"; @@ -59,28 +60,62 @@ export default function IncidentDetailScreen() { setIncident({ ...incident, status: "resolved" }); } - async function handleGiteaPush() { + async function handleGitPush() { if (!incident) return; - const [url, token, owner, repo, template] = await Promise.all([ - SecureStore.getItemAsync("gitea_url"), - SecureStore.getItemAsync("gitea_token"), - SecureStore.getItemAsync("gitea_owner"), - SecureStore.getItemAsync("gitea_repo"), - SecureStore.getItemAsync("md_template"), - ]); - if (!url || !token || !owner || !repo) { - Alert.alert("Gitea not configured", "Set up Gitea in Settings first."); + const [provider, template, giteaUrl, giteaToken, giteaOwner, giteaRepo, githubToken, githubOwner, githubRepo, gitlabUrl, gitlabToken, gitlabOwner, gitlabRepo] = + await Promise.all([ + SecureStore.getItemAsync("git_provider"), + SecureStore.getItemAsync("md_template"), + SecureStore.getItemAsync("gitea_url"), + SecureStore.getItemAsync("gitea_token"), + SecureStore.getItemAsync("gitea_owner"), + SecureStore.getItemAsync("gitea_repo"), + SecureStore.getItemAsync("github_token"), + SecureStore.getItemAsync("github_owner"), + SecureStore.getItemAsync("github_repo"), + SecureStore.getItemAsync("gitlab_url"), + SecureStore.getItemAsync("gitlab_token"), + SecureStore.getItemAsync("gitlab_owner"), + SecureStore.getItemAsync("gitlab_repo"), + ]); + + const p = (provider ?? "gitea") as GitProvider; + + let url: string | undefined; + let token: string | null; + let owner: string | null; + let repo: string | null; + + if (p === "gitea") { + url = giteaUrl ?? undefined; + token = giteaToken; + owner = giteaOwner; + repo = giteaRepo; + } else if (p === "github") { + token = githubToken; + owner = githubOwner; + repo = githubRepo; + } else { + url = gitlabUrl ?? undefined; + token = gitlabToken; + owner = gitlabOwner; + repo = gitlabRepo; + } + + if (!token || !owner || !repo) { + Alert.alert("Git not configured", `Set up ${p.charAt(0).toUpperCase() + p.slice(1)} in Settings first.`); return; } + setPushing(true); - const result = await pushIncidentToGitea( + const result = await pushIncidentToGit( incident, - { url, token, owner, repo }, + { provider: p, url, token, owner, repo }, template ?? undefined ); setPushing(false); if (result.success) { - Alert.alert("Pushed", result.url ? `${result.url}` : "Push successful."); + Alert.alert("Pushed", result.url ?? "Push successful."); } else { Alert.alert("Push failed", result.error ?? "Unknown error"); } @@ -117,7 +152,7 @@ export default function IncidentDetailScreen() { - {pushing ? "Pushing…" : "Push to Gitea"} + {pushing ? "Pushing…" : "Push to Git"} diff --git a/app/index.tsx b/app/index.tsx index ec85399..3d87170 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -100,7 +100,7 @@ export default function HomeScreen() { backgroundColor: item.status === "open" ? "#f59e0b20" - : "#22c55e20", + : "#88D65620", borderRadius: 4, paddingHorizontal: 8, paddingVertical: 2, diff --git a/app/settings.tsx b/app/settings.tsx index 186f6d2..5e06173 100644 --- a/app/settings.tsx +++ b/app/settings.tsx @@ -6,22 +6,33 @@ import { 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"; +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", - MD_TEMPLATE: "md_template", + // 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 = { @@ -53,40 +64,76 @@ const INPUT = { fontFamily: "monospace", } as const; +const PROVIDER_LABELS: Record = { + 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("gitea"); + // Gitea 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); + // 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, 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), - ]); + 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 (tpl) setMdTemplate(tpl); + 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); })(); }, []); @@ -96,11 +143,19 @@ export default function SettingsScreen() { 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.MD_TEMPLATE, mdTemplate), + 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); @@ -171,8 +226,8 @@ export default function SettingsScreen() { > @@ -185,6 +240,38 @@ export default function SettingsScreen() { ); } + function Field({ + label, + value, + onChange, + placeholder, + secure, + url, + }: { + label: string; + value: string; + onChange: (v: string) => void; + placeholder?: string; + secure?: boolean; + url?: boolean; + }) { + return ( + <> + {label} + + + ); + } + return ( {aiEnabled && ( + + )} + + Git Repository + + setGitProvider(v as GitProvider)} + options={(["gitea", "github", "gitlab"] as GitProvider[]).map((p) => ({ + key: p, + label: PROVIDER_LABELS[p], + }))} + /> + + {gitProvider === "gitea" && ( <> - API Key - + + + )} - Gitea - Instance URL - - Token - - Owner - - Repository - + {gitProvider === "github" && ( + <> + + + Uses api.github.com — no instance URL required. + + + + + + + )} + + {gitProvider === "gitlab" && ( + <> + + + + + + )} Markdown Template - + {saved ? "Saved ✓" : "Save Settings"} diff --git a/assets/adaptive-icon.png b/assets/adaptive-icon.png index 154ac6860911249051b5013489aa98734863ec0a..8de41484c9953a5e70330783f99c7b4b530fed59 100644 GIT binary patch literal 10211 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 diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..05c50db86102e7f959459ac41ee0565240f0b3d3 GIT binary patch literal 39115 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 GcmEeYE`weG literal 0 HcmV?d00001 diff --git a/constants/theme.ts b/constants/theme.ts index ac80a39..a458332 100644 --- a/constants/theme.ts +++ b/constants/theme.ts @@ -1,16 +1,16 @@ export const Colors = { - bg: "#0f1117", - surface: "#1a1d27", - surface2: "#252836", - primary: "#6366f1", - primaryDim: "#4338ca", - success: "#22c55e", - warning: "#f59e0b", - danger: "#ef4444", - text1: "#f1f5f9", - text2: "#94a3b8", - textDim: "#64748b", - border: "#2d3147", + bg: "#1B2433", + surface: "#222D3D", + surface2: "#293648", + primary: "#88D656", + primaryDim: "#5FA33A", + success: "#88D656", + warning: "#F5A623", + danger: "#EF4444", + text1: "#EEF2F5", + text2: "#8FA5BF", + textDim: "#4E6480", + border: "#2E3F55", } as const; export const FontFamily = { diff --git a/lib/git.ts b/lib/git.ts new file mode 100644 index 0000000..24af9b5 --- /dev/null +++ b/lib/git.ts @@ -0,0 +1,174 @@ +import { incidentToFilename, renderMarkdown } from "./markdown"; +import type { Incident } from "@/types/incident"; + +export type GitProvider = "gitea" | "github" | "gitlab"; + +export interface GitConfig { + provider: GitProvider; + url?: string; // required for gitea and self-hosted gitlab + token: string; + owner: string; + repo: string; +} + +export interface PushResult { + success: boolean; + url?: string; + error?: string; +} + +export async function pushIncidentToGit( + incident: Incident, + config: GitConfig, + markdownTemplate?: string +): Promise { + switch (config.provider) { + case "gitea": + return pushGitea(incident, config, markdownTemplate); + case "github": + return pushGitHub(incident, config, markdownTemplate); + case "gitlab": + return pushGitLab(incident, config, markdownTemplate); + } +} + +async function pushGitea( + incident: Incident, + config: GitConfig, + markdownTemplate?: string +): Promise { + const filepath = incidentToFilename(incident); + const content = renderMarkdown(incident, markdownTemplate); + const base64Content = btoa(unescape(encodeURIComponent(content))); + const apiBase = (config.url ?? "").replace(/\/$/, ""); + const endpoint = `${apiBase}/api/v1/repos/${config.owner}/${config.repo}/contents/${filepath}`; + + try { + const existing = await fetch(endpoint, { + headers: { Authorization: `Bearer ${config.token}` }, + }); + let sha: string | undefined; + if (existing.ok) { + const data = await existing.json() as { sha?: string }; + sha = data.sha; + } + + const body: Record = { + message: `incident: ${incident.title}`, + content: base64Content, + }; + if (sha) body.sha = sha; + + const res = await fetch(endpoint, { + method: "POST", + headers: { + Authorization: `Bearer ${config.token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + if (!res.ok) { + return { success: false, error: `HTTP ${res.status}: ${await res.text()}` }; + } + const data = await res.json() as { content?: { html_url?: string } }; + return { success: true, url: data.content?.html_url }; + } catch (e) { + return { success: false, error: e instanceof Error ? e.message : String(e) }; + } +} + +async function pushGitHub( + incident: Incident, + config: GitConfig, + markdownTemplate?: string +): Promise { + const filepath = incidentToFilename(incident); + const content = renderMarkdown(incident, markdownTemplate); + const base64Content = btoa(unescape(encodeURIComponent(content))); + const endpoint = `https://api.github.com/repos/${config.owner}/${config.repo}/contents/${filepath}`; + + try { + const existing = await fetch(endpoint, { + headers: { + Authorization: `Bearer ${config.token}`, + Accept: "application/vnd.github+json", + }, + }); + let sha: string | undefined; + if (existing.ok) { + const data = await existing.json() as { sha?: string }; + sha = data.sha; + } + + const body: Record = { + message: `incident: ${incident.title}`, + content: base64Content, + }; + if (sha) body.sha = sha; + + const res = await fetch(endpoint, { + method: "PUT", + headers: { + Authorization: `Bearer ${config.token}`, + Accept: "application/vnd.github+json", + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + if (!res.ok) { + return { success: false, error: `HTTP ${res.status}: ${await res.text()}` }; + } + const data = await res.json() as { content?: { html_url?: string } }; + return { success: true, url: data.content?.html_url }; + } catch (e) { + return { success: false, error: e instanceof Error ? e.message : String(e) }; + } +} + +async function pushGitLab( + incident: Incident, + config: GitConfig, + markdownTemplate?: string +): Promise { + const filepath = incidentToFilename(incident); + const content = renderMarkdown(incident, markdownTemplate); + const base64Content = btoa(unescape(encodeURIComponent(content))); + const apiBase = (config.url ?? "https://gitlab.com").replace(/\/$/, ""); + const projectId = encodeURIComponent(`${config.owner}/${config.repo}`); + const encodedPath = encodeURIComponent(filepath); + const endpoint = `${apiBase}/api/v4/projects/${projectId}/repository/files/${encodedPath}`; + + try { + const existing = await fetch(`${endpoint}?ref=HEAD`, { + headers: { "PRIVATE-TOKEN": config.token }, + }); + const method = existing.ok ? "PUT" : "POST"; + + const res = await fetch(endpoint, { + method, + headers: { + "PRIVATE-TOKEN": config.token, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + branch: "main", + content: base64Content, + encoding: "base64", + commit_message: `incident: ${incident.title}`, + }), + }); + + if (!res.ok) { + return { success: false, error: `HTTP ${res.status}: ${await res.text()}` }; + } + const data = await res.json() as { file_path?: string }; + const webUrl = data.file_path + ? `${apiBase}/${config.owner}/${config.repo}/-/blob/main/${data.file_path}` + : undefined; + return { success: true, url: webUrl }; + } catch (e) { + return { success: false, error: e instanceof Error ? e.message : String(e) }; + } +}