import { useState, useEffect, useRef } from 'react'; import { createPortal } from 'react-dom'; import { Plus, Database, Globe, FileClock, Loader2, CheckCircle2, XCircle, Copy, Check, Info, Link2 } from 'lucide-react'; import { useAccountStore } from '../../stores/useAccountStore'; import { useTranslation } from 'react-i18next'; import { listen } from '@tauri-apps/api/event'; import { open } from '@tauri-apps/plugin-dialog'; import { request as invoke } from '../../utils/request'; import { isTauri } from '../../utils/env'; import { copyToClipboard } from '../../utils/clipboard'; interface AddAccountDialogProps { onAdd: (email: string, refreshToken: string) => Promise; showText?: boolean; } type Status = 'idle' | 'loading' | 'success' | 'error'; function AddAccountDialog({ onAdd, showText = true }: AddAccountDialogProps) { const { t } = useTranslation(); const fetchAccounts = useAccountStore(state => state.fetchAccounts); const [isOpen, setIsOpen] = useState(false); const [activeTab, setActiveTab] = useState<'oauth' | 'token' | 'import'>(isTauri() ? 'oauth' : 'token'); const [refreshToken, setRefreshToken] = useState(''); const [oauthUrl, setOauthUrl] = useState(''); const [oauthUrlCopied, setOauthUrlCopied] = useState(false); const [manualCode, setManualCode] = useState(''); // UI State const [status, setStatus] = useState('idle'); const [message, setMessage] = useState(''); const { startOAuthLogin, completeOAuthLogin, cancelOAuthLogin, importFromDb, importV1Accounts, importFromCustomDb } = useAccountStore(); const oauthUrlRef = useRef(oauthUrl); const statusRef = useRef(status); const activeTabRef = useRef(activeTab); const isOpenRef = useRef(isOpen); useEffect(() => { oauthUrlRef.current = oauthUrl; statusRef.current = status; activeTabRef.current = activeTab; isOpenRef.current = isOpen; }, [oauthUrl, status, activeTab, isOpen]); // Reset state when dialog opens or tab changes useEffect(() => { if (isOpen) { resetState(); } }, [isOpen, activeTab]); // Listen for OAuth URL useEffect(() => { if (!isTauri()) return; let unlisten: (() => void) | undefined; const setupListener = async () => { unlisten = await listen('oauth-url-generated', (event) => { setOauthUrl(event.payload as string); // 自动复制到剪贴板? 可选,这里只设置状态让用户手动复制 }); }; setupListener(); return () => { if (unlisten) unlisten(); }; }, []); // Listen for OAuth callback completion (user may open the URL manually without clicking Start) useEffect(() => { if (!isTauri()) return; let unlisten: (() => void) | undefined; const setupListener = async () => { unlisten = await listen('oauth-callback-received', async () => { if (!isOpenRef.current) return; if (activeTabRef.current !== 'oauth') return; if (statusRef.current === 'loading' || statusRef.current === 'success') return; if (!oauthUrlRef.current) return; // Auto-complete: exchange code and save account (no browser open) setStatus('loading'); setMessage(`${t('accounts.add.tabs.oauth')}...`); try { await completeOAuthLogin(); setStatus('success'); setMessage(`${t('accounts.add.tabs.oauth')} ${t('common.success')}!`); setTimeout(() => { setIsOpen(false); resetState(); }, 1500); } catch (error) { setStatus('error'); let errorMsg = String(error); if (errorMsg.includes('Refresh Token') || errorMsg.includes('refresh_token')) { setMessage(errorMsg); } else if (errorMsg.includes('Tauri') || errorMsg.toLowerCase().includes('environment') || errorMsg.includes('环境')) { setMessage(t('common.environment_error', { error: errorMsg })); } else { setMessage(`${t('accounts.add.tabs.oauth')} ${t('common.error')}: ${errorMsg}`); } } }); }; setupListener(); return () => { if (unlisten) unlisten(); }; }, [completeOAuthLogin, t]); // Pre-generate OAuth URL when dialog opens on OAuth tab (so URL is shown BEFORE "Start OAuth") useEffect(() => { if (!isOpen) return; if (activeTab !== 'oauth') return; if (oauthUrl) return; invoke('prepare_oauth_url') .then((res) => { const url = typeof res === 'string' ? res : res?.url; if (url && url.length > 0) setOauthUrl(url); }) .catch((e) => { console.error('Failed to prepare OAuth URL:', e); }); }, [isOpen, activeTab, oauthUrl]); // If user navigates away from OAuth tab, cancel prepared flow to release the port. useEffect(() => { if (!isOpen) return; if (activeTab === 'oauth') return; if (!oauthUrl) return; cancelOAuthLogin().catch(() => { }); setOauthUrl(''); setOauthUrlCopied(false); }, [isOpen, activeTab]); const resetState = () => { setStatus('idle'); setMessage(''); setRefreshToken(''); setOauthUrl(''); setOauthUrlCopied(false); }; const handleAction = async ( actionName: string, actionFn: () => Promise, options?: { clearOauthUrl?: boolean } ) => { setStatus('loading'); setMessage(`${actionName}...`); if (options?.clearOauthUrl !== false) { setOauthUrl(''); // Clear previous URL } try { await actionFn(); setStatus('success'); setMessage(`${actionName} ${t('common.success')}!`); // 延迟关闭,让用户看到成功状态 setTimeout(() => { setIsOpen(false); resetState(); }, 1500); } catch (error) { setStatus('error'); // 改进错误信息显示 let errorMsg = String(error); // 如果是 refresh_token 缺失错误,显示完整信息(包含解决方案) if (errorMsg.includes('Refresh Token') || errorMsg.includes('refresh_token')) { setMessage(errorMsg); } else if (errorMsg.includes('Tauri') || errorMsg.toLowerCase().includes('environment') || errorMsg.includes('环境')) { // 环境错误 setMessage(t('common.environment_error', { error: errorMsg })); } else { // 其他错误 setMessage(`${actionName} ${t('common.error')}: ${errorMsg}`); } } }; const handleSubmit = async () => { if (!refreshToken) { setStatus('error'); setMessage(t('accounts.add.token.error_token')); return; } setStatus('loading'); // 1. 尝试解析输入 let tokens: string[] = []; const input = refreshToken.trim(); try { // 尝试解析为 JSON if (input.startsWith('[') && input.endsWith(']')) { const parsed = JSON.parse(input); if (Array.isArray(parsed)) { tokens = parsed .map((item: any) => item.refresh_token) .filter((t: any) => typeof t === 'string' && t.startsWith('1//')); } } } catch (e) { // JSON 解析失败,忽略 console.debug('JSON parse failed, falling back to regex', e); } // 2. 如果 JSON 解析没有结果,尝试正则提取 (或者输入不是 JSON) if (tokens.length === 0) { const regex = /1\/\/[a-zA-Z0-9_\-]+/g; const matches = input.match(regex); if (matches) { tokens = matches; } } // 去重 tokens = [...new Set(tokens)]; if (tokens.length === 0) { setStatus('error'); setMessage(t('accounts.add.token.error_token')); // 或者提示"未找到有效 Token" return; } // 3. 批量添加 let successCount = 0; let failCount = 0; for (let i = 0; i < tokens.length; i++) { const currentToken = tokens[i]; setMessage(t('accounts.add.token.batch_progress', { current: i + 1, total: tokens.length })); try { await onAdd("", currentToken); successCount++; } catch (error) { console.error(`Failed to add token ${i + 1}:`, error); failCount++; } // 稍微延迟一下,避免太快 await new Promise(r => setTimeout(r, 100)); } // 4. 结果反馈 if (successCount === tokens.length) { setStatus('success'); setMessage(t('accounts.add.token.batch_success', { count: successCount })); setTimeout(() => { setIsOpen(false); resetState(); }, 1500); } else if (successCount > 0) { // 部分成功 setStatus('success'); // 还是用绿色,但提示部分失败 setMessage(t('accounts.add.token.batch_partial', { success: successCount, fail: failCount })); // 不自动关闭,让用户看到结果 } else { // 全部失败 setStatus('error'); setMessage(t('accounts.add.token.batch_fail')); } }; const handleOAuthWeb = async () => { try { setStatus('loading'); setMessage(t('accounts.add.oauth.btn_start') + '...'); // 1. 获取 URL (指向 /auth/callback) const res = await invoke('prepare_oauth_url'); const url = typeof res === 'string' ? res : res.url; if (!url) { throw new Error(t('accounts.add.oauth.error_no_url', 'OAuth URLを取得できませんでした')); } setOauthUrl(url); // 确保链接在 UI 中可见,方便用户手动复制 // 2. 打开新标签页 (响应用户反馈:Web 端直接使用新标签体验更好) const popup = window.open(url, '_blank'); if (!popup) { setStatus('error'); setMessage(t('accounts.add.oauth.popup_blocked', 'ポップアップがブロックされました')); return; } // 3. 监听消息 const handleMessage = async (event: MessageEvent) => { // 安全检查: 如果定义了 ORIGIN 校验更好,这里暂时检查 data type if (event.data?.type === 'oauth-success') { popup.close(); window.removeEventListener('message', handleMessage); // 4. 成功后刷新列表 await fetchAccounts(); setStatus('success'); setMessage(t('accounts.add.oauth_success') || t('common.success')); setTimeout(() => { setIsOpen(false); resetState(); }, 1500); } }; window.addEventListener('message', handleMessage); // 5. 检测窗口关闭 (用户手动关闭) const timer = setInterval(() => { if (popup.closed) { clearInterval(timer); window.removeEventListener('message', handleMessage); if (statusRef.current === 'loading') { // 如果还在 loading 状态就关闭了,说明取消了 setStatus('idle'); setMessage(''); } } }, 1000); } catch (error) { console.error('OAuth Web Error:', error); setStatus('error'); setMessage(`${t('common.error')}: ${error}`); } }; const handleOAuth = () => { if (!isTauri()) { handleOAuthWeb(); return; } // Default flow: opens the default browser and completes automatically. // (If user opened the URL manually, completion is also triggered by oauth-callback-received.) handleAction(t('accounts.add.tabs.oauth'), startOAuthLogin, { clearOauthUrl: false }); }; const handleCompleteOAuth = () => { // Manual flow: user already authorized in their preferred browser, just finish the flow. handleAction(t('accounts.add.tabs.oauth'), completeOAuthLogin, { clearOauthUrl: false }); }; const handleCopyUrl = async () => { if (oauthUrl) { const success = await copyToClipboard(oauthUrl); if (success) { setOauthUrlCopied(true); window.setTimeout(() => setOauthUrlCopied(false), 1500); } } }; const handleManualSubmit = async () => { if (!manualCode.trim()) return; setStatus('loading'); setMessage(t('accounts.add.oauth.manual_submitting', '認可コードを送信中...')); try { await invoke('submit_oauth_code', { code: manualCode.trim(), state: null }); // 提交成功反馈 setStatus('success'); setMessage(t('accounts.add.oauth.manual_submitted', '認可コードを送信しました。バックエンドで処理中です...')); setManualCode(''); // 对齐 Web 模式下的刷新逻辑 if (!isTauri()) { setTimeout(async () => { await fetchAccounts(); setIsOpen(false); resetState(); }, 2000); } } catch (error) { let errStr = String(error); if (errStr.includes("No active OAuth flow")) { setMessage(t('accounts.add.oauth.error_no_flow')); setStatus('error'); } else { setMessage(`${t('common.error')}: ${errStr}`); setStatus('error'); } } }; const handleImportDb = () => { handleAction(t('accounts.add.tabs.import'), importFromDb); }; const handleImportV1 = () => { handleAction(t('accounts.add.import.btn_v1'), importV1Accounts); }; const handleImportCustomDb = async () => { try { if (!isTauri()) { alert(t('common.tauri_api_not_loaded') || 'Storage import only works in desktop app.'); return; } const selected = await open({ multiple: false, filters: [{ name: 'VSCode DB', extensions: ['vscdb'] }, { name: 'All Files', extensions: ['*'] }] }); if (selected && typeof selected === 'string') { handleAction(t('accounts.add.import.btn_custom_db') || 'Import Custom DB', () => importFromCustomDb(selected)); } } catch (err) { console.error('Failed to open dialog:', err); } }; // 状态提示组件 const StatusAlert = () => { if (status === 'idle' || !message) return null; const styles = { loading: 'alert-info', success: 'alert-success', error: 'alert-error' }; const icons = { loading: , success: , error: }; return (
{icons[status]} {message}
); }; return ( <> {isOpen && createPortal(
{/* Draggable Top Region */}
{/* Click outside to close */}
setIsOpen(false)} />

{t('accounts.add.title')}

{/* Tab 导航 - 胶囊风格 */}
{/* 添加 Web 模式提示 */} {!isTauri() && (
{t('accounts.add.oauth.web_hint', '将在新窗口中打开 Google 登录页')}
)} {/* 状态提示区 */}
{/* OAuth 授权 */} {activeTab === 'oauth' && (

{t('accounts.add.oauth.recommend')}

{t('accounts.add.oauth.desc')}

{oauthUrl && (
{t('accounts.add.oauth.link_label')}
)} {/* Manual Code Entry - Always enabled to rescue stuck flows */}
{t('accounts.add.oauth.manual_hint')}
setManualCode(e.target.value)} />
)} {/* Refresh Token */} {activeTab === 'token' && (
{t('accounts.add.token.label')}