From 7487be2bb5a07111ebd5fb6994518ec976a2d8b9 Mon Sep 17 00:00:00 2001 From: huty Date: Sat, 22 Nov 2025 16:38:36 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=83=A8=E5=88=86=E5=B7=B2?= =?UTF-8?q?=E7=9F=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App.tsx | 71 ++++++++++++++--- index.html | 84 ++++++++++++++++++++ index.tsx | 22 ++++- releases/HTY1024-APP-SKR-0.4.0_20251122.zip | Bin 0 -> 85516 bytes utils/localization.ts | 12 ++- views/OCRView.tsx | 55 ++++++++----- views/ReadingView.tsx | 4 +- views/SpeakingPracticeView.tsx | 11 ++- 8 files changed, 216 insertions(+), 43 deletions(-) create mode 100644 releases/HTY1024-APP-SKR-0.4.0_20251122.zip diff --git a/App.tsx b/App.tsx index fc5ca31..cbf1b92 100644 --- a/App.tsx +++ b/App.tsx @@ -32,8 +32,10 @@ const App: React.FC = () => { // Default to 'zh' (Chinese) const [language, setLanguage] = useState(() => (localStorage.getItem(STORAGE_KEYS.LANGUAGE) as Language) || 'zh'); const [chatSessions, setChatSessions] = useState(() => { - const stored = localStorage.getItem(STORAGE_KEYS.CHAT_SESSIONS); - if (stored) return JSON.parse(stored); + try { + const stored = localStorage.getItem(STORAGE_KEYS.CHAT_SESSIONS); + if (stored) return JSON.parse(stored); + } catch (e) { console.error("Failed to load chat sessions", e); } return []; }); const [activeSessionId, setActiveSessionId] = useState(() => localStorage.getItem(STORAGE_KEYS.ACTIVE_SESSION) || ''); @@ -65,14 +67,63 @@ const App: React.FC = () => { const t = translations[language]; - useEffect(() => { localStorage.setItem(STORAGE_KEYS.CHAT_SESSIONS, JSON.stringify(chatSessions)); }, [chatSessions]); - useEffect(() => { localStorage.setItem(STORAGE_KEYS.ACTIVE_SESSION, activeSessionId); }, [activeSessionId]); - useEffect(() => { localStorage.setItem(STORAGE_KEYS.TRANSLATION_HISTORY, JSON.stringify(translationHistory)); }, [translationHistory]); - useEffect(() => { localStorage.setItem(STORAGE_KEYS.READING_HISTORY, JSON.stringify(readingHistory)); }, [readingHistory]); - useEffect(() => { localStorage.setItem(STORAGE_KEYS.LISTENING_HISTORY, JSON.stringify(listeningHistory)); }, [listeningHistory]); - useEffect(() => { localStorage.setItem(STORAGE_KEYS.OCR_HISTORY, JSON.stringify(ocrHistory)); }, [ocrHistory]); - useEffect(() => { localStorage.setItem(STORAGE_KEYS.LANGUAGE, language); }, [language]); - useEffect(() => { localStorage.setItem(STORAGE_KEYS.SELECTED_MODEL, selectedModel); }, [selectedModel]); + // Safe Storage Helper to prevent white screen on QuotaExceededError + const saveToStorage = (key: string, data: any) => { + try { + const json = JSON.stringify(data); + localStorage.setItem(key, json); + } catch (e: any) { + // Check for QuotaExceededError + if (e.name === 'QuotaExceededError' || e.message?.includes('exceeded the quota')) { + console.warn(`Storage quota exceeded for key: ${key}`); + addToast('error', translations[language].common.storageFull); + + // Attempt to recover by stripping heavy data (e.g., audioUrl from chats, base64 images from OCR) + if (key === STORAGE_KEYS.CHAT_SESSIONS && Array.isArray(data)) { + try { + // Create a lightweight version by removing audioUrl base64 strings + const leanData = (data as ChatSession[]).map(s => ({ + ...s, + messages: s.messages.map(m => ({ + ...m, + metadata: { + ...m.metadata, + audioUrl: undefined // Remove cached audio to save space + } + })) + })); + localStorage.setItem(key, JSON.stringify(leanData)); + addToast('info', translations[language].common.storageOptimized); + } catch (retryError) { + console.error("Failed to save even lean chat data", retryError); + } + } else if (key === STORAGE_KEYS.OCR_HISTORY && Array.isArray(data)) { + try { + // Create lightweight version by removing imagePreview + const leanData = (data as OCRRecord[]).map(r => ({ + ...r, + imagePreview: '' // Remove heavy base64 image + })); + localStorage.setItem(key, JSON.stringify(leanData)); + addToast('info', translations[language].common.storageOptimized); + } catch (retryError) { + console.error("Failed to save even lean OCR data", retryError); + } + } + } else { + console.error("LocalStorage save failed", e); + } + } + }; + + useEffect(() => { saveToStorage(STORAGE_KEYS.CHAT_SESSIONS, chatSessions); }, [chatSessions]); + useEffect(() => { saveToStorage(STORAGE_KEYS.ACTIVE_SESSION, activeSessionId); }, [activeSessionId]); + useEffect(() => { saveToStorage(STORAGE_KEYS.TRANSLATION_HISTORY, translationHistory); }, [translationHistory]); + useEffect(() => { saveToStorage(STORAGE_KEYS.READING_HISTORY, readingHistory); }, [readingHistory]); + useEffect(() => { saveToStorage(STORAGE_KEYS.LISTENING_HISTORY, listeningHistory); }, [listeningHistory]); + useEffect(() => { saveToStorage(STORAGE_KEYS.OCR_HISTORY, ocrHistory); }, [ocrHistory]); + useEffect(() => { saveToStorage(STORAGE_KEYS.LANGUAGE, language); }, [language]); + useEffect(() => { saveToStorage(STORAGE_KEYS.SELECTED_MODEL, selectedModel); }, [selectedModel]); useEffect(() => { const activeSession = chatSessions.find(s => s.id === activeSessionId); diff --git a/index.html b/index.html index 0d50d57..301f479 100644 --- a/index.html +++ b/index.html @@ -28,6 +28,77 @@ background: #94a3b8; } + /* Loading Animation Styles */ + #app-loader { + position: fixed; + inset: 0; + z-index: 9999; + background-color: #f8fafc; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + transition: opacity 0.5s ease-out; + } + + .sakura-spinner { + position: relative; + width: 80px; + height: 80px; + animation: spin-slow 6s linear infinite; + } + + .petal { + position: absolute; + top: 50%; + left: 50%; + width: 30px; + height: 30px; + background: radial-gradient(circle at 70% 70%, #f43f5e, #fb7185); + border-radius: 0 100% 0 100%; + opacity: 0.9; + transform-origin: 0 0; + } + + .petal:nth-child(1) { transform: rotate(0deg) translate(10px, 10px); } + .petal:nth-child(2) { transform: rotate(72deg) translate(10px, 10px); } + .petal:nth-child(3) { transform: rotate(144deg) translate(10px, 10px); } + .petal:nth-child(4) { transform: rotate(216deg) translate(10px, 10px); } + .petal:nth-child(5) { transform: rotate(288deg) translate(10px, 10px); } + + .center-dot { + position: absolute; + top: 50%; + left: 50%; + width: 12px; + height: 12px; + background-color: #fef3c7; + border-radius: 50%; + transform: translate(-50%, -50%); + z-index: 10; + box-shadow: 0 0 10px rgba(251, 113, 133, 0.5); + } + + .loader-text { + margin-top: 2rem; + font-family: 'Nunito', sans-serif; + font-weight: 800; + font-size: 1.5rem; + color: #e11d48; + letter-spacing: -0.025em; + animation: pulse 2s infinite; + } + + @keyframes spin-slow { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } + + @keyframes pulse { + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.7; transform: scale(0.98); } + } + /* Animations */ @keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } @@ -70,6 +141,19 @@ + +
+
+
+
+
+
+
+
+
+
Sakura Sensei
+
+