"use client" import { useCallback, useEffect, useMemo, useState } from "react" import { CheckCircle2, GitBranch, Github, Globe, Loader2, Trash2, XCircle, } from "lucide-react" import { useTranslations } from "next-intl" import { toast } from "sonner" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Badge } from "@/components/ui/badge" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog" import { detectGit, getGitSettings, updateGitSettings, testGitPath, getGitHubAccounts, updateGitHubAccounts, validateGitHubToken, getAccountToken, deleteAccountToken, } from "@/lib/tauri" import type { GitDetectResult, GitHubAccount, GitHubAccountsSettings, } from "@/lib/types" import { AddGitHubAccountDialog } from "./add-github-account-dialog" import { AddGitAccountDialog } from "./add-git-account-dialog" // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function isGitHubAccount(account: GitHubAccount): boolean { const url = account.server_url.toLowerCase() return url.includes("github.com") } // --------------------------------------------------------------------------- // Shared account row component // --------------------------------------------------------------------------- function AccountRow({ account, testingId, onTest, onSetDefault, onRemove, t, }: { account: GitHubAccount testingId: string | null onTest: (account: GitHubAccount) => void onSetDefault: (id: string) => void onRemove: (account: GitHubAccount) => void t: ReturnType> }) { return (
{account.avatar_url ? ( {account.username} ) : (
{account.username[0]?.toUpperCase()}
)}
{account.username} {account.is_default && ( {t("defaultLabel")} )}
{account.server_url} {account.scopes.length > 0 && ( <> ยท {account.scopes.join(", ")} )}
{!account.is_default && ( )}
) } // --------------------------------------------------------------------------- // Main component // --------------------------------------------------------------------------- export function VersionControlSettings() { const t = useTranslations("VersionControlSettings") const [loading, setLoading] = useState(true) const [gitInfo, setGitInfo] = useState(null) const [customPath, setCustomPath] = useState("") const [editingPath, setEditingPath] = useState(false) const [savingGit, setSavingGit] = useState(false) const [accounts, setAccounts] = useState({ accounts: [], }) const [addGitHubOpen, setAddGitHubOpen] = useState(false) const [addGitOpen, setAddGitOpen] = useState(false) const [testingAccountId, setTestingAccountId] = useState(null) const [removeTarget, setRemoveTarget] = useState(null) // Split accounts into GitHub vs other const githubAccounts = useMemo( () => accounts.accounts.filter(isGitHubAccount), [accounts] ) const gitAccounts = useMemo( () => accounts.accounts.filter((a) => !isGitHubAccount(a)), [accounts] ) const loadData = useCallback(async () => { setLoading(true) try { const [git, settings, ghAccounts] = await Promise.all([ detectGit(), getGitSettings(), getGitHubAccounts(), ]) setGitInfo(git) setCustomPath(settings.custom_path ?? "") setAccounts(ghAccounts) } catch (err) { const message = err instanceof Error ? err.message : String(err) toast.error(t("loadFailed", { message })) } finally { setLoading(false) } }, [t]) useEffect(() => { loadData().catch(console.error) }, [loadData]) // --- Git path handlers --- const handleSaveGit = useCallback(async () => { const trimmed = customPath.trim() setSavingGit(true) try { // Test first if a custom path is provided if (trimmed) { const result = await testGitPath(trimmed) if (!result.installed) { toast.error( t("testFailed", { message: "not a valid git executable" }) ) return } } await updateGitSettings({ custom_path: trimmed || null }) const git = await detectGit() setGitInfo(git) setEditingPath(false) toast.success(t("saveSuccess")) } catch (err) { const message = err instanceof Error ? err.message : String(err) toast.error(t("saveFailed", { message })) } finally { setSavingGit(false) } }, [customPath, t]) const handleCancelEdit = useCallback(() => { setEditingPath(false) // Restore to the saved value getGitSettings() .then((s) => setCustomPath(s.custom_path ?? "")) .catch(() => {}) }, []) // --- Shared account handlers --- const handleAccountAdded = useCallback( async (account: GitHubAccount) => { const updated: GitHubAccountsSettings = { accounts: [ ...accounts.accounts.map((a) => account.is_default ? { ...a, is_default: false } : a ), account, ], } try { const saved = await updateGitHubAccounts(updated) setAccounts(saved) toast.success(t("addSuccess", { username: account.username })) } catch (err) { const message = err instanceof Error ? err.message : String(err) toast.error(t("addFailed", { message })) } }, [accounts, t] ) const handleTestConnection = useCallback( async (account: GitHubAccount) => { setTestingAccountId(account.id) try { const token = await getAccountToken(account.id) if (!token) { toast.error(t("connectionFailed", { message: "Token not found" })) return } if (isGitHubAccount(account)) { const result = await validateGitHubToken(account.server_url, token) if (result.success) { toast.success(t("connectionSuccess")) } else { toast.error( t("connectionFailed", { message: result.message ?? "Unknown error", }) ) } } else { // For non-GitHub accounts we can't validate via API, // just confirm the token exists in keyring. toast.success(t("connectionSuccess")) } } catch (err) { const message = err instanceof Error ? err.message : String(err) toast.error(t("connectionFailed", { message })) } finally { setTestingAccountId(null) } }, [t] ) const handleSetDefault = useCallback( async (accountId: string) => { const updated: GitHubAccountsSettings = { accounts: accounts.accounts.map((a) => ({ ...a, is_default: a.id === accountId, })), } try { const saved = await updateGitHubAccounts(updated) setAccounts(saved) toast.success(t("defaultSet")) } catch (err) { const message = err instanceof Error ? err.message : String(err) toast.error(message) } }, [accounts, t] ) const handleRemoveAccount = useCallback(async () => { if (!removeTarget) return const updated: GitHubAccountsSettings = { accounts: accounts.accounts.filter((a) => a.id !== removeTarget.id), } try { await deleteAccountToken(removeTarget.id) const saved = await updateGitHubAccounts(updated) setAccounts(saved) toast.success(t("removeSuccess")) } catch (err) { const message = err instanceof Error ? err.message : String(err) toast.error(message) } finally { setRemoveTarget(null) } }, [accounts, removeTarget, t]) // --- Render --- if (loading) { return (
{t("loading")}
) } return (

{t("sectionTitle")}

{t("sectionDescription")}

{/* ---- Git Configuration ---- */}

{t("gitTitle")}

{gitInfo?.installed ? ( <> {t("gitDetected")} ) : ( <> {t("gitNotFound")} )} {gitInfo?.version && ( {gitInfo.version} )}
{!editingPath && ( )}
{gitInfo?.path && (

{t("gitPath")}: {gitInfo.path}

)}
{editingPath && (
setCustomPath(e.target.value)} placeholder={t("customGitPathPlaceholder")} className="flex-1" autoFocus />

{t("customGitPathHint")}

)}
{/* ---- GitHub Accounts ---- */}

{t("githubTitle")}

{t("githubDescription")}

{githubAccounts.length === 0 ? (
{t("noAccounts")}
) : (
{githubAccounts.map((account) => ( ))}
)}
{/* ---- Git Accounts (non-GitHub) ---- */}

{t("gitAccount.sectionTitle")}

{t("gitAccount.sectionDescription")}

{gitAccounts.length === 0 ? (
{t("gitAccount.noAccounts")}
) : (
{gitAccounts.map((account) => ( ))}
)}
{/* Dialogs */} {/* Remove Confirmation */} !open && setRemoveTarget(null)} > {t("removeConfirmTitle")} {t("removeConfirmMessage", { username: removeTarget?.username ?? "", })} {t("removeCancel")} {t("removeConfirm")}
) }