diff --git a/app/page.tsx b/app/page.tsx index 254129e..fe8145d 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -617,13 +617,20 @@ export default function TwoFactorAuth() { const imported = JSON.parse(decrypted) if (Array.isArray(imported)) { - setTokens([...tokens, ...imported]) + const existingSecrets = new Set(tokens.map((tk) => tk.secret.toUpperCase())) + const newTokens = (imported as TOTPToken[]).filter( + (tk) => !existingSecrets.has(tk.secret.toUpperCase()) + ) + const skipped = imported.length - newTokens.length + setTokens((prev) => [...prev, ...newTokens]) setImportPassword("") setImportFile(null) setShowImportPassword(false) toast({ title: t.importSuccess, - description: `${t.added} ${imported.length} ${t.importedTokens}`, + description: skipped > 0 + ? `${t.added} ${newTokens.length} ${t.importedTokens},已跳过 ${skipped} 个重复令牌` + : `${t.added} ${newTokens.length} ${t.importedTokens}`, }) } } catch { @@ -1189,14 +1196,14 @@ export default function TwoFactorAuth() { - Set Export Password + {t.setExportPassword}
setExportPassword(e.target.value)} /> @@ -1229,11 +1236,11 @@ export default function TwoFactorAuth() { }}> - Import Backup + {t.importBackup}
- + Password setImportPassword(e.target.value)} /> diff --git a/components/theme-provider.tsx b/components/theme-provider.tsx index 20feb83..df0d5bf 100644 --- a/components/theme-provider.tsx +++ b/components/theme-provider.tsx @@ -1,12 +1,101 @@ "use client" -import type * as React from "react" -import { ThemeProvider as NextThemesProvider } from "next-themes" +import * as React from "react" +import { createContext, useContext, useEffect, useState } from "react" -export { useTheme } from "next-themes" +type Theme = "dark" | "light" | "system" -type ThemeProviderProps = React.ComponentProps - -export function ThemeProvider({ children, ...props }: ThemeProviderProps) { - return {children} +type ThemeProviderProps = { + children: React.ReactNode + defaultTheme?: Theme + storageKey?: string + attribute?: string + enableSystem?: boolean + disableTransitionOnChange?: boolean +} + +type ThemeProviderState = { + theme: Theme + setTheme: (theme: Theme) => void + resolvedTheme?: string +} + +const ThemeProviderContext = createContext(undefined) + +export function ThemeProvider({ + children, + defaultTheme = "system", + storageKey = "theme", + ...props +}: ThemeProviderProps) { + const [theme, setTheme] = useState(defaultTheme) + const [resolvedTheme, setResolvedTheme] = useState(undefined) + const [mounted, setMounted] = useState(false) + + useEffect(() => { + setMounted(true) + const stored = localStorage.getItem(storageKey) as Theme | null + if (stored) { + setTheme(stored) + } + }, [storageKey]) + + useEffect(() => { + if (!mounted) return + + const root = window.document.documentElement + + root.classList.remove("light", "dark") + + let resolved: string + if (theme === "system") { + resolved = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" + } else { + resolved = theme + } + + root.classList.add(resolved) + setResolvedTheme(resolved) + }, [theme, mounted]) + + useEffect(() => { + if (!mounted || theme !== "system") return + + const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)") + const handleChange = () => { + const resolved = mediaQuery.matches ? "dark" : "light" + const root = window.document.documentElement + root.classList.remove("light", "dark") + root.classList.add(resolved) + setResolvedTheme(resolved) + } + + mediaQuery.addEventListener("change", handleChange) + return () => mediaQuery.removeEventListener("change", handleChange) + }, [theme, mounted]) + + const value = { + theme, + setTheme: (newTheme: Theme) => { + localStorage.setItem(storageKey, newTheme) + setTheme(newTheme) + }, + resolvedTheme, + } + + return ( + + {children} + + ) +} + +export function useTheme() { + const context = useContext(ThemeProviderContext) + + if (context === undefined) { + throw new Error("useTheme must be used within a ThemeProvider") + } + + return context } diff --git a/lib/i18n.tsx b/lib/i18n.tsx index f7b4de6..75ad9d8 100644 --- a/lib/i18n.tsx +++ b/lib/i18n.tsx @@ -116,6 +116,10 @@ const translations = { imageLoadFailed: "图片加载失败", duplicateToken: "令牌已存在", duplicateTokenDesc: "该密钥的令牌已添加过", + setExportPassword: "设置导出密码", + passwordPlaceholder: "输入密码以保护您的备份", + selectFile: "选择文件", + passwordInput: "输入备份密码", }, en: { // Header @@ -227,6 +231,10 @@ const translations = { imageLoadFailed: "Failed to load image", duplicateToken: "Token already exists", duplicateTokenDesc: "A token with this secret key has already been added", + setExportPassword: "Set Export Password", + passwordPlaceholder: "Enter a password to protect your backup", + selectFile: "Select File", + passwordInput: "Enter backup password", }, }