Lashtw commited on
Commit
1713756
·
verified ·
1 Parent(s): 2fa8505

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +352 -102
index.html CHANGED
@@ -3,7 +3,7 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>單字閃卡</title>
7
  <!-- 載入 Tailwind CSS CDN -->
8
  <script src="https://cdn.tailwindcss.com"></script>
9
  <!-- 載入彩帶效果庫 -->
@@ -14,8 +14,6 @@
14
  <script src="https://cdnjs.cloudflare.com/ajax/libs/pako/2.1.0/pako.min.js"></script>
15
  <!-- 載入 PDF.js 函式庫 -->
16
  <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.min.js"></script>
17
- <!-- [第一步] 引入手寫辨識函式庫 -->
18
- <script src="https://cdn.jsdelivr.net/gh/ChenYuHo/handwriting.js/handwriting.js"></script>
19
 
20
  <!-- 【新】Firebase SDK -->
21
  <script type="module">
@@ -127,16 +125,6 @@
127
  #handwriting-container {
128
  transition: all 0.3s ease;
129
  }
130
- #handwriting-canvas {
131
- width: 100%;
132
- max-width: 100%; /* 確保不超出容器 */
133
- height: 250px; /* 手寫區域高度 */
134
- background-color: #fff;
135
- border: 2px dashed #cbd5e1; /* 虛線邊框更有「畫布」的感覺 */
136
- border-radius: 0.75rem;
137
- cursor: crosshair;
138
- touch-action: none; /* [重要] 防止手指寫字時觸發網頁捲動 */
139
- }
140
  /* 讓答案框在唯讀模式下看起來像「顯示區」 */
141
  input[readonly].handwriting-mode {
142
  background-color: #f3f4f6;
@@ -145,10 +133,68 @@
145
  border-color: #818cf8;
146
  cursor: not-allowed;
147
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  </style>
149
  </head>
150
  <body class="bg-gray-50 min-h-screen flex flex-col items-center p-4 sm:p-8 font-sans">
151
 
 
 
 
152
  <!-- 主選單:新增單字 & 模式選擇 -->
153
  <div id="main-menu" class="w-full max-w-2xl bg-white p-6 rounded-3xl shadow-2xl transition-all duration-500">
154
  <h1 id="main-title" class="text-4xl font-extrabold text-center text-gray-900 mb-6"></h1>
@@ -369,11 +415,14 @@
369
  <input id="answer-input" placeholder="輸入你的答案 (按 Enter 提交)" class="w-full p-4 border-2 border-gray-300 rounded-xl text-2xl focus:border-indigo-500 focus:ring-indigo-500 transition-colors" autocomplete="off">
370
 
371
  <div id="handwriting-container" class="hidden flex-col gap-2">
372
- <div class="relative w-full">
373
- <canvas id="handwriting-canvas" width="500" height="250"></canvas>
374
- <p class="absolute bottom-2 right-3 text-xs text-gray-400 pointer-events-none">請在此區域手寫</p>
375
  </div>
376
 
 
 
 
377
  <div class="flex gap-2 w-full">
378
  <button id="hw-clear-btn" class="flex-1 py-3 bg-gray-200 text-gray-700 rounded-xl font-semibold hover:bg-gray-300 transition-colors flex items-center justify-center gap-1">
379
  <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm4 0a1 1 0 012 0v6a1 1 0 11-2 0V8z" clip-rule="evenodd" /></svg>
@@ -613,7 +662,12 @@
613
  <script>
614
  // [第四步] JavaScript 邏輯 (Script)
615
  let effectiveVersion = 'pc'; // 預設為 PC 版
616
- let handwritingCanvasObj = null; // [新增] 存放手寫板實例
 
 
 
 
 
617
 
618
  document.addEventListener('DOMContentLoaded', () => {
619
  // DOM 元素
@@ -701,6 +755,12 @@
701
  // 為了配合新版介面,建議將提示顯示區域也移動到新區塊內,但為了最小修改,我們這裡用 JS 控制
702
  const quizCompletionMessage = document.getElementById('quiz-completion-message');
703
 
 
 
 
 
 
 
704
  // 複習模式導覽
705
  const reviewNavContainer = document.getElementById('review-nav-container');
706
  const prevBtn = document.getElementById('prev-btn');
@@ -796,109 +856,302 @@
796
  'sentence-cloze': { title: '情境克漏字' },
797
  };
798
 
799
- // [新增] 初始化手寫板功能 (修復無反應問題)
 
 
 
 
 
 
 
 
 
 
800
  function initHandwritingBoard() {
801
- // 建立 handwriting.js 實例 (參數: canvas元素, 筆畫粗細)
802
- const canvasEl = document.getElementById('handwriting-canvas');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
803
 
804
- // 確保 canvas 寬度正確 (解決有些手機上寬度不對的問題)
805
- const rect = canvasEl.parentElement.getBoundingClientRect();
 
806
 
807
- // [關鍵修復]:如果寬度是 0 (表示元素隱藏中),則不進行初始化,避免狀態錯誤
808
- if (rect.width === 0) return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
809
 
810
- // 設定 canvas 屬性寬高 (這是繪圖解析度)
811
- canvasEl.width = rect.width || 300;
812
- canvasEl.height = 250;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
813
 
814
- // [重要修復] 強制註冊 passive: false 的事件監聽器,防止畫面捲動
815
- const preventDefaultTouch = (e) => {
816
- if (e.cancelable) e.preventDefault();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
817
  };
818
- canvasEl.addEventListener('touchstart', preventDefaultTouch, { passive: false });
819
- canvasEl.addEventListener('touchmove', preventDefaultTouch, { passive: false });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
820
 
821
- // 如果物件已經存在,不需要重新 new,但需要確保屬性設定正確
822
- if (!handwritingCanvasObj) {
823
- handwritingCanvasObj = new handwriting.Canvas(canvasEl, 3);
824
-
825
- // 設定辨識後的回呼函式
826
- handwritingCanvasObj.setCallBack(function(data, err) {
827
- if (err) {
828
- console.error(err);
829
- alert("辨識失敗,請檢查網路連線"); // handwriting.js 需要連網
830
- return;
831
  }
832
- // data 是候選字陣列,取第一個
833
- if (data && data.length > 0) {
834
- const result = data[0];
835
- answerInput.value = result; // 填入答案框
836
-
837
- // 視覺回饋
838
- answerInput.classList.add('border-green-500', 'bg-green-50');
839
- setTimeout(() => answerInput.classList.remove('border-green-500', 'bg-green-50'), 500);
840
- }
841
- });
842
 
843
- // 綁定按鈕事件 (只需綁定一次)
844
- document.getElementById('hw-clear-btn').addEventListener('click', () => {
845
- handwritingCanvasObj.erase();
846
- answerInput.value = '';
847
- answerInput.focus(); // 保持焦點
848
- });
849
-
850
- document.getElementById('hw-undo-btn').addEventListener('click', () => {
851
- handwritingCanvasObj.undo();
852
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
853
 
854
- document.getElementById('hw-recognize-btn').addEventListener('click', () => {
855
- handwritingCanvasObj.recognize();
 
 
 
 
856
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
857
  }
 
 
 
 
 
 
858
 
859
- // [關鍵] 每次 resize 或是重啟時,都要重新設定筆畫樣式,以免因為 canvas 重設寬高而遺失
860
- handwritingCanvasObj.setLineWidth(5);
861
- handwritingCanvasObj.setPenColor("#333");
862
- handwritingCanvasObj.setOptions({
863
- language: "en",
864
- numOfReturn: 1
 
 
 
865
  });
866
  }
867
 
 
 
 
 
 
 
868
  // [新增] 更新輸入介面 (控制手寫板顯示與輸入法)
869
- // expectedLang: 'en' (預期輸入英文) 或 'zh' (預期輸入中文)
870
  function updateInputInterface(expectedLang) {
871
  const hwContainer = document.getElementById('handwriting-container');
872
 
873
- // 只有在行動裝置 預期輸入英文時,才開啟手寫板
874
- const shouldUseHandwriting = (effectiveVersion === 'mobile' && expectedLang === 'en');
 
875
 
876
  if (shouldUseHandwriting) {
877
- // --- 行動版 + 輸入英文 (開啟手寫) ---
878
- // 1. 顯示手寫板容器
879
  hwContainer.classList.remove('hidden');
880
  hwContainer.classList.add('flex');
881
 
882
- // 2. 初始化手寫板 (使用 setTimeout 確保 flex 佈局生效後有正確寬度)
883
- setTimeout(() => initHandwritingBoard(), 50);
 
884
 
885
- // 3. 鎖定輸入框 (變成顯示結果用)
886
  answerInput.readOnly = true;
887
  answerInput.placeholder = "請在下方手寫英文...";
888
  answerInput.classList.add('handwriting-mode');
889
 
890
- // 4. 嘗試移除原生輸入法干擾
891
- if (window.google && window.google.ime) {
892
- window.google.ime.setOptions({ ime: 'none', trigger: null });
893
  }
894
 
895
  } else {
896
- // --- PC 行動版輸入中文 (關閉手寫,用鍵盤) ---
897
- // 1. 隱藏手寫板
898
  hwContainer.classList.add('hidden');
899
  hwContainer.classList.remove('flex');
900
 
901
- // 2. 恢復輸入框
902
  answerInput.readOnly = false;
903
  answerInput.placeholder = (expectedLang === 'zh') ? "輸入中文答案 (按 Enter 提交)" : "輸入英文答案 (按 Enter 提交)";
904
  answerInput.classList.remove('handwriting-mode');
@@ -1213,7 +1466,7 @@
1213
  if (mode === 'sentence-cloze') {
1214
  wordsForCurrentMode = wordPool.filter(w => w.sentence && w.sentence.en && w.sentence.zh && w.sentence.answer);
1215
  if (wordsForCurrentMode.length === 0) {
1216
- alert("題庫中沒有附帶完整例句(包含克漏字答案)的單字可供此模式使用。");
1217
  return;
1218
  }
1219
  } else if (mode === 'review') {
@@ -1223,7 +1476,7 @@
1223
  if (!isNaN(randomCount) && randomCount > 0 && randomCount <= wordPool.length) {
1224
  wordsForCurrentMode = shuffleArray([...wordPool]).slice(0, randomCount);
1225
  } else {
1226
- alert('請輸入有效的隨機題數。題數不能為零或超過所選範圍的單字總數。');
1227
  return;
1228
  }
1229
  } else {
@@ -1231,7 +1484,7 @@
1231
  }
1232
 
1233
  if (wordsForCurrentMode.length === 0) {
1234
- alert("選定的範圍內沒有單字,請重新選擇或新增。");
1235
  return;
1236
  }
1237
 
@@ -1239,7 +1492,7 @@
1239
  const hardWords = words.filter(w => w.incorrectCount > 0)
1240
  .sort((a, b) => b.incorrectCount - a.incorrectCount).slice(0, 10);
1241
  if (hardWords.length === 0) {
1242
- alert('太棒了!目前沒有需要特別複習的困難單字。'); return;
1243
  }
1244
  wordsForCurrentMode = hardWords;
1245
  quizQueue = shuffleArray([...wordsForCurrentMode]);
@@ -1332,9 +1585,8 @@
1332
  feedbackDisplay.classList.add('text-green-600');
1333
  triggerConfetti();
1334
 
1335
- if(handwritingCanvasObj) {
1336
- handwritingCanvasObj.erase(); // 答對時清空手寫板
1337
- }
1338
 
1339
  if (!flashcardContainer.classList.contains('flipped')) {
1340
  flashcardContainer.classList.add('flipped');
@@ -1435,10 +1687,8 @@
1435
  const wrongCard = quizQueue.shift();
1436
  const reinsertIndex = Math.min(quizQueue.length, 3);
1437
  quizQueue.splice(reinsertIndex, 0, wrongCard);
1438
- // 答錯後繼續,需清空手寫板
1439
- if(handwritingCanvasObj) {
1440
- handwritingCanvasObj.erase();
1441
- }
1442
  displayCard();
1443
  });
1444
 
@@ -1706,7 +1956,7 @@
1706
  async function callGeminiToParseText(text) {
1707
  const apiKey = apiKeyInput.value.trim();
1708
  if (!apiKey) {
1709
- parseStatus.textContent = '錯誤:請先輸入您的 Gemini API 金鑰並儲存。';
1710
  parsePdfBtn.disabled = false;
1711
  aiProgressContainer.classList.add('hidden');
1712
  return;
@@ -2010,7 +2260,7 @@
2010
  saveTitleBtn.classList.add('bg-indigo-600', 'hover:bg-indigo-700');
2011
  }, 2000);
2012
  } else {
2013
- alert('冊次和課次不能為空!');
2014
  }
2015
  });
2016
 
@@ -2038,7 +2288,7 @@
2038
  const { collection, addDoc } = window.firebaseFirestore;
2039
 
2040
  if (!db) {
2041
- alert("Firebase 初始化失敗,無法分享。");
2042
  generateLinkBtn.textContent = '產生分享連結';
2043
  generateLinkBtn.disabled = false;
2044
  return;
@@ -2069,7 +2319,7 @@
2069
 
2070
  } catch (error) {
2071
  console.error("分享至 Firebase 失敗:", error);
2072
- alert("分享失敗,請檢查您的網路連線或 Firebase 設定。");
2073
  } finally {
2074
  generateLinkBtn.textContent = '重新產生';
2075
  generateLinkBtn.disabled = false;
@@ -2163,7 +2413,7 @@
2163
  }, 2000);
2164
  } else {
2165
  localStorage.removeItem('geminiApiKey');
2166
- alert('API 金鑰已清除。');
2167
  }
2168
  });
2169
 
@@ -2263,11 +2513,11 @@
2263
  loadedFromUrl = true;
2264
  }
2265
  } else {
2266
- alert("找不到分享的單字庫,請確認連結是否正確。");
2267
  }
2268
  } catch (error) {
2269
  console.error("從 Firebase 讀取資料失敗:", error);
2270
- alert("讀取分享資料時發生錯誤。");
2271
  }
2272
  }
2273
  }
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>單字閃卡 (手寫測試版)</title>
7
  <!-- 載入 Tailwind CSS CDN -->
8
  <script src="https://cdn.tailwindcss.com"></script>
9
  <!-- 載入彩帶效果庫 -->
 
14
  <script src="https://cdnjs.cloudflare.com/ajax/libs/pako/2.1.0/pako.min.js"></script>
15
  <!-- 載入 PDF.js 函式庫 -->
16
  <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.min.js"></script>
 
 
17
 
18
  <!-- 【新】Firebase SDK -->
19
  <script type="module">
 
125
  #handwriting-container {
126
  transition: all 0.3s ease;
127
  }
 
 
 
 
 
 
 
 
 
 
128
  /* 讓答案框在唯讀模式下看起來像「顯示區」 */
129
  input[readonly].handwriting-mode {
130
  background-color: #f3f4f6;
 
133
  border-color: #818cf8;
134
  cursor: not-allowed;
135
  }
136
+
137
+ /* 觸摸手寫板時的視覺回饋類別 */
138
+ .canvas-active {
139
+ border-color: #6366f1 !important; /* Indigo-500 */
140
+ background-color: #f0fdf4 !important; /* Green-50 */
141
+ }
142
+
143
+ /* 候選字列樣式 */
144
+ #candidate-bar {
145
+ display: flex;
146
+ gap: 0.5rem;
147
+ overflow-x: auto;
148
+ padding: 0.5rem 0;
149
+ margin-top: 0.5rem;
150
+ min-height: 40px;
151
+ }
152
+ .candidate-btn {
153
+ background-color: #e0e7ff;
154
+ color: #4338ca;
155
+ padding: 0.25rem 0.75rem;
156
+ border-radius: 9999px;
157
+ font-weight: 600;
158
+ white-space: nowrap;
159
+ border: 1px solid #c7d2fe;
160
+ transition: all 0.2s;
161
+ }
162
+ .candidate-btn:hover {
163
+ background-color: #c7d2fe;
164
+ }
165
+
166
+ /* [新增] Toast 通知樣式 (取代 alert) */
167
+ #toast {
168
+ visibility: hidden;
169
+ min-width: 280px;
170
+ background-color: rgba(31, 41, 55, 0.95); /* Gray-900 with opacity */
171
+ color: #fff;
172
+ text-align: center;
173
+ border-radius: 12px;
174
+ padding: 16px;
175
+ position: fixed;
176
+ z-index: 100;
177
+ left: 50%;
178
+ bottom: 30px;
179
+ transform: translateX(-50%);
180
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
181
+ font-size: 16px;
182
+ font-weight: 500;
183
+ opacity: 0;
184
+ transition: opacity 0.3s, bottom 0.3s;
185
+ }
186
+ #toast.show {
187
+ visibility: visible;
188
+ opacity: 1;
189
+ bottom: 50px;
190
+ }
191
  </style>
192
  </head>
193
  <body class="bg-gray-50 min-h-screen flex flex-col items-center p-4 sm:p-8 font-sans">
194
 
195
+ <!-- [新增] Toast 通知元素 -->
196
+ <div id="toast"></div>
197
+
198
  <!-- 主選單:新增單字 & 模式選擇 -->
199
  <div id="main-menu" class="w-full max-w-2xl bg-white p-6 rounded-3xl shadow-2xl transition-all duration-500">
200
  <h1 id="main-title" class="text-4xl font-extrabold text-center text-gray-900 mb-6"></h1>
 
415
  <input id="answer-input" placeholder="輸入你的答案 (按 Enter 提交)" class="w-full p-4 border-2 border-gray-300 rounded-xl text-2xl focus:border-indigo-500 focus:ring-indigo-500 transition-colors" autocomplete="off">
416
 
417
  <div id="handwriting-container" class="hidden flex-col gap-2">
418
+ <!-- 使用 wrapper 包裹 canvas,並強制設定固定高度,防止高度塌陷 -->
419
+ <div id="canvas-wrapper" class="relative w-full h-[250px]">
420
+ <!-- Canvas 將會由 JS 動態生成插入 -->
421
  </div>
422
 
423
+ <!-- 候選字列 -->
424
+ <div id="candidate-bar" class="hidden"></div>
425
+
426
  <div class="flex gap-2 w-full">
427
  <button id="hw-clear-btn" class="flex-1 py-3 bg-gray-200 text-gray-700 rounded-xl font-semibold hover:bg-gray-300 transition-colors flex items-center justify-center gap-1">
428
  <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm4 0a1 1 0 012 0v6a1 1 0 11-2 0V8z" clip-rule="evenodd" /></svg>
 
662
  <script>
663
  // [第四步] JavaScript 邏輯 (Script)
664
  let effectiveVersion = 'pc'; // 預設為 PC 版
665
+
666
+ // [修改] 移除第三方庫物件,改用原生變數
667
+ let isDrawing = false;
668
+ let currentStrokes = []; // 存放所有筆畫: [ [[x], [y], [t]], [[x], [y], [t]], ... ]
669
+ let currentStroke = {x: [], y: [], t: []}; // 暫存當前筆畫
670
+ let canvasCtx = null;
671
 
672
  document.addEventListener('DOMContentLoaded', () => {
673
  // DOM 元素
 
755
  // 為了配合新版介面,建議將提示顯示區域也移動到新區塊內,但為了最小修改,我們這裡用 JS 控制
756
  const quizCompletionMessage = document.getElementById('quiz-completion-message');
757
 
758
+ // 手寫功能按鈕
759
+ const hwClearBtn = document.getElementById('hw-clear-btn');
760
+ const hwUndoBtn = document.getElementById('hw-undo-btn');
761
+ const hwRecognizeBtn = document.getElementById('hw-recognize-btn');
762
+ const candidateBar = document.getElementById('candidate-bar');
763
+
764
  // 複習模式導覽
765
  const reviewNavContainer = document.getElementById('review-nav-container');
766
  const prevBtn = document.getElementById('prev-btn');
 
856
  'sentence-cloze': { title: '情境克漏字' },
857
  };
858
 
859
+ // [新增] Toast 通知函式
860
+ function showToast(message) {
861
+ const toast = document.getElementById('toast');
862
+ toast.textContent = message;
863
+ toast.classList.add('show');
864
+ setTimeout(() => {
865
+ toast.classList.remove('show');
866
+ }, 3000);
867
+ }
868
+
869
+ // [修改] 核心重寫:初始化手寫板功能 (使用原生 Canvas API)
870
  function initHandwritingBoard() {
871
+ const wrapper = document.getElementById('canvas-wrapper');
872
+ if (!wrapper) return;
873
+
874
+ // 1. 確保 wrapper 可見且有寬度
875
+ const rect = wrapper.getBoundingClientRect();
876
+ if (rect.width < 50) {
877
+ console.log('Canvas container too small, retrying in 100ms...');
878
+ setTimeout(initHandwritingBoard, 100);
879
+ return;
880
+ }
881
+
882
+ // 2. 清空並建立原生 Canvas
883
+ wrapper.innerHTML = '';
884
+ const canvas = document.createElement('canvas');
885
+ canvas.id = 'handwriting-canvas';
886
+ canvas.style.width = '100%';
887
+ canvas.style.height = '250px';
888
+ canvas.style.backgroundColor = '#ffffff';
889
+ canvas.style.border = '2px dashed #cbd5e1';
890
+ canvas.style.borderRadius = '0.75rem';
891
+ canvas.style.cursor = 'crosshair';
892
+ canvas.style.touchAction = 'none'; // 防止觸控時捲動
893
+ canvas.style.display = 'block';
894
+
895
+ // 設定 HTML 屬性的寬高 (解析度),必須與 CSS 顯示大小匹配
896
+ canvas.width = rect.width;
897
+ canvas.height = 250;
898
+
899
+ wrapper.appendChild(canvas);
900
+
901
+ // 3. 設定 Context
902
+ canvasCtx = canvas.getContext('2d');
903
+ canvasCtx.lineWidth = 5;
904
+ canvasCtx.lineCap = 'round';
905
+ canvasCtx.lineJoin = 'round';
906
+ canvasCtx.strokeStyle = '#000000';
907
+
908
+ // 4. 重置筆跡資料
909
+ currentStrokes = [];
910
+ isDrawing = false;
911
+
912
+ // 5. 綁定事件監聽器
913
+ // 滑鼠
914
+ canvas.addEventListener('mousedown', startStroke);
915
+ canvas.addEventListener('mousemove', moveStroke);
916
+ canvas.addEventListener('mouseup', endStroke);
917
+ canvas.addEventListener('mouseleave', endStroke);
918
+ // 觸控
919
+ canvas.addEventListener('touchstart', startStroke, {passive: false});
920
+ canvas.addEventListener('touchmove', moveStroke, {passive: false});
921
+ canvas.addEventListener('touchend', endStroke);
922
+
923
+ console.log('Native Handwriting board initialized with width:', rect.width);
924
+ }
925
+
926
+ // [新增] 筆畫開始
927
+ function startStroke(e) {
928
+ if (e.type === 'touchstart') e.preventDefault(); // 防止滾動
929
+ isDrawing = true;
930
+ const pos = getPos(e);
931
 
932
+ // 初始化新的一筆:[x陣列, y陣列, t陣列]
933
+ // 這裡我們先用物件暫存,endStroke 時再轉成陣列推入 currentStrokes
934
+ currentStroke = { x: [pos.x], y: [pos.y], t: [Date.now()] };
935
 
936
+ canvasCtx.beginPath();
937
+ canvasCtx.moveTo(pos.x, pos.y);
938
+
939
+ // 視覺回饋
940
+ document.getElementById('handwriting-canvas').classList.add('canvas-active');
941
+ }
942
+
943
+ // [新增] 筆畫移動
944
+ function moveStroke(e) {
945
+ if (!isDrawing) return;
946
+ if (e.type === 'touchmove') e.preventDefault(); // 防止滾動
947
+
948
+ const pos = getPos(e);
949
+ currentStroke.x.push(pos.x);
950
+ currentStroke.y.push(pos.y);
951
+ currentStroke.t.push(Date.now());
952
+
953
+ canvasCtx.lineTo(pos.x, pos.y);
954
+ canvasCtx.stroke();
955
+ }
956
 
957
+ // [新增] 筆畫結束
958
+ function endStroke(e) {
959
+ if (!isDrawing) return;
960
+ isDrawing = false;
961
+ canvasCtx.closePath();
962
+
963
+ // 將暫存的筆畫轉為 Google API 格式並存入總筆跡
964
+ // Google 格式: [ [x1, x2...], [y1, y2...], [t1, t2...] ]
965
+ if (currentStroke.x.length > 0) {
966
+ currentStrokes.push([
967
+ currentStroke.x,
968
+ currentStroke.y,
969
+ currentStroke.t
970
+ ]);
971
+ }
972
+
973
+ document.getElementById('handwriting-canvas').classList.remove('canvas-active');
974
+ }
975
 
976
+ // [新增] 取得座標 (相對於 Canvas)
977
+ function getPos(e) {
978
+ const canvas = document.getElementById('handwriting-canvas');
979
+ const rect = canvas.getBoundingClientRect();
980
+ let clientX, clientY;
981
+
982
+ if (e.touches && e.touches.length > 0) {
983
+ clientX = e.touches[0].clientX;
984
+ clientY = e.touches[0].clientY;
985
+ } else {
986
+ clientX = e.clientX;
987
+ clientY = e.clientY;
988
+ }
989
+
990
+ return {
991
+ x: clientX - rect.left,
992
+ y: clientY - rect.top
993
  };
994
+ }
995
+
996
+ // [新增] 清空畫布與資料
997
+ function clearCanvas() {
998
+ const canvas = document.getElementById('handwriting-canvas');
999
+ if(!canvas || !canvasCtx) return;
1000
+ canvasCtx.clearRect(0, 0, canvas.width, canvas.height);
1001
+ currentStrokes = [];
1002
+ answerInput.value = '';
1003
+ candidateBar.innerHTML = '';
1004
+ candidateBar.classList.add('hidden');
1005
+ }
1006
+
1007
+ // [新增] 復原上一筆
1008
+ function undoStroke() {
1009
+ if (currentStrokes.length === 0) return;
1010
+ currentStrokes.pop();
1011
+ redrawCanvas();
1012
+ }
1013
+
1014
+ // [新增] 重繪畫布 (用於 Undo)
1015
+ function redrawCanvas() {
1016
+ const canvas = document.getElementById('handwriting-canvas');
1017
+ if(!canvas || !canvasCtx) return;
1018
+ canvasCtx.clearRect(0, 0, canvas.width, canvas.height);
1019
 
1020
+ canvasCtx.beginPath();
1021
+ currentStrokes.forEach(stroke => {
1022
+ // stroke is [[x...], [y...], [t...]]
1023
+ const xs = stroke[0];
1024
+ const ys = stroke[1];
1025
+ if (xs.length > 0) {
1026
+ canvasCtx.moveTo(xs[0], ys[0]);
1027
+ for (let i = 1; i < xs.length; i++) {
1028
+ canvasCtx.lineTo(xs[i], ys[i]);
 
1029
  }
1030
+ }
1031
+ });
1032
+ canvasCtx.stroke();
1033
+ }
 
 
 
 
 
 
1034
 
1035
+ // [修正] 辨識手寫 (呼叫 Google API) - 修正 INVALID_INPUT_METHOD_NAME
1036
+ async function recognizeHandwriting() {
1037
+ if (currentStrokes.length === 0) {
1038
+ showToast("請先手寫內容!");
1039
+ return;
1040
+ }
1041
+
1042
+ hwRecognizeBtn.disabled = true;
1043
+ hwRecognizeBtn.textContent = "辨識中...";
1044
+
1045
+ const canvas = document.getElementById('handwriting-canvas');
1046
+ const width = canvas.width;
1047
+ const height = canvas.height;
1048
+
1049
+ // [修正] 移除 language 欄位,避免參數衝突
1050
+ const payload = {
1051
+ "options": "enable_pre_space",
1052
+ "requests": [{
1053
+ "writing_guide": {
1054
+ "writing_area_width": width,
1055
+ "writing_area_height": height
1056
+ },
1057
+ "ink": currentStrokes
1058
+ }]
1059
+ };
1060
 
1061
+ try {
1062
+ // [修正] 改用最通用的 www.google.com 端點,並將 app 參數改為 translate
1063
+ const response = await fetch('https://www.google.com/inputtools/request?itc=en-t-i0-handwriting&app=translate', {
1064
+ method: 'POST',
1065
+ headers: { 'Content-Type': 'application/json' },
1066
+ body: JSON.stringify(payload)
1067
  });
1068
+
1069
+ const result = await response.json();
1070
+
1071
+ // 解析回傳資料: ["SUCCESS", [[["candidate1", ...], ...]]]
1072
+ if (result[0] === 'SUCCESS' && result[1] && result[1][0] && result[1][0][1]) {
1073
+ const candidates = result[1][0][1];
1074
+
1075
+ // 1. 填入第一個候選字
1076
+ answerInput.value = candidates[0];
1077
+
1078
+ // 2. 顯示候選字列
1079
+ showCandidates(candidates);
1080
+
1081
+ // 視覺回饋
1082
+ answerInput.classList.add('border-green-500', 'bg-green-50');
1083
+ setTimeout(() => answerInput.classList.remove('border-green-500', 'bg-green-50'), 500);
1084
+ } else {
1085
+ console.warn("API 回傳格式不如預期", result);
1086
+ if(result[0] === 'INVALID_INPUT_METHOD_NAME') {
1087
+ showToast("API 錯誤:輸入法名稱無效,請確認網路環境或稍後再試。");
1088
+ }
1089
+ }
1090
+ } catch (error) {
1091
+ console.error("手寫辨識失敗:", error);
1092
+ showToast("辨識失敗,請檢查網路連線。");
1093
+ } finally {
1094
+ hwRecognizeBtn.disabled = false;
1095
+ hwRecognizeBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" /></svg> 辨識輸入`;
1096
  }
1097
+ }
1098
+
1099
+ // [新增] 顯示候選字
1100
+ function showCandidates(candidates) {
1101
+ candidateBar.innerHTML = '';
1102
+ candidateBar.classList.remove('hidden');
1103
 
1104
+ candidates.forEach(text => {
1105
+ const btn = document.createElement('button');
1106
+ btn.textContent = text;
1107
+ btn.className = 'candidate-btn';
1108
+ btn.onclick = () => {
1109
+ answerInput.value = text;
1110
+ answerInput.focus();
1111
+ };
1112
+ candidateBar.appendChild(btn);
1113
  });
1114
  }
1115
 
1116
+ // 綁定手寫按鈕事件
1117
+ if (hwClearBtn) hwClearBtn.addEventListener('click', clearCanvas);
1118
+ if (hwUndoBtn) hwUndoBtn.addEventListener('click', undoStroke);
1119
+ if (hwRecognizeBtn) hwRecognizeBtn.addEventListener('click', recognizeHandwriting);
1120
+
1121
+
1122
  // [新增] 更新輸入介面 (控制手寫板顯示與輸入法)
 
1123
  function updateInputInterface(expectedLang) {
1124
  const hwContainer = document.getElementById('handwriting-container');
1125
 
1126
+ // [修改] 為了方便在 PC 上除錯,暫時移除了 effectiveVersion === 'mobile' 的檢查
1127
+ // 現在只要是輸入英文模式 (expectedLang === 'en'),都會顯示手寫板
1128
+ const shouldUseHandwriting = (expectedLang === 'en');
1129
 
1130
  if (shouldUseHandwriting) {
1131
+ // --- 啟用手寫模式 ---
1132
+ // 1. 顯示
1133
  hwContainer.classList.remove('hidden');
1134
  hwContainer.classList.add('flex');
1135
 
1136
+ // 2. 初始化 (延遲以確保 flex 渲染完成)
1137
+ // [關鍵] 延遲時間設為 400ms,配合 CSS transition
1138
+ setTimeout(() => initHandwritingBoard(), 400);
1139
 
1140
+ // 3. 鎖定輸入框
1141
  answerInput.readOnly = true;
1142
  answerInput.placeholder = "請在下方手寫英文...";
1143
  answerInput.classList.add('handwriting-mode');
1144
 
1145
+ // 4. 嘗試讓輸入框失去焦點,避免鍵盤彈出
1146
+ if (document.activeElement === answerInput) {
1147
+ answerInput.blur();
1148
  }
1149
 
1150
  } else {
1151
+ // --- 啟用鍵盤模式 (PC 或 輸入中文) ---
 
1152
  hwContainer.classList.add('hidden');
1153
  hwContainer.classList.remove('flex');
1154
 
 
1155
  answerInput.readOnly = false;
1156
  answerInput.placeholder = (expectedLang === 'zh') ? "輸入中文答案 (按 Enter 提交)" : "輸入英文答案 (按 Enter 提交)";
1157
  answerInput.classList.remove('handwriting-mode');
 
1466
  if (mode === 'sentence-cloze') {
1467
  wordsForCurrentMode = wordPool.filter(w => w.sentence && w.sentence.en && w.sentence.zh && w.sentence.answer);
1468
  if (wordsForCurrentMode.length === 0) {
1469
+ showToast("題庫中沒有附帶完整例句(包含克漏字答案)的單字可供此模式使用。");
1470
  return;
1471
  }
1472
  } else if (mode === 'review') {
 
1476
  if (!isNaN(randomCount) && randomCount > 0 && randomCount <= wordPool.length) {
1477
  wordsForCurrentMode = shuffleArray([...wordPool]).slice(0, randomCount);
1478
  } else {
1479
+ showToast('請輸入有效的隨機題數。題數不能為零或超過所選範圍的單字總數。');
1480
  return;
1481
  }
1482
  } else {
 
1484
  }
1485
 
1486
  if (wordsForCurrentMode.length === 0) {
1487
+ showToast("選定的範圍內沒有單字,請重新選擇或新增。");
1488
  return;
1489
  }
1490
 
 
1492
  const hardWords = words.filter(w => w.incorrectCount > 0)
1493
  .sort((a, b) => b.incorrectCount - a.incorrectCount).slice(0, 10);
1494
  if (hardWords.length === 0) {
1495
+ showToast('太棒了!目前沒有需要特別複習的困難單字。'); return;
1496
  }
1497
  wordsForCurrentMode = hardWords;
1498
  quizQueue = shuffleArray([...wordsForCurrentMode]);
 
1585
  feedbackDisplay.classList.add('text-green-600');
1586
  triggerConfetti();
1587
 
1588
+ // [修正] 使用新的清空函式
1589
+ clearCanvas();
 
1590
 
1591
  if (!flashcardContainer.classList.contains('flipped')) {
1592
  flashcardContainer.classList.add('flipped');
 
1687
  const wrongCard = quizQueue.shift();
1688
  const reinsertIndex = Math.min(quizQueue.length, 3);
1689
  quizQueue.splice(reinsertIndex, 0, wrongCard);
1690
+ // [修正] 清空手寫板
1691
+ clearCanvas();
 
 
1692
  displayCard();
1693
  });
1694
 
 
1956
  async function callGeminiToParseText(text) {
1957
  const apiKey = apiKeyInput.value.trim();
1958
  if (!apiKey) {
1959
+ showToast('錯誤:請先輸入您的 Gemini API 金鑰並儲存。');
1960
  parsePdfBtn.disabled = false;
1961
  aiProgressContainer.classList.add('hidden');
1962
  return;
 
2260
  saveTitleBtn.classList.add('bg-indigo-600', 'hover:bg-indigo-700');
2261
  }, 2000);
2262
  } else {
2263
+ showToast('冊次和課次不能為空!');
2264
  }
2265
  });
2266
 
 
2288
  const { collection, addDoc } = window.firebaseFirestore;
2289
 
2290
  if (!db) {
2291
+ showToast("Firebase 初始化失敗,無法分享。");
2292
  generateLinkBtn.textContent = '產生分享連結';
2293
  generateLinkBtn.disabled = false;
2294
  return;
 
2319
 
2320
  } catch (error) {
2321
  console.error("分享至 Firebase 失敗:", error);
2322
+ showToast("分享失敗,請檢查您的網路連線或 Firebase 設定。");
2323
  } finally {
2324
  generateLinkBtn.textContent = '重新產生';
2325
  generateLinkBtn.disabled = false;
 
2413
  }, 2000);
2414
  } else {
2415
  localStorage.removeItem('geminiApiKey');
2416
+ showToast('API 金鑰已清除。');
2417
  }
2418
  });
2419
 
 
2513
  loadedFromUrl = true;
2514
  }
2515
  } else {
2516
+ showToast("找不到分享的單字庫,請確認連結是否正確。");
2517
  }
2518
  } catch (error) {
2519
  console.error("從 Firebase 讀取資料失敗:", error);
2520
+ showToast("讀取分享資料時發生錯誤。");
2521
  }
2522
  }
2523
  }