Initial commit

This commit is contained in:
xggz
2026-03-06 22:56:13 +08:00
commit 54d1097b41
273 changed files with 92457 additions and 0 deletions

View File

@@ -0,0 +1,137 @@
"use client"
import { useState } from "react"
import { open } from "@tauri-apps/plugin-dialog"
import { cloneRepository, openFolderWindow } from "@/lib/tauri"
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import { FolderOpen, Loader2 } from "lucide-react"
interface CloneDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
}
export function CloneDialog({ open: isOpen, onOpenChange }: CloneDialogProps) {
const [url, setUrl] = useState("")
const [targetDir, setTargetDir] = useState("")
const [cloning, setCloning] = useState(false)
const [error, setError] = useState<string | null>(null)
const handleBrowse = async () => {
const selected = await open({ directory: true, multiple: false })
if (selected) {
setTargetDir(selected)
}
}
const handleClone = async () => {
if (!url || !targetDir) return
// Derive repo name from URL
const repoName =
url
.replace(/\.git$/, "")
.split("/")
.pop() ?? "repo"
const fullPath = `${targetDir}/${repoName}`
setCloning(true)
setError(null)
try {
await cloneRepository(url, fullPath)
await openFolderWindow(fullPath)
onOpenChange(false)
resetForm()
} catch (e) {
setError(e instanceof Error ? e.message : String(e))
} finally {
setCloning(false)
}
}
const resetForm = () => {
setUrl("")
setTargetDir("")
setError(null)
}
return (
<Dialog
open={isOpen}
onOpenChange={(v) => {
onOpenChange(v)
if (!v) resetForm()
}}
>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Clone Repository</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-2">
<div className="space-y-2">
<Label htmlFor="repo-url">Repository URL</Label>
<Input
id="repo-url"
placeholder="https://github.com/user/repo.git"
value={url}
onChange={(e) => setUrl(e.target.value)}
disabled={cloning}
/>
</div>
<div className="space-y-2">
<Label htmlFor="target-dir">Directory</Label>
<div className="flex gap-2">
<Input
id="target-dir"
placeholder="Select target directory..."
value={targetDir}
onChange={(e) => setTargetDir(e.target.value)}
disabled={cloning}
className="flex-1"
/>
<Button
variant="outline"
size="icon"
onClick={handleBrowse}
disabled={cloning}
>
<FolderOpen className="h-4 w-4" />
</Button>
</div>
</div>
{error && <p className="text-sm text-destructive">{error}</p>}
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => onOpenChange(false)}
disabled={cloning}
>
Cancel
</Button>
<Button
onClick={handleClone}
disabled={!url || !targetDir || cloning}
>
{cloning && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Clone
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,42 @@
"use client"
import { useState } from "react"
import { FolderOpen, GitBranch } from "lucide-react"
import { open } from "@tauri-apps/plugin-dialog"
import { openFolderWindow } from "@/lib/tauri"
import { Button } from "@/components/ui/button"
import { CloneDialog } from "./clone-dialog"
export function FolderActions() {
const [cloneOpen, setCloneOpen] = useState(false)
const handleOpen = async () => {
const selected = await open({ directory: true, multiple: false })
if (selected) {
await openFolderWindow(selected)
}
}
return (
<div className="w-full flex flex-col gap-1 px-3">
<Button
variant="ghost"
className="justify-start gap-2 h-9"
onClick={handleOpen}
>
<FolderOpen className="h-4 w-4" />
Open Folder
</Button>
<Button
variant="ghost"
className="justify-start gap-2 h-9"
onClick={() => setCloneOpen(true)}
>
<GitBranch className="h-4 w-4" />
Clone Repository
</Button>
<CloneDialog open={cloneOpen} onOpenChange={setCloneOpen} />
</div>
)
}

View File

@@ -0,0 +1,114 @@
"use client"
import { useState } from "react"
import { Search, X, FolderOpen } from "lucide-react"
import { formatDistanceToNow } from "date-fns"
import { zhCN } from "date-fns/locale"
import { openFolderWindow, removeFolderFromHistory } from "@/lib/tauri"
import type { FolderHistoryEntry } from "@/lib/types"
import { Input } from "@/components/ui/input"
interface FolderListProps {
history: FolderHistoryEntry[]
loading: boolean
onRefresh: () => void
}
export function FolderList({ history, loading, onRefresh }: FolderListProps) {
const [search, setSearch] = useState("")
const filtered = search
? history.filter(
(h) =>
h.name.toLowerCase().includes(search.toLowerCase()) ||
h.path.toLowerCase().includes(search.toLowerCase())
)
: history
const handleOpen = async (path: string) => {
try {
await openFolderWindow(path)
} catch (e) {
console.error("Failed to open folder:", e)
}
}
const handleRemove = async (e: React.MouseEvent, path: string) => {
e.stopPropagation()
try {
await removeFolderFromHistory(path)
onRefresh()
} catch (err) {
console.error("Failed to remove folder:", err)
}
}
return (
<div className="flex-1 flex flex-col min-w-0 p-8">
<div className="relative mb-4">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="搜索文件夹..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-9 h-9"
/>
</div>
<div className="flex-1 overflow-y-auto">
{loading ? (
<div className="flex items-center justify-center h-32 text-sm text-muted-foreground">
Loading...
</div>
) : filtered.length === 0 ? (
<div className="flex flex-col items-center justify-center h-32 text-sm text-muted-foreground gap-2">
<FolderOpen className="h-8 w-8" />
<span></span>
</div>
) : (
filtered.map((entry) => (
<div
key={entry.id}
role="button"
tabIndex={0}
className="w-full text-left p-3 rounded-lg hover:bg-accent/50 transition-colors group flex items-start gap-3 cursor-pointer"
onClick={() => handleOpen(entry.path)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault()
handleOpen(entry.path)
}
}}
>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<span className="text-sm font-medium truncate">
{entry.name}
</span>
</div>
<div className="text-xs text-muted-foreground truncate mt-0.5">
{entry.path}
</div>
<div className="flex items-center gap-2 mt-0.5">
<span className="text-xs text-muted-foreground">
{formatDistanceToNow(new Date(entry.last_opened_at), {
addSuffix: true,
locale: zhCN,
})}
</span>
</div>
</div>
<button
className="opacity-0 group-hover:opacity-100 transition-opacity p-1 hover:bg-destructive/10 rounded shrink-0 mt-0.5"
onClick={(e) => handleRemove(e, entry.path)}
title="从历史中移除"
>
<X className="h-3.5 w-3.5 text-muted-foreground hover:text-destructive" />
</button>
</div>
))
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,15 @@
"use client"
import { MessageCircleCode } from "lucide-react"
export function SoftwareInfo() {
return (
<div className="w-full flex gap-4 px-6 py-8">
<MessageCircleCode className="size-12" />
<div className="flex flex-col">
<span className="text-base">Codeg</span>
<span className="text-sm text-muted-foreground">version 0.0.1</span>
</div>
</div>
)
}

View File

@@ -0,0 +1,68 @@
"use client"
import { useState, useEffect } from "react"
import { Settings } from "lucide-react"
import { loadFolderHistory } from "@/lib/tauri"
import type { FolderHistoryEntry } from "@/lib/types"
import { FolderList } from "@/components/welcome/folder-list"
import { FolderActions } from "@/components/welcome/folder-actions"
import { SoftwareInfo } from "@/components/welcome/software-info"
import { Button } from "@/components/ui/button"
import { openSettingsWindow } from "@/lib/tauri"
import { AppTitleBar } from "@/components/layout/app-title-bar"
export function WelcomeScreen() {
const [history, setHistory] = useState<FolderHistoryEntry[]>([])
const [loading, setLoading] = useState(true)
const refreshHistory = async () => {
try {
setHistory(await loadFolderHistory())
} finally {
setLoading(false)
}
}
useEffect(() => {
refreshHistory()
}, [])
return (
<div className="h-screen flex flex-col overflow-hidden bg-background text-foreground">
<AppTitleBar
center={
<span className="text-sm font-bold tracking-tight">
使Codeg
</span>
}
right={
<Button
variant="ghost"
size="icon"
className="h-6 w-6 hover:text-foreground/80"
onClick={() => {
openSettingsWindow().catch((err) => {
console.error("[WelcomeScreen] failed to open settings:", err)
})
}}
title="Open Settings"
>
<Settings className="h-3.5 w-3.5" />
</Button>
}
/>
<div className="flex-1 flex overflow-hidden">
<div className="w-60 shrink-0 flex flex-col border-r">
<SoftwareInfo />
<FolderActions />
</div>
<FolderList
history={history}
loading={loading}
onRefresh={refreshHistory}
/>
</div>
</div>
)
}