diff --git a/src/components/biz/sync-from-files-button.tsx b/src/components/biz/sync-from-files-button.tsx new file mode 100644 index 0000000..db81e9f --- /dev/null +++ b/src/components/biz/sync-from-files-button.tsx @@ -0,0 +1,106 @@ +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { useFileConnections } from "@/store/file-connection"; +import { toast } from "sonner"; +import { upsertLanguageTranslations } from "@/lib/db"; +import { flattenValues } from "@/lib/i18n-structure"; +import { RefreshCw } from "lucide-react"; + +type Props = { + projectId: string; + onSynced: () => void | Promise; + disabled?: boolean; +}; + +async function ensureReadPermission(handle: FileSystemFileHandle): Promise { + try { + // @ts-expect-error - queryPermission/requestPermission exist in supporting browsers + const q = await handle.queryPermission?.({ mode: "read" }); + if (q === "granted") return true; + // @ts-expect-error - requestPermission exist in supporting browsers + const r = await handle.requestPermission?.({ mode: "read" }); + return r === "granted"; + } catch { + try { + // Fallback: try to read once; if it throws, we treat as no permission + const f = await handle.getFile(); + // Touch the file object so TS doesn't complain about unused variable + if (!f) return false; + return true; + } catch { + return false; + } + } +} + +export function SyncFromFilesButton({ projectId, onSynced, disabled }: Props) { + const [syncing, setSyncing] = useState(false); + const connSnap = useFileConnections(projectId); + + const hasConnections = Object.keys(connSnap.connections).length > 0; + + async function handleSync() { + if (!projectId) return; + if (!hasConnections) { + toast.info("没有已连接的语言"); + return; + } + + setSyncing(true); + + try { + const entries = Object.entries(connSnap.connections); + const results = await Promise.allSettled( + entries.map(async ([lang, conn]) => { + const canRead = await ensureReadPermission(conn.handle); + if (!canRead) throw new Error(`${lang}: 无读取权限`); + + const file = await conn.handle.getFile(); + const text = await file.text(); + + let json: unknown; + try { + json = JSON.parse(text); + } catch { + throw new Error(`${lang}: JSON 解析失败`); + } + + const values = flattenValues(json); + await upsertLanguageTranslations(projectId, lang, values); + return lang; + }) + ); + + const failed: string[] = []; + let success = 0; + + for (const r of results) { + if (r.status === "fulfilled") success += 1; + else failed.push((r.reason as Error)?.message || "未知语言"); + } + + if (failed.length === 0) { + toast.success(`同步完成(${success})`); + } else if (success === 0) { + toast.error(`全部失败(${failed.length}):${failed.join(",")}`); + } else { + toast.warning(`部分成功(成功 ${success},失败 ${failed.length}):${failed.join(",")}`); + } + } finally { + setSyncing(false); + await onSynced?.(); + } + } + + return ( + + ); +} diff --git a/src/pages/editor.tsx b/src/pages/editor.tsx index d9eaec6..8c1f8a7 100644 --- a/src/pages/editor.tsx +++ b/src/pages/editor.tsx @@ -41,6 +41,7 @@ import { Textarea } from "@/components/ui/textarea"; import { clearAllConnections, useFileConnections, writeLanguageToConnectedFile } from "@/store/file-connection"; import { generateLanguageJson } from "@/lib/utils"; import { HeaderConnectionIndicator } from "@/components/biz/header-connection-indicator"; +import { SyncFromFilesButton } from "@/components/biz/sync-from-files-button"; export default function Editor() { const { id: projectId } = useParams(); @@ -494,6 +495,12 @@ export default function Editor() { 导出 )} + {projectId && ( + + )} {projectId && (