diff --git a/src/App.tsx b/src/App.tsx index 1ddcd6c..5096276 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -53,7 +53,8 @@ function App() { // 锁定状态 const [isLocked, setIsLocked] = useState(false) - const [lockAvatar, setLockAvatar] = useState('') + const [lockAvatar, setLockAvatar] = useState(undefined) + const [lockUseHello, setLockUseHello] = useState(false) // 协议同意状态 const [showAgreement, setShowAgreement] = useState(false) @@ -260,8 +261,14 @@ function App() { if (isAgreementWindow || isOnboardingWindow || isVideoPlayerWindow) return const checkLock = async () => { - const enabled = await configService.getAuthEnabled() + // 并行获取配置,减少等待 + const [enabled, useHello] = await Promise.all([ + configService.getAuthEnabled(), + configService.getAuthUseHello() + ]) + if (enabled) { + setLockUseHello(useHello) setIsLocked(true) // 尝试获取头像 try { @@ -298,6 +305,7 @@ function App() { setIsLocked(false)} avatar={lockAvatar} + useHello={lockUseHello} /> )} diff --git a/src/components/LockScreen.scss b/src/components/LockScreen.scss index fd6685b..a2546a9 100644 --- a/src/components/LockScreen.scss +++ b/src/components/LockScreen.scss @@ -12,6 +12,24 @@ justify-content: center; user-select: none; -webkit-app-region: drag; + transition: all 0.5s cubic-bezier(0.22, 1, 0.36, 1); + backdrop-filter: blur(25px) saturate(180%); + background-color: var(--bg-primary); + // 让背景带一点透明度以增强毛玻璃效果 + opacity: 1; + + &.unlocked { + opacity: 0; + pointer-events: none; + backdrop-filter: blur(0) saturate(100%); + transform: scale(1.02); + + .lock-content { + transform: translateY(-20px) scale(0.95); + filter: blur(10px); + opacity: 0; + } + } .lock-content { display: flex; @@ -19,7 +37,8 @@ align-items: center; width: 320px; -webkit-app-region: no-drag; - animation: fadeIn 0.5s ease-out; + animation: fadeIn 0.5s cubic-bezier(0.4, 0, 0.2, 1); + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); .lock-avatar { width: 100px; diff --git a/src/components/LockScreen.tsx b/src/components/LockScreen.tsx index 1c3c086..88b74fb 100644 --- a/src/components/LockScreen.tsx +++ b/src/components/LockScreen.tsx @@ -6,6 +6,7 @@ import './LockScreen.scss' interface LockScreenProps { onUnlock: () => void avatar?: string + useHello?: boolean } async function sha256(message: string) { @@ -16,72 +17,132 @@ async function sha256(message: string) { return hashHex } -export default function LockScreen({ onUnlock, avatar }: LockScreenProps) { +export default function LockScreen({ onUnlock, avatar, useHello = false }: LockScreenProps) { const [password, setPassword] = useState('') const [error, setError] = useState('') const [isVerifying, setIsVerifying] = useState(false) + const [isUnlocked, setIsUnlocked] = useState(false) const [showHello, setShowHello] = useState(false) const [helloAvailable, setHelloAvailable] = useState(false) + // 用于取消 WebAuthn 请求 + const abortControllerRef = useRef(null) const inputRef = useRef(null) useEffect(() => { - checkHelloAvailability() - // Auto focus input + // 快速检查配置并启动 + quickStartHello() inputRef.current?.focus() + + return () => { + // 组件卸载时取消请求 + abortControllerRef.current?.abort() + } }, []) - const checkHelloAvailability = async () => { + const handleUnlock = () => { + setIsUnlocked(true) + setTimeout(() => { + onUnlock() + }, 1500) + } + + const quickStartHello = async () => { try { - const useHello = await configService.getAuthUseHello() - if (useHello && window.PublicKeyCredential) { - // Simple check if WebAuthn is supported - const available = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable() - setHelloAvailable(available) - if (available) { - setShowHello(true) - verifyHello() + // 如果父组件已经告诉我们要用 Hello,直接开始,不等待 IPC + let shouldUseHello = useHello + + // 为了稳健,如果 prop 没传(虽然现在都传了),再 check 一次 config + if (!shouldUseHello) { + shouldUseHello = await configService.getAuthUseHello() + } + + if (shouldUseHello) { + // 标记为可用,显示按钮 + setHelloAvailable(true) + setShowHello(true) + // 立即执行验证 (0延迟) + verifyHello() + + // 后台再次确认可用性,如果其实不可用,再隐藏? + // 或者信任用户的配置。为了速度,我们优先信任配置。 + if (window.PublicKeyCredential) { + PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable() + .then(available => { + if (!available) { + // 如果系统报告不支持,但配置开了,我们可能需要提示? + // 暂时保持开启状态,反正 verifyHello 会报错 + } + }) } } } catch (e) { - console.error('Failed to check Hello availability', e) + console.error('Quick start hello failed', e) } } const verifyHello = async () => { + if (isVerifying || isUnlocked) return + + // 取消之前的请求(如果有) + if (abortControllerRef.current) { + abortControllerRef.current.abort() + } + + const abortController = new AbortController() + abortControllerRef.current = abortController + setIsVerifying(true) setError('') try { - // Use WebAuthn for authentication - // We use a dummy challenge because we are just verifying local presence const challenge = new Uint8Array(32) window.crypto.getRandomValues(challenge) + const rpId = 'localhost' const credential = await navigator.credentials.get({ publicKey: { challenge, - rpId: window.location.hostname, // 'localhost' or empty for file:// + rpId, userVerification: 'required', - } + }, + signal: abortController.signal }) if (credential) { - onUnlock() + handleUnlock() } } catch (e: any) { - // NotAllowedError is common if user cancels - if (e.name !== 'NotAllowedError') { - console.error('Hello verification failed', e) + if (e.name === 'AbortError') { + console.log('Hello verification aborted') + return + } + if (e.name === 'NotAllowedError') { + console.log('User cancelled Hello verification') + } else { + console.error('Hello verification error:', e) + // 仅在非手动取消时显示错误 + if (e.name !== 'AbortError') { + setError(`验证失败: ${e.message || e.name}`) + } } } finally { - setIsVerifying(false) + if (!abortController.signal.aborted) { + setIsVerifying(false) + } } } const handlePasswordSubmit = async (e?: React.FormEvent) => { e?.preventDefault() - if (!password) return + if (!password || isUnlocked) return + // 如果正在进行 Hello 验证,取消它 + if (abortControllerRef.current) { + abortControllerRef.current.abort() + abortControllerRef.current = null + } + + // 不再检查 isVerifying,因为我们允许打断 Hello setIsVerifying(true) setError('') @@ -90,20 +151,22 @@ export default function LockScreen({ onUnlock, avatar }: LockScreenProps) { const inputHash = await sha256(password) if (inputHash === storedHash) { - onUnlock() + handleUnlock() } else { setError('密码错误') setPassword('') + setIsVerifying(false) + // 如果密码错误,是否重新触发 Hello? + // 用户可能想重试密码,暂时不自动触发 } } catch (e) { setError('验证失败') - } finally { setIsVerifying(false) } } return ( -
+
{avatar ? ( @@ -123,14 +186,14 @@ export default function LockScreen({ onUnlock, avatar }: LockScreenProps) { placeholder="输入应用密码" value={password} onChange={(e) => setPassword(e.target.value)} - disabled={isVerifying} + // 移除 disabled,允许用户随时输入 /> -
- {helloAvailable && ( + {showHello && ( @@ -834,7 +835,17 @@ function SettingsPage() { xwechat_files 目录 目录路径不可包含中文,如有中文请去微信-设置-存储位置点击更改,迁移至全英文目录 - setDbPath(e.target.value)} /> + setDbPath(e.target.value)} + onBlur={async () => { + if (dbPath) { + await configService.setDbPath(dbPath) + } + }} + />
@@ -860,13 +877,25 @@ function SettingsPage() {
用于解密图片缓存 - setImageXorKey(e.target.value)} /> + setImageXorKey(e.target.value)} + onBlur={syncCurrentKeys} + />
16 位密钥 - setImageAesKey(e.target.value)} /> + setImageAesKey(e.target.value)} + onBlur={syncCurrentKeys} + /> @@ -1219,7 +1248,15 @@ function SettingsPage() {
留空使用默认目录 - setCachePath(e.target.value)} /> + setCachePath(e.target.value)} + onBlur={async () => { + await configService.setCachePath(cachePath) + }} + />
@@ -1255,7 +1292,7 @@ function SettingsPage() { const credential = await navigator.credentials.create({ publicKey: { challenge, - rp: { name: 'WeFlow', id: window.location.hostname }, + rp: { name: 'WeFlow', id: 'localhost' }, user: { id: new Uint8Array([1]), name: 'user', displayName: 'User' }, pubKeyCredParams: [{ alg: -7, type: 'public-key' }], authenticatorSelection: { userVerification: 'required' }, @@ -1307,7 +1344,15 @@ function SettingsPage() { 每次启动应用时需要验证密码
@@ -1459,9 +1504,6 @@ function SettingsPage() { -
diff --git a/src/pages/WelcomePage.tsx b/src/pages/WelcomePage.tsx index 16d7cce..4955ef9 100644 --- a/src/pages/WelcomePage.tsx +++ b/src/pages/WelcomePage.tsx @@ -80,7 +80,7 @@ function WelcomePage({ standalone = false }: WelcomePageProps) { const credential = await navigator.credentials.create({ publicKey: { challenge, - rp: { name: 'WeFlow', id: window.location.hostname }, + rp: { name: 'WeFlow', id: 'localhost' }, user: { id: new Uint8Array([1]), name: 'user',