syurein commited on
Commit
016f062
·
1 Parent(s): 04a9f31
Files changed (3) hide show
  1. static/script.js +272 -89
  2. static/style.css +167 -32
  3. templates/learning.html +23 -16
static/script.js CHANGED
@@ -6,6 +6,8 @@
6
  let learningData = null; // 学習データ (learning.html用)
7
  let currentItemIndex = 0; // 現在表示中のアイテムインデックス (learning.html用)
8
  let currentMode = 'quiz'; // 現在のモード 'quiz' or 'summary' (learning.html用)
 
 
9
 
10
  // --- 共通関数 ---
11
 
@@ -23,28 +25,43 @@ function navigateTo(url) {
23
  */
24
  function openMenu() {
25
  console.log("Menu button clicked. Implement menu display logic here.");
26
- // 例: サイドバーメニューを表示する、モーダルを表示するなど
27
  alert("メニュー機能は未実装です。\nフッターのナビゲーションを使用してください。");
28
  }
29
 
30
  /**
31
  * ローディングスピナーを表示/非表示します。
32
  * @param {boolean} show - trueで表示、falseで非表示
 
33
  */
34
- function toggleLoading(show) {
35
- const spinner = document.querySelector('.loading-spinner'); // input.html用
36
- const buttonText = document.querySelector('.button-text'); // input.html用
37
- const generateButton = document.getElementById('generate-button'); // input.html用
38
-
39
- if (spinner && buttonText && generateButton) {
40
- spinner.style.display = show ? 'inline-block' : 'none';
41
- buttonText.textContent = show ? '生成中...' : '生成する';
42
- generateButton.disabled = show;
 
 
 
 
 
 
43
  }
44
- // learning.html 用のローディング表示/非表示も必要に応じて追加
45
- const loadingCard = document.getElementById('loading-card-indicator'); // 仮のID
46
- if (loadingCard) {
47
- loadingCard.style.display = show ? 'block' : 'none';
 
 
 
 
 
 
 
 
 
48
  }
49
  }
50
 
@@ -96,23 +113,40 @@ async function handleGenerateSubmit() {
96
 
97
  if (!youtubeUrl) {
98
  displayErrorMessage('YouTubeリンクを入力してください。');
99
- return false; // prevent default form submission
100
  }
101
 
102
- // 簡単なURL形式チェック(より厳密なチェックも可能)
103
- if (!youtubeUrl.includes('youtube.com/') && !youtubeUrl.includes('youtu.be/')) {
104
- displayErrorMessage('有効なYouTubeリンクを入力してください。');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  return false;
106
  }
107
 
108
 
109
- toggleLoading(true); // ローディング開始
110
 
111
  try {
112
  const response = await fetch('/api/generate', {
113
  method: 'POST',
114
  headers: {
115
  'Content-Type': 'application/json',
 
116
  },
117
  body: JSON.stringify({ url: youtubeUrl }),
118
  });
@@ -121,19 +155,29 @@ async function handleGenerateSubmit() {
121
 
122
  if (response.ok && result.success && result.data && result.data.id) {
123
  // 成功したら学習画面へ遷移
124
- alert('生成に成功しました!学習画面に移動します。');
125
  goToLearning(result.data.id);
126
  } else {
127
  // 失敗したらエラーメッセージ表示
128
  console.error('Generation failed:', result);
129
- displayErrorMessage(result.message || '生成中にエラーが発生しました。');
 
 
130
  }
131
 
132
  } catch (error) {
133
  console.error('Error during generation request:', error);
134
- displayErrorMessage('通信エラーが発生しました。');
 
 
 
 
 
 
 
 
135
  } finally {
136
- toggleLoading(false); // ローディング終了
137
  }
138
 
139
  return false; // prevent default form submission
@@ -149,9 +193,9 @@ async function initializeLearningScreen() {
149
  console.log('Initializing Learning Screen...');
150
  const params = new URLSearchParams(window.location.search);
151
  const contentId = params.get('id');
152
- const loadingIndicator = document.getElementById('mode-indicator'); // 仮にモード表示部を使う
153
- const cardElement = document.getElementById('learning-card');
154
- const paginationElement = document.querySelector('.pagination');
155
 
156
  if (!contentId) {
157
  displayLearningError('コンテンツIDが指定されていません。');
@@ -159,17 +203,20 @@ async function initializeLearningScreen() {
159
  }
160
  console.log('Content ID:', contentId);
161
 
162
- // ローディング表示(簡易版)
163
- if (loadingIndicator) loadingIndicator.textContent = '読み込み中...';
164
- if (cardElement) cardElement.style.opacity = '0.5'; // 少し薄くする
165
- if (paginationElement) paginationElement.style.display = 'none';
166
-
167
 
168
  try {
169
  const response = await fetch(`/api/learning/${contentId}`);
170
  if (!response.ok) {
171
- const errorData = await response.json().catch(() => ({ message: `HTTPエラー: ${response.status}` }));
172
- throw new Error(errorData.message || `サーバーからのデータ取得に失敗しました (${response.status})`);
 
 
 
 
 
 
 
173
  }
174
 
175
  const result = await response.json();
@@ -177,8 +224,8 @@ async function initializeLearningScreen() {
177
 
178
  if (result.success && result.data) {
179
  learningData = result.data; // グローバル変数に格納
180
- if (!learningData.items || learningData.items.length === 0) {
181
- throw new Error('学習データが空です。');
182
  }
183
  // タイトルを設定
184
  const titleElement = document.getElementById('learning-title');
@@ -189,7 +236,6 @@ async function initializeLearningScreen() {
189
  // 最初のアイテムを表示
190
  currentItemIndex = 0;
191
  displayCurrentItem();
192
- if (paginationElement) paginationElement.style.display = 'flex'; // ページネーション表示
193
 
194
  } else {
195
  throw new Error(result.message || '学習データの読み込みに失敗しました。');
@@ -199,9 +245,7 @@ async function initializeLearningScreen() {
199
  console.error('Error initializing learning screen:', error);
200
  displayLearningError(`読み込みエラー: ${error.message}`);
201
  } finally {
202
- // ローディング表示終了(簡易版)
203
- if (cardElement) cardElement.style.opacity = '1';
204
- // mode-indicatorはdisplayCurrentItemで更新される
205
  }
206
  }
207
 
@@ -209,6 +253,10 @@ async function initializeLearningScreen() {
209
  * 現在の学習アイテムをカードに表示
210
  */
211
  function displayCurrentItem() {
 
 
 
 
212
  if (!learningData || !learningData.items || currentItemIndex < 0 || currentItemIndex >= learningData.items.length) {
213
  console.error('Invalid learning data or index');
214
  displayLearningError('学習データを表示できません。');
@@ -216,6 +264,7 @@ function displayCurrentItem() {
216
  }
217
 
218
  const item = learningData.items[currentItemIndex];
 
219
  const cardTextElement = document.getElementById('card-text');
220
  const answerTextElement = document.getElementById('answer-text');
221
  const tapToShowElement = document.getElementById('tap-to-show');
@@ -227,52 +276,100 @@ function displayCurrentItem() {
227
  answerTextElement.style.display = 'none';
228
  tapToShowElement.style.display = 'none';
229
  optionsArea.innerHTML = '';
230
- optionsArea.style.display = 'none';
 
231
 
232
- if (item.type === 'question') {
233
  currentMode = 'quiz';
234
  modeIndicator.textContent = 'クイズモード';
235
- cardTextElement.textContent = item.text; // 質問文
236
  answerTextElement.textContent = `答え: ${item.answer}`; // 答えを事前に設定(非表示)
237
  tapToShowElement.style.display = 'block'; // タップして表示を表示
238
 
239
- // 選択肢ボタンを生成(解答チェックはここではしない)
240
- if (item.options && Array.isArray(item.options)) {
241
- optionsArea.style.display = 'block';
242
- item.options.forEach(option => {
 
 
 
 
243
  const button = document.createElement('button');
244
  button.classList.add('option-button');
245
  button.textContent = option;
246
- // ★★★ 選択肢クリック時の動作を変更: 正誤判定ではなく解答表示 ★★★
247
- button.onclick = revealAnswer; // どのボタンを押しても解答表示
248
  optionsArea.appendChild(button);
249
  });
 
 
 
 
250
  }
 
 
 
251
 
252
- } else if (item.type === 'summary') {
253
  currentMode = 'summary';
254
  modeIndicator.textContent = '要約モード';
255
- cardTextElement.innerHTML = item.text.replace(/\n/g, '<br>'); // 要約文(改行対応)
 
 
256
 
257
- // 要約モードでは答えも選択肢も不要
 
 
258
  } else {
259
- console.warn('Unknown item type:', item.type);
260
- cardTextElement.textContent = `[不明なデータタイプ: ${item.type}] ${item.text || ''}`;
261
- modeIndicator.textContent = '不明モード';
 
 
262
  }
263
 
264
  updatePagination();
265
  }
266
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  /**
268
- * クイズの解答を表示する
 
269
  */
270
- function revealAnswer() {
271
  // クイズモードの場合のみ動作
272
  if (currentMode === 'quiz') {
273
  const answerTextElement = document.getElementById('answer-text');
274
  const tapToShowElement = document.getElementById('tap-to-show');
275
  const optionsArea = document.getElementById('options-area');
 
 
 
 
 
 
 
276
 
277
  if (answerTextElement) {
278
  answerTextElement.style.display = 'block'; // 答えを表示
@@ -280,20 +377,26 @@ function revealAnswer() {
280
  if (tapToShowElement) {
281
  tapToShowElement.style.display = 'none'; // 「タップして表示」を隠す
282
  }
 
 
 
 
283
 
284
  // 選択肢ボタンを正解・不正解で色付け&無効化
285
  if (optionsArea && learningData && learningData.items[currentItemIndex]) {
286
  const correctAnswer = learningData.items[currentItemIndex].answer;
287
  const buttons = optionsArea.getElementsByTagName('button');
288
  for (let btn of buttons) {
289
- btn.disabled = true; // ボタンを無効化
 
 
290
  if (btn.textContent === correctAnswer) {
291
- btn.classList.add('correct'); // 正解スタイル
 
 
292
  } else {
293
- btn.classList.add('disabled'); // 不正解または他の選択肢スタイル
294
  }
295
- // クリックイベントを削除(任意)
296
- btn.onclick = null;
297
  }
298
  }
299
  }
@@ -330,9 +433,21 @@ function updatePagination() {
330
 
331
  if (pageInfo && prevButton && nextButton && learningData && learningData.items) {
332
  const totalItems = learningData.items.length;
333
- pageInfo.textContent = `${currentItemIndex + 1} / ${totalItems}`;
334
- prevButton.disabled = currentItemIndex === 0;
335
- nextButton.disabled = currentItemIndex === totalItems - 1;
 
 
 
 
 
 
 
 
 
 
 
 
336
  }
337
  }
338
 
@@ -348,13 +463,44 @@ function displayLearningError(message) {
348
  const tapToShow = document.getElementById('tap-to-show');
349
 
350
  if (titleElement) titleElement.textContent = 'エラー';
351
- if (cardElement) cardElement.innerHTML = `<p class="main-text" style="color: red; text-align: center;">${message}</p>`;
 
 
 
 
352
  if (paginationElement) paginationElement.style.display = 'none';
 
353
  if (optionsArea) optionsArea.style.display = 'none';
354
- if (modeIndicator) modeIndicator.style.display = 'none';
355
  if (tapToShow) tapToShow.style.display = 'none';
356
  }
357
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
 
359
  // --- settings.html 用の処理 ---
360
 
@@ -380,9 +526,11 @@ function handleToggleChange(checkbox, type) {
380
  function handleLogout() {
381
  console.log("Logout clicked");
382
  // TODO: 実際のログアウト処理(API呼び出し、セッションクリアなど)
383
- alert("ログアウト機能は未実装です。");
384
  // 必要であればログイン画面などに遷移
385
  // navigateTo('/login');
 
 
386
  }
387
 
388
  /**
@@ -393,10 +541,8 @@ function applyDarkModePreference() {
393
  const darkModeEnabled = localStorage.getItem('darkModeEnabled') === 'true';
394
  document.body.classList.toggle('dark-mode', darkModeEnabled);
395
  // 設定画面のトグルスイッチの状態も合わせる
396
- const toggle = document.querySelector('input[onchange*="handleToggleChange"][onchange*="dark"]');
397
- if (toggle) {
398
- toggle.checked = darkModeEnabled;
399
- }
400
  } catch (e) {
401
  console.warn('Could not load dark mode preference from localStorage.');
402
  }
@@ -404,30 +550,54 @@ function applyDarkModePreference() {
404
 
405
 
406
  // --- ページの初期化処理 ---
407
- document.addEventListener('DOMContentLoaded', () => {
408
- const pathname = window.location.pathname;
 
 
409
 
410
- applyDarkModePreference(); // 全ページでダークモード設定を適用
411
 
412
- if (pathname === '/' || pathname === '/input') {
413
- // input.html の初期化(特に不要かもしれないが、フォームイベントリスナーなど)
414
  const form = document.getElementById('generate-form');
415
  if (form) {
416
  form.addEventListener('submit', (event) => {
417
- event.preventDefault(); // デフォルトの送信をキャンセル
418
  handleGenerateSubmit();
419
  });
420
  }
421
- } else if (pathname === '/learning') {
 
 
 
 
 
 
 
 
 
 
 
422
  initializeLearningScreen();
423
- } else if (pathname === '/history') {
424
  // history.html の初期化(動的にリストを生成する場合など)
425
- // このサンプルでは静的なので特に不要
426
- } else if (pathname === '/settings') {
427
- // settings.html の初期化(ダークモードの適用は applyDarkModePreference で実施済み)
 
 
 
 
 
 
 
 
 
 
 
 
428
  }
429
 
430
- // フッターナビゲーションのアクティブ状態設定(任意)
431
  updateFooterNavActiveState(pathname);
432
  });
433
 
@@ -435,7 +605,7 @@ document.addEventListener('DOMContentLoaded', () => {
435
  /**
436
  * フッターナビゲーションのアクティブ状態を更新
437
  */
438
- function updateFooterNavActiveState(pathname) {
439
  const footerNav = document.querySelector('.footer-nav');
440
  if (!footerNav) return;
441
 
@@ -444,19 +614,25 @@ document.addEventListener('DOMContentLoaded', () => {
444
  button.classList.remove('active'); // Reset all
445
  const onclickAttr = button.getAttribute('onclick');
446
  if (onclickAttr) {
447
- if ((pathname === '/' || pathname === '/input') && onclickAttr.includes('goToInput')) {
 
448
  button.classList.add('active');
449
- } else if (pathname === '/history' && onclickAttr.includes('goToHistory')) {
450
  button.classList.add('active');
451
- } else if (pathname === '/settings' && onclickAttr.includes('goToSettings')) {
452
  button.classList.add('active');
453
  }
 
 
 
 
454
  }
455
  });
456
  }
457
 
458
 
459
  // デバッグ用に一部関数をグローバルスコープに公開(開発中のみ推奨)
 
460
  window.debug = {
461
  navigateTo,
462
  goToInput,
@@ -466,9 +642,16 @@ window.debug = {
466
  openMenu,
467
  handleGenerateSubmit,
468
  initializeLearningScreen,
 
469
  revealAnswer,
470
  goToNext,
471
  goToPrev,
472
  handleToggleChange,
473
- handleLogout
474
- };
 
 
 
 
 
 
 
6
  let learningData = null; // 学習データ (learning.html用)
7
  let currentItemIndex = 0; // 現在表示中のアイテムインデックス (learning.html用)
8
  let currentMode = 'quiz'; // 現在のモード 'quiz' or 'summary' (learning.html用)
9
+ let correctEffectTimeout; // 正解エフェクト非表示用のタイマーID (learning.html用)
10
+ let correctEffect = null; // 正解エフェクト要素 (learning.html用)
11
 
12
  // --- 共通関数 ---
13
 
 
25
  */
26
  function openMenu() {
27
  console.log("Menu button clicked. Implement menu display logic here.");
 
28
  alert("メニュー機能は未実装です。\nフッターのナビゲーションを使用してください。");
29
  }
30
 
31
  /**
32
  * ローディングスピナーを表示/非表示します。
33
  * @param {boolean} show - trueで表示、falseで非表示
34
+ * @param {string} buttonId - 操作対象のボタンID (input.html用)
35
  */
36
+ function toggleLoading(show, buttonId = 'generate-button') {
37
+ const generateButton = document.getElementById(buttonId);
38
+ if (!generateButton) return;
39
+
40
+ const spinner = generateButton.querySelector('.loading-spinner');
41
+ const buttonText = generateButton.querySelector('.button-text');
42
+
43
+ if (show) {
44
+ generateButton.disabled = true;
45
+ if (spinner) spinner.style.display = 'inline-block';
46
+ if (buttonText) buttonText.textContent = '生成中...';
47
+ } else {
48
+ generateButton.disabled = false;
49
+ if (spinner) spinner.style.display = 'none';
50
+ if (buttonText) buttonText.textContent = '生成する'; // 元のテキストに戻す
51
  }
52
+
53
+ // learning.html 用のローディング表示/非表示
54
+ const loadingIndicator = document.getElementById('mode-indicator');
55
+ const cardElement = document.getElementById('learning-card');
56
+ const paginationElement = document.querySelector('.pagination');
57
+ const optionsArea = document.getElementById('options-area');
58
+
59
+ if (pathname === '/learning') { // learning.htmlの場合のみ
60
+ if (loadingIndicator) loadingIndicator.textContent = show ? '読み込み中...' : ''; // モード表示はdisplayCurrentItemで行う
61
+ if (loadingIndicator) loadingIndicator.classList.toggle('loading', show);
62
+ if (cardElement) cardElement.style.opacity = show ? '0.5' : '1';
63
+ if (paginationElement) paginationElement.style.display = show ? 'none' : 'flex';
64
+ if (optionsArea) optionsArea.style.display = show ? 'none' : 'block'; // 初期はblockだがdisplayCurrentItemで調整
65
  }
66
  }
67
 
 
113
 
114
  if (!youtubeUrl) {
115
  displayErrorMessage('YouTubeリンクを入力してください。');
116
+ return false;
117
  }
118
 
119
+ // 簡単なURL形式チェック
120
+ try {
121
+ const urlObj = new URL(youtubeUrl);
122
+ if (!['www.youtube.com', 'youtube.com', 'youtu.be'].includes(urlObj.hostname)) {
123
+ throw new Error('Invalid hostname');
124
+ }
125
+ // youtu.be/XXX または youtube.com/watch?v=XXX の形式を期待
126
+ if (urlObj.hostname === 'youtu.be' && !urlObj.pathname.substring(1)) {
127
+ throw new Error('Missing video ID for youtu.be');
128
+ }
129
+ if (urlObj.hostname.includes('youtube.com') && urlObj.pathname === '/watch' && !urlObj.searchParams.get('v')) {
130
+ throw new Error('Missing video ID for youtube.com/watch');
131
+ }
132
+ if (urlObj.hostname.includes('youtube.com') && urlObj.pathname.startsWith('/shorts/') && !urlObj.pathname.substring(8)) {
133
+ throw new Error('Missing video ID for youtube.com/shorts');
134
+ }
135
+
136
+ } catch (e) {
137
+ displayErrorMessage('有効なYouTube動画リンクを入力してください。(例: https://www.youtube.com/watch?v=...)');
138
  return false;
139
  }
140
 
141
 
142
+ toggleLoading(true, 'generate-button'); // ローディング開始
143
 
144
  try {
145
  const response = await fetch('/api/generate', {
146
  method: 'POST',
147
  headers: {
148
  'Content-Type': 'application/json',
149
+ 'Accept': 'application/json', // サーバーがJSONを返すことを期待
150
  },
151
  body: JSON.stringify({ url: youtubeUrl }),
152
  });
 
155
 
156
  if (response.ok && result.success && result.data && result.data.id) {
157
  // 成功したら学習画面へ遷移
158
+ // alert('生成に成功しました!学習画面に移動します。'); // すぐ遷移するので不要かも
159
  goToLearning(result.data.id);
160
  } else {
161
  // 失敗したらエラーメッセージ表示
162
  console.error('Generation failed:', result);
163
+ // サーバーからのメッセージがあれば表示、なければ汎用メッセージ
164
+ const serverMessage = result.message || (result.error ? result.error.message : null);
165
+ displayErrorMessage(serverMessage || '生成中に不明なエラーが発生しました。');
166
  }
167
 
168
  } catch (error) {
169
  console.error('Error during generation request:', error);
170
+ // ネットワークエラーか、JSONパースエラーか
171
+ if (error instanceof SyntaxError) {
172
+ displayErrorMessage('サーバーからの応答形式が不正です。');
173
+ } else if (error instanceof TypeError) {
174
+ displayErrorMessage('通信エラーが発生しました。ネットワーク接続を確認してください。');
175
+ }
176
+ else {
177
+ displayErrorMessage(`通信エラーが発生しました: ${error.message}`);
178
+ }
179
  } finally {
180
+ toggleLoading(false, 'generate-button'); // ローディング終了
181
  }
182
 
183
  return false; // prevent default form submission
 
193
  console.log('Initializing Learning Screen...');
194
  const params = new URLSearchParams(window.location.search);
195
  const contentId = params.get('id');
196
+
197
+ // ★★★ 正解エフェクト要素を取得 ★★★
198
+ correctEffect = document.getElementById('correct-effect');
199
 
200
  if (!contentId) {
201
  displayLearningError('コンテンツIDが指定されていません。');
 
203
  }
204
  console.log('Content ID:', contentId);
205
 
206
+ toggleLoading(true); // ローディング表示開始
 
 
 
 
207
 
208
  try {
209
  const response = await fetch(`/api/learning/${contentId}`);
210
  if (!response.ok) {
211
+ // エラーレスポンスがJSON形式でない場合も考慮
212
+ let errorMessage = `サーバーからのデータ取得に失敗しました (${response.status})`;
213
+ try {
214
+ const errorData = await response.json();
215
+ errorMessage = errorData.message || errorMessage;
216
+ } catch (e) {
217
+ // JSONパース失敗時はそのままのエラーメッセージを使う
218
+ }
219
+ throw new Error(errorMessage);
220
  }
221
 
222
  const result = await response.json();
 
224
 
225
  if (result.success && result.data) {
226
  learningData = result.data; // グローバル変数に格納
227
+ if (!learningData.items || !Array.isArray(learningData.items) || learningData.items.length === 0) {
228
+ throw new Error('学習データが見つからないか、形式が正しくありません。');
229
  }
230
  // タイトルを設定
231
  const titleElement = document.getElementById('learning-title');
 
236
  // 最初のアイテムを表示
237
  currentItemIndex = 0;
238
  displayCurrentItem();
 
239
 
240
  } else {
241
  throw new Error(result.message || '学習データの読み込みに失敗しました。');
 
245
  console.error('Error initializing learning screen:', error);
246
  displayLearningError(`読み込みエラー: ${error.message}`);
247
  } finally {
248
+ toggleLoading(false); // ローディング表示終了
 
 
249
  }
250
  }
251
 
 
253
  * 現在の学習アイテムをカードに表示
254
  */
255
  function displayCurrentItem() {
256
+ // ★★★ 最初にエフェクトを隠し、タイマーもクリア ★★★
257
+ hideCorrectEffect();
258
+ clearTimeout(correctEffectTimeout);
259
+
260
  if (!learningData || !learningData.items || currentItemIndex < 0 || currentItemIndex >= learningData.items.length) {
261
  console.error('Invalid learning data or index');
262
  displayLearningError('学習データを表示できません。');
 
264
  }
265
 
266
  const item = learningData.items[currentItemIndex];
267
+ const cardElement = document.getElementById('learning-card');
268
  const cardTextElement = document.getElementById('card-text');
269
  const answerTextElement = document.getElementById('answer-text');
270
  const tapToShowElement = document.getElementById('tap-to-show');
 
276
  answerTextElement.style.display = 'none';
277
  tapToShowElement.style.display = 'none';
278
  optionsArea.innerHTML = '';
279
+ optionsArea.style.display = 'none'; // デフォルトは非表示
280
+ modeIndicator.classList.remove('loading'); // ローディングクラス除去
281
 
282
+ if (item.type === 'question' && item.question && item.answer) { // question と answer が存在するかチェック
283
  currentMode = 'quiz';
284
  modeIndicator.textContent = 'クイズモード';
285
+ cardTextElement.textContent = item.question; // question プロパティを使用
286
  answerTextElement.textContent = `答え: ${item.answer}`; // 答えを事前に設定(非表示)
287
  tapToShowElement.style.display = 'block'; // タップして表示を表示
288
 
289
+ // 選択肢ボタンを生成
290
+ if (item.options && Array.isArray(item.options) && item.options.length > 0) {
291
+ optionsArea.style.display = 'block'; // 選択肢があれば表示
292
+ // 選択肢をシャッフルする場合(任意)
293
+ // const shuffledOptions = [...item.options].sort(() => Math.random() - 0.5);
294
+ const optionsToDisplay = item.options; // シャッフルしない場合
295
+
296
+ optionsToDisplay.forEach(option => {
297
  const button = document.createElement('button');
298
  button.classList.add('option-button');
299
  button.textContent = option;
300
+ // ★★★ 選択肢クリック時の動作を変更 ★★★
301
+ button.onclick = () => handleOptionClick(option); // クリックされた選択肢を渡す
302
  optionsArea.appendChild(button);
303
  });
304
+ } else {
305
+ console.warn(`Item ${currentItemIndex} is a question but has no options.`);
306
+ // 選択肢がないクイズの場合の表示(例:自由記述?)
307
+ tapToShowElement.style.display = 'block'; // 答え表示はできるようにしておく
308
  }
309
+ // カード自体のクリックでも解答表示できるようにする
310
+ cardElement.onclick = revealAnswer;
311
+ tapToShowElement.onclick = revealAnswer;
312
 
313
+ } else if (item.type === 'summary' && item.text) { // summary と text が存在するかチェック
314
  currentMode = 'summary';
315
  modeIndicator.textContent = '要約モード';
316
+ // 改行を<br>に変換し、安全にHTMLとして設定(XSS対策のためtextContent推奨だが、改行表示のためにinnerHTMLを使用)
317
+ // サニタイズが必要な場合はライブラリ(DOMPurifyなど)を使うこと
318
+ cardTextElement.innerHTML = item.text.replace(/\n/g, '<br>');
319
 
320
+ // 要約モードでは答えも選択肢も不要、カードクリックも不要
321
+ cardElement.onclick = null;
322
+ tapToShowElement.style.display = 'none';
323
  } else {
324
+ console.warn('Unknown or invalid item type:', item.type, item);
325
+ modeIndicator.textContent = '不明なデータ';
326
+ cardTextElement.textContent = `[データ形式エラー] ${item.text || item.question || '内容なし'}`;
327
+ cardElement.onclick = null; // クリック動作無効
328
+ tapToShowElement.style.display = 'none';
329
  }
330
 
331
  updatePagination();
332
  }
333
 
334
+ // ★★★ 選択肢クリック時の処理を追加 ★★★
335
+ function handleOptionClick(selectedOption) {
336
+ if (currentMode !== 'quiz') return; // クイズモード以外では何もしない
337
+
338
+ const currentItem = learningData.items[currentItemIndex];
339
+ const correctAnswer = currentItem.answer;
340
+ const isCorrect = selectedOption === correctAnswer;
341
+
342
+ // --- 正解だった場合の処理 ---
343
+ if (isCorrect) {
344
+ console.log("正解!");
345
+ showCorrectEffect(); // ★★★ 正解エフェクトを表示 ★★★
346
+ } else {
347
+ console.log("不正解...");
348
+ // 不正解時のエフェクトなどが必要ならここに追加
349
+ }
350
+
351
+ // 選択肢ボタンの状態更新と解答表示
352
+ revealAnswer(selectedOption); // 選んだ選択肢を渡してスタイルを設定
353
+ }
354
+
355
+
356
  /**
357
+ * クイズの解答を表示する(選択肢クリックまたはカードタップ時)
358
+ * @param {string|null} selectedOption - ユーザーが選択した選択肢 (選択肢クリック時のみ渡される)
359
  */
360
+ function revealAnswer(selectedOption = null) {
361
  // クイズモードの場合のみ動作
362
  if (currentMode === 'quiz') {
363
  const answerTextElement = document.getElementById('answer-text');
364
  const tapToShowElement = document.getElementById('tap-to-show');
365
  const optionsArea = document.getElementById('options-area');
366
+ const cardElement = document.getElementById('learning-card');
367
+
368
+ // 既に表示済みなら何もしない(二重実行防止)
369
+ if (answerTextElement && answerTextElement.style.display === 'block') {
370
+ return;
371
+ }
372
+
373
 
374
  if (answerTextElement) {
375
  answerTextElement.style.display = 'block'; // 答えを表示
 
377
  if (tapToShowElement) {
378
  tapToShowElement.style.display = 'none'; // 「タップして表示」を隠す
379
  }
380
+ // カード自体のクリックイベントを解除(解答表示後は不要)
381
+ if (cardElement) {
382
+ cardElement.onclick = null;
383
+ }
384
 
385
  // 選択肢ボタンを正解・不正解で色付け&無効化
386
  if (optionsArea && learningData && learningData.items[currentItemIndex]) {
387
  const correctAnswer = learningData.items[currentItemIndex].answer;
388
  const buttons = optionsArea.getElementsByTagName('button');
389
  for (let btn of buttons) {
390
+ btn.disabled = true; // すべてのボタンを無効化
391
+ btn.onclick = null; // クリックイベントも削除
392
+
393
  if (btn.textContent === correctAnswer) {
394
+ btn.classList.add('correct'); // 正解ボタン
395
+ } else if (btn.textContent === selectedOption) {
396
+ btn.classList.add('incorrect'); // 選択された不正解ボタン
397
  } else {
398
+ btn.classList.add('other-disabled'); // 選ばれなかった他の選択肢
399
  }
 
 
400
  }
401
  }
402
  }
 
433
 
434
  if (pageInfo && prevButton && nextButton && learningData && learningData.items) {
435
  const totalItems = learningData.items.length;
436
+ // totalItemsが0の場合のエラーを防ぐ
437
+ if (totalItems > 0) {
438
+ pageInfo.textContent = `${currentItemIndex + 1} / ${totalItems}`;
439
+ prevButton.disabled = currentItemIndex === 0;
440
+ nextButton.disabled = currentItemIndex === totalItems - 1;
441
+ } else {
442
+ pageInfo.textContent = `0 / 0`;
443
+ prevButton.disabled = true;
444
+ nextButton.disabled = true;
445
+ }
446
+ } else {
447
+ // 要素がないかデータがない場合
448
+ if(pageInfo) pageInfo.textContent = '- / -';
449
+ if(prevButton) prevButton.disabled = true;
450
+ if(nextButton) nextButton.disabled = true;
451
  }
452
  }
453
 
 
463
  const tapToShow = document.getElementById('tap-to-show');
464
 
465
  if (titleElement) titleElement.textContent = 'エラー';
466
+ if (modeIndicator) modeIndicator.textContent = 'エラー';
467
+ if (cardElement) {
468
+ cardElement.innerHTML = `<p class="main-text" style="color: red; text-align: center; padding: 20px;">${message}</p>`;
469
+ cardElement.onclick = null; // クリック無効
470
+ }
471
  if (paginationElement) paginationElement.style.display = 'none';
472
+ if (optionsArea) optionsArea.innerHTML = ''; // オプションクリア
473
  if (optionsArea) optionsArea.style.display = 'none';
 
474
  if (tapToShow) tapToShow.style.display = 'none';
475
  }
476
 
477
+ // --- ★★★ 正解エフェクト表示関数を追加 ★★★ ---
478
+ function showCorrectEffect() {
479
+ if (correctEffect) {
480
+ // 既存のタイムアウトがあればクリア
481
+ clearTimeout(correctEffectTimeout);
482
+
483
+ correctEffect.classList.add('show'); // 表示用クラスを追加
484
+
485
+ // 1秒後 (1000ms) に非表示処理を開始
486
+ correctEffectTimeout = setTimeout(() => {
487
+ hideCorrectEffect();
488
+ }, 1000); // 表示時間はここで調整 (ミリ秒)
489
+ }
490
+ }
491
+
492
+ // --- ★★★ 正解エフェクト非表示関数を追加 ★★★ ---
493
+ function hideCorrectEffect() {
494
+ if (correctEffect && correctEffect.classList.contains('show')) {
495
+ correctEffect.classList.remove('show');
496
+ // アニメーション完了後に display: none に戻したい場合は transitionend イベントを使う
497
+ // correctEffect.addEventListener('transitionend', () => {
498
+ // correctEffect.style.display = 'none';
499
+ // }, { once: true }); // 一度だけ実行
500
+ // ただし、クラスの付け外しだけで opacity と transform を制御する方がシンプル
501
+ }
502
+ }
503
+
504
 
505
  // --- settings.html 用の処理 ---
506
 
 
526
  function handleLogout() {
527
  console.log("Logout clicked");
528
  // TODO: 実際のログアウト処理(API呼び出し、セッションクリアなど)
529
+ alert("ログアウトしました。(機能は未実装)");
530
  // 必要であればログイン画面などに遷移
531
  // navigateTo('/login');
532
+ // 例として入力画面に戻る
533
+ goToInput();
534
  }
535
 
536
  /**
 
541
  const darkModeEnabled = localStorage.getItem('darkModeEnabled') === 'true';
542
  document.body.classList.toggle('dark-mode', darkModeEnabled);
543
  // 設定画面のトグルスイッチの状態も合わせる
544
+ // DOMContentLoaded より前に実行される場合があるので、
545
+ // settings.htmlの初期化時にスイッチの状態を更新する方が確実
 
 
546
  } catch (e) {
547
  console.warn('Could not load dark mode preference from localStorage.');
548
  }
 
550
 
551
 
552
  // --- ページの初期化処理 ---
553
+ const pathname = window.location.pathname; // グローバルスコープで取得
554
+
555
+ // ダークモード設定を最初に適用 (FOUC対策)
556
+ applyDarkModePreference();
557
 
558
+ document.addEventListener('DOMContentLoaded', () => {
559
 
560
+ if (pathname === '/' || pathname === '/input' || pathname === '/input.html') { // input.html のパスパターンを考慮
 
561
  const form = document.getElementById('generate-form');
562
  if (form) {
563
  form.addEventListener('submit', (event) => {
564
+ event.preventDefault();
565
  handleGenerateSubmit();
566
  });
567
  }
568
+ // input.html 専用の初期化があればここに追加
569
+ // 例: URLパラメータから初期値を設定するなど
570
+ const urlParams = new URLSearchParams(window.location.search);
571
+ const initialUrl = urlParams.get('url');
572
+ if (initialUrl) {
573
+ const urlInput = document.getElementById('youtube-url');
574
+ if (urlInput) {
575
+ urlInput.value = initialUrl;
576
+ }
577
+ }
578
+
579
+ } else if (pathname === '/learning' || pathname === '/learning.html') {
580
  initializeLearningScreen();
581
+ } else if (pathname === '/history' || pathname === '/history.html') {
582
  // history.html の初期化(動的にリストを生成する場合など)
583
+ console.log("History page loaded.");
584
+ // loadHistoryData(); // 仮の関数呼び出し
585
+ } else if (pathname === '/settings' || pathname === '/settings.html') {
586
+ // settings.html の初期化
587
+ console.log("Settings page loaded.");
588
+ // ダークモードトグルの状態をlocalStorageに合わせて更新
589
+ try {
590
+ const darkModeEnabled = localStorage.getItem('darkModeEnabled') === 'true';
591
+ const toggle = document.querySelector('input[onchange*="handleToggleChange"][onchange*="dark"]');
592
+ if (toggle) {
593
+ toggle.checked = darkModeEnabled;
594
+ }
595
+ } catch (e) {
596
+ console.warn('Could not set dark mode toggle state.');
597
+ }
598
  }
599
 
600
+ // フッターナビゲーションのアクティブ状態設定
601
  updateFooterNavActiveState(pathname);
602
  });
603
 
 
605
  /**
606
  * フッターナビゲーションのアクティブ状態を更新
607
  */
608
+ function updateFooterNavActiveState(currentPath) {
609
  const footerNav = document.querySelector('.footer-nav');
610
  if (!footerNav) return;
611
 
 
614
  button.classList.remove('active'); // Reset all
615
  const onclickAttr = button.getAttribute('onclick');
616
  if (onclickAttr) {
617
+ // パスの末尾が一致するか、またはルートパスの場合を確認
618
+ if ((currentPath === '/' || currentPath.endsWith('/input') || currentPath.endsWith('/input.html')) && onclickAttr.includes('goToInput')) {
619
  button.classList.add('active');
620
+ } else if ((currentPath.endsWith('/history') || currentPath.endsWith('/history.html')) && onclickAttr.includes('goToHistory')) {
621
  button.classList.add('active');
622
+ } else if ((currentPath.endsWith('/settings') || currentPath.endsWith('/settings.html')) && onclickAttr.includes('goToSettings')) {
623
  button.classList.add('active');
624
  }
625
+ // learning ページの場合はどのフッターもアクティブにしない (任意)
626
+ else if ((currentPath.endsWith('/learning') || currentPath.endsWith('/learning.html'))) {
627
+ // 何もしない(アクティブにならない)
628
+ }
629
  }
630
  });
631
  }
632
 
633
 
634
  // デバッグ用に一部関数をグローバルスコープに公開(開発中のみ推奨)
635
+ // 本番環境では削除またはコメントアウトすること
636
  window.debug = {
637
  navigateTo,
638
  goToInput,
 
642
  openMenu,
643
  handleGenerateSubmit,
644
  initializeLearningScreen,
645
+ handleOptionClick, // handleOptionClick を公開
646
  revealAnswer,
647
  goToNext,
648
  goToPrev,
649
  handleToggleChange,
650
+ handleLogout,
651
+ showCorrectEffect, // デバッグ用にエフェクト表示関数も公開
652
+ hideCorrectEffect, // デバッグ用にエフェクト非表示関数も公開
653
+ learningData, // 現在の学習データ確認用
654
+ currentItemIndex // 現在のインデックス確認用
655
+ };
656
+
657
+ // --- END OF FILE script.js ---
static/style.css CHANGED
@@ -1,3 +1,5 @@
 
 
1
  /* 基本スタイル */
2
  body {
3
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
@@ -21,6 +23,7 @@ body {
21
  display: flex;
22
  flex-direction: column; /* 子要素を縦に並べる */
23
  overflow: hidden; /* はみ出しを隠す */
 
24
  }
25
 
26
  .header {
@@ -39,23 +42,36 @@ body {
39
  font-size: 24px;
40
  cursor: pointer;
41
  padding: 5px;
 
 
 
 
42
  }
43
 
 
44
  .header .action-btn {
45
- width: 30px;
 
 
46
  height: 30px;
47
- background-color: #007bff; /* 青い丸 */
48
- border-radius: 50%;
49
- padding: 0; /* 内側の余白を削除 */
 
50
  /* 必要ならアイコンや文字を配置 */
51
  }
52
 
 
53
  .header .title {
54
  font-size: 18px;
55
  font-weight: bold;
56
  margin: 0;
57
  text-align: center;
58
  flex-grow: 1; /* 中央に配置するために */
 
 
 
 
59
  }
60
 
61
  main {
@@ -104,6 +120,7 @@ li.list-item:last-child {
104
  align-items: center;
105
  flex-grow: 1; /* テキスト部分が幅を取るように */
106
  margin-right: 10px; /* 矢印とのスペース */
 
107
  }
108
 
109
  .list-item-icon {
@@ -112,8 +129,14 @@ li.list-item:last-child {
112
  color: #007bff; /* アイコンの色 */
113
  width: 24px; /* 幅を固定して揃える */
114
  text-align: center;
 
 
 
 
 
115
  }
116
 
 
117
  .list-item-text h3 {
118
  font-size: 16px;
119
  margin: 0 0 4px 0;
@@ -127,6 +150,9 @@ li.list-item:last-child {
127
  font-size: 13px;
128
  color: #777;
129
  margin: 0;
 
 
 
130
  }
131
 
132
  .list-arrow {
@@ -148,18 +174,26 @@ li.list-item:last-child {
148
  display: flex;
149
  flex-direction: column;
150
  justify-content: center;
 
151
  cursor: pointer; /* タップ可能を示す */
 
 
 
 
152
  }
 
153
  .card p.main-text {
154
  margin: 0;
155
  line-height: 1.7; /* 行間調整 */
156
  font-size: 16px;
 
157
  }
158
  .card p.answer-text {
159
  margin-top: 15px;
160
  font-size: 18px;
161
  font-weight: bold;
162
  color: #007bff;
 
163
  }
164
  .tap-to-show {
165
  text-align: center;
@@ -167,6 +201,7 @@ li.list-item:last-child {
167
  font-size: 14px;
168
  margin-top: 10px;
169
  cursor: pointer; /* タップ可能を示す */
 
170
  }
171
 
172
  /* Pagination */
@@ -184,7 +219,13 @@ li.list-item:last-child {
184
  color: #007bff;
185
  cursor: pointer;
186
  padding: 5px 15px; /* タップしやすく */
 
 
187
  }
 
 
 
 
188
  .pagination button:disabled {
189
  color: #ccc;
190
  cursor: default;
@@ -213,6 +254,7 @@ li.list-item:last-child {
213
  width: 50px;
214
  height: 24px;
215
  cursor: pointer;
 
216
  }
217
  .toggle-switch input {
218
  opacity: 0;
@@ -294,27 +336,25 @@ input:checked + .slider:before {
294
  align-items: center;
295
  justify-content: center;
296
  margin: 0 auto 25px auto;
297
- transition: background-color 0.2s;
298
  }
299
  .input-area .generate-button:hover {
300
  background-color: #e03024;
301
  }
302
  .input-area .generate-button:disabled {
303
- background-color: #fca9a4;
 
304
  cursor: not-allowed;
305
  }
 
 
 
306
  .input-area .generate-button .icon { /* 再生ボタン風アイコン */
307
  margin-left: 8px;
308
  font-size: 14px;
309
  }
310
  .input-area .loading-spinner {
311
- border: 3px solid rgba(255, 255, 255, 0.3);
312
- border-radius: 50%;
313
- border-top-color: #fff;
314
- width: 16px;
315
- height: 16px;
316
- animation: spin 1s linear infinite;
317
- margin-right: 8px; /* テキストとのスペース */
318
  }
319
  @keyframes spin {
320
  to { transform: rotate(360deg); }
@@ -347,6 +387,8 @@ input:checked + .slider:before {
347
  margin-top: -15px; /* ボタンとの間を詰める */
348
  margin-bottom: 15px;
349
  min-height: 1em; /* エラーなくても高さを確保 */
 
 
350
  }
351
  /* static/style.css に追加 */
352
 
@@ -360,9 +402,9 @@ input:checked + .slider:before {
360
  border-top: 1px solid #e0e0e0;
361
  display: flex;
362
  justify-content: space-around;
363
- padding: 4px 0 8px 0; /* 上下少し調整 */
364
  box-shadow: 0 -1px 4px rgba(0,0,0,0.08);
365
- z-index: 1000;
366
  }
367
 
368
  .footer-nav button {
@@ -378,6 +420,9 @@ input:checked + .slider:before {
378
  flex-grow: 1; /* ボタンが均等に幅を取るように */
379
  transition: color 0.2s ease;
380
  }
 
 
 
381
 
382
  .footer-nav .nav-icon {
383
  font-size: 22px;
@@ -391,21 +436,26 @@ input:checked + .slider:before {
391
  /* --- main要素の底上げ --- */
392
  /* フッターにコンテンツが隠れないように */
393
  main {
394
- padding-bottom: 70px; /* フッターの高さに応じて調整 */
395
  }
396
 
397
- /* --- ローディングスピナー (input.html用) --- */
398
  .loading-spinner {
399
- border: 3px solid rgba(0, 0, 0, 0.1);
400
- border-left-color: #fff; /* スピナーの色 */
401
  border-radius: 50%;
402
  width: 16px;
403
  height: 16px;
404
  animation: spin 1s linear infinite;
405
  display: inline-block; /* ボタン内で表示 */
406
- margin-left: 8px; /* ボタンテキストとの間隔 */
407
  vertical-align: middle;
408
  }
 
 
 
 
 
409
 
410
  @keyframes spin {
411
  0% { transform: rotate(0deg); }
@@ -413,53 +463,138 @@ main {
413
  }
414
 
415
  /* --- learning.html のローディング(簡易版)--- */
416
- /* 必要であれば .loading-spinner-large などを定義 */
417
-
 
 
418
 
419
  /* --- learning.html の解答ボタン スタイル --- */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
420
  .option-button.correct {
421
  background-color: #d4edda; /* 緑系 */
422
  color: #155724;
423
  border-color: #c3e6cb;
 
424
  }
425
 
426
- .option-button.incorrect { /* これは revealAnswer では使わないかも */
427
  background-color: #f8d7da; /* 赤系 */
428
  color: #721c24;
429
  border-color: #f5c6cb;
 
430
  }
431
 
432
- .option-button.disabled {
 
 
 
 
 
 
 
 
 
 
 
433
  background-color: #e9ecef;
434
  color: #6c757d;
435
  border-color: #ced4da;
436
- opacity: 0.7;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
437
  }
438
 
 
 
 
 
 
 
 
 
439
 
440
  /* --- ダークモード用スタイル (一部) --- */
441
  body.dark-mode {
442
  background-color: #121212;
443
  color: #e0e0e0;
444
  }
445
- body.dark-mode .screen { background-color: #1e1e1e; }
446
  body.dark-mode .header { background-color: #1f1f1f; border-bottom-color: #333; }
447
- body.dark-mode .header .title, body.dark-mode .header .menu-btn { color: #e0e0e0; }
448
  body.dark-mode .footer-nav { background-color: #1f1f1f; border-top-color: #333; }
449
  body.dark-mode .footer-nav button { color: #888; }
450
  body.dark-mode .footer-nav button.active { color: #58a6ff; } /* ダークモードでのアクティブ色 */
451
  body.dark-mode .card { background-color: #2c2c2c; border-color: #444; color: #e0e0e0; }
452
- body.dark-mode .list-item-button { background-color: #2c2c2c; border-bottom-color: #444; }
453
- body.dark-mode .list-item-text h3, body.dark-mode .list-item-text p { color: #e0e0e0; }
 
 
 
454
  body.dark-mode .list-arrow { color: #aaa; }
455
  body.dark-mode .settings-item span { color: #e0e0e0; }
456
  body.dark-mode .section-title { color: #aaa; }
457
  body.dark-mode input[type="text"] { background-color: #333; border-color: #555; color: #e0e0e0; }
458
- body.dark-mode .generate-button { background-color: #3081d8; color: white; }
 
 
 
459
  body.dark-mode .option-button { background-color: #444; color: #e0e0e0; border-color: #666; }
 
 
460
  body.dark-mode .option-button.correct { background-color: #2a6831; color: #e0e0e0; border-color: #41984b; }
461
- body.dark-mode .option-button.disabled { background-color: #333; color: #888; border-color: #555; opacity: 0.7; }
 
 
 
462
  body.dark-mode .pagination button:disabled { color: #666; }
 
 
463
  body.dark-mode .toggle-switch .slider { background-color: #555; }
464
  body.dark-mode .toggle-switch input:checked + .slider { background-color: #58a6ff; }
465
- /* 他の要素も必要に応じてダークモードスタイルを追加 */
 
 
 
 
1
+ /* --- START OF FILE style.css --- */
2
+
3
  /* 基本スタイル */
4
  body {
5
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
 
23
  display: flex;
24
  flex-direction: column; /* 子要素を縦に並べる */
25
  overflow: hidden; /* はみ出しを隠す */
26
+ position: relative; /* Correct Effectの基準になるかも */
27
  }
28
 
29
  .header {
 
42
  font-size: 24px;
43
  cursor: pointer;
44
  padding: 5px;
45
+ color: #007bff; /* ボタンの色も少し設定 */
46
+ }
47
+ .header .menu-btn:active, .header .action-btn:active {
48
+ opacity: 0.7;
49
  }
50
 
51
+
52
  .header .action-btn {
53
+ /* 右上のボタンのデザイン例 (不要なら削除) */
54
+ /* visibility: hidden; */ /* 表示しない場合はこれ */
55
+ width: 30px; /* サイズ調整 */
56
  height: 30px;
57
+ color: #007bff; /* アイコンの色 */
58
+ /* background-color: #007bff; */ /* 背景色なし */
59
+ /* border-radius: 50%; */
60
+ /* padding: 0; */
61
  /* 必要ならアイコンや文字を配置 */
62
  }
63
 
64
+
65
  .header .title {
66
  font-size: 18px;
67
  font-weight: bold;
68
  margin: 0;
69
  text-align: center;
70
  flex-grow: 1; /* 中央に配置するために */
71
+ padding: 0 5px; /* 左右ボタンとの間隔 */
72
+ white-space: nowrap;
73
+ overflow: hidden;
74
+ text-overflow: ellipsis;
75
  }
76
 
77
  main {
 
120
  align-items: center;
121
  flex-grow: 1; /* テキスト部分が幅を取るように */
122
  margin-right: 10px; /* 矢印とのスペース */
123
+ overflow: hidden; /* はみ出し防止 */
124
  }
125
 
126
  .list-item-icon {
 
129
  color: #007bff; /* アイコンの色 */
130
  width: 24px; /* 幅を固定して揃える */
131
  text-align: center;
132
+ flex-shrink: 0;
133
+ }
134
+
135
+ .list-item-text {
136
+ overflow: hidden; /* はみ出し防止 */
137
  }
138
 
139
+
140
  .list-item-text h3 {
141
  font-size: 16px;
142
  margin: 0 0 4px 0;
 
150
  font-size: 13px;
151
  color: #777;
152
  margin: 0;
153
+ white-space: nowrap;
154
+ overflow: hidden;
155
+ text-overflow: ellipsis;
156
  }
157
 
158
  .list-arrow {
 
174
  display: flex;
175
  flex-direction: column;
176
  justify-content: center;
177
+ align-items: center; /* 中央揃え */
178
  cursor: pointer; /* タップ可能を示す */
179
+ transition: background-color 0.2s; /* 背景色変更アニメーション */
180
+ }
181
+ .card:active {
182
+ background-color: #f0f0f0; /* タップ時のフィードバック */
183
  }
184
+
185
  .card p.main-text {
186
  margin: 0;
187
  line-height: 1.7; /* 行間調整 */
188
  font-size: 16px;
189
+ max-width: 100%; /* はみ出し防止 */
190
  }
191
  .card p.answer-text {
192
  margin-top: 15px;
193
  font-size: 18px;
194
  font-weight: bold;
195
  color: #007bff;
196
+ max-width: 100%; /* はみ出し防止 */
197
  }
198
  .tap-to-show {
199
  text-align: center;
 
201
  font-size: 14px;
202
  margin-top: 10px;
203
  cursor: pointer; /* タップ可能を示す */
204
+ padding: 5px; /* タップしやすく */
205
  }
206
 
207
  /* Pagination */
 
219
  color: #007bff;
220
  cursor: pointer;
221
  padding: 5px 15px; /* タップしやすく */
222
+ border-radius: 8px; /* 視覚的フィードバック用 */
223
+ transition: background-color 0.2s;
224
  }
225
+ .pagination button:active:not(:disabled) {
226
+ background-color: rgba(0, 123, 255, 0.1); /* タップフィードバック */
227
+ }
228
+
229
  .pagination button:disabled {
230
  color: #ccc;
231
  cursor: default;
 
254
  width: 50px;
255
  height: 24px;
256
  cursor: pointer;
257
+ flex-shrink: 0; /* 縮まないように */
258
  }
259
  .toggle-switch input {
260
  opacity: 0;
 
336
  align-items: center;
337
  justify-content: center;
338
  margin: 0 auto 25px auto;
339
+ transition: background-color 0.2s, opacity 0.2s;
340
  }
341
  .input-area .generate-button:hover {
342
  background-color: #e03024;
343
  }
344
  .input-area .generate-button:disabled {
345
+ background-color: #ff3b30; /* 色は変えず */
346
+ opacity: 0.6; /* 薄くする */
347
  cursor: not-allowed;
348
  }
349
+ .input-area .generate-button .button-text {
350
+ /* display: inline-block; */ /* 縦中央揃えのためFlexで十分 */
351
+ }
352
  .input-area .generate-button .icon { /* 再生ボタン風アイコン */
353
  margin-left: 8px;
354
  font-size: 14px;
355
  }
356
  .input-area .loading-spinner {
357
+ /* Spinnerは下の共通スタイルで定義 */
 
 
 
 
 
 
358
  }
359
  @keyframes spin {
360
  to { transform: rotate(360deg); }
 
387
  margin-top: -15px; /* ボタンとの間を詰める */
388
  margin-bottom: 15px;
389
  min-height: 1em; /* エラーなくても高さを確保 */
390
+ text-align: center; /* 中央揃え */
391
+ padding: 0 15px; /* 左右に余白 */
392
  }
393
  /* static/style.css に追加 */
394
 
 
402
  border-top: 1px solid #e0e0e0;
403
  display: flex;
404
  justify-content: space-around;
405
+ padding: 4px 0 calc(env(safe-area-inset-bottom, 0px) + 8px) 0; /* iPhone下部対応 + 少し余白 */
406
  box-shadow: 0 -1px 4px rgba(0,0,0,0.08);
407
+ z-index: 100; /* Correct Effectより下 */
408
  }
409
 
410
  .footer-nav button {
 
420
  flex-grow: 1; /* ボタンが均等に幅を取るように */
421
  transition: color 0.2s ease;
422
  }
423
+ .footer-nav button:active {
424
+ opacity: 0.7;
425
+ }
426
 
427
  .footer-nav .nav-icon {
428
  font-size: 22px;
 
436
  /* --- main要素の底上げ --- */
437
  /* フッターにコンテンツが隠れないように */
438
  main {
439
+ padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 80px); /* フッターの高さ + iPhone下部 + 余白 */
440
  }
441
 
442
+ /* --- ローディングスピナー (共通) --- */
443
  .loading-spinner {
444
+ border: 3px solid rgba(0, 0, 0, 0.1); /* generate button以外でのデフォルト */
445
+ border-left-color: #007bff; /* デフォルトの色 */
446
  border-radius: 50%;
447
  width: 16px;
448
  height: 16px;
449
  animation: spin 1s linear infinite;
450
  display: inline-block; /* ボタン内で表示 */
451
+ margin-right: 8px; /* ボタンテキストとの間隔 */
452
  vertical-align: middle;
453
  }
454
+ /* input.html の generate-button 内のスピナー */
455
+ .generate-button .loading-spinner {
456
+ border-color: rgba(255, 255, 255, 0.3);
457
+ border-left-color: #fff;
458
+ }
459
 
460
  @keyframes spin {
461
  0% { transform: rotate(0deg); }
 
463
  }
464
 
465
  /* --- learning.html のローディング(簡易版)--- */
466
+ /* 必要であれば .loading-indicator-large などを定義 */
467
+ #mode-indicator.loading {
468
+ color: #888;
469
+ }
470
 
471
  /* --- learning.html の解答ボタン スタイル --- */
472
+ .option-button {
473
+ display: block;
474
+ width: calc(100% - 20px); /* 左右の余白を考慮 */
475
+ margin: 10px auto; /* 中央寄せ */
476
+ padding: 15px;
477
+ font-size: 16px;
478
+ border: 1px solid #ccc;
479
+ border-radius: 8px;
480
+ background-color: #fff;
481
+ color: #333;
482
+ cursor: pointer;
483
+ transition: background-color 0.2s, border-color 0.2s, color 0.2s, opacity 0.2s;
484
+ text-align: left; /* テキスト左寄せ */
485
+ }
486
+ .option-button:hover:not(:disabled) {
487
+ background-color: #f0f0f0;
488
+ }
489
+ .option-button:active:not(:disabled) {
490
+ background-color: #e0e0e0;
491
+ }
492
+
493
+
494
  .option-button.correct {
495
  background-color: #d4edda; /* 緑系 */
496
  color: #155724;
497
  border-color: #c3e6cb;
498
+ font-weight: bold; /* 正解を強調 */
499
  }
500
 
501
+ .option-button.incorrect { /* 不正解を選んだ場合のスタイル */
502
  background-color: #f8d7da; /* 赤系 */
503
  color: #721c24;
504
  border-color: #f5c6cb;
505
+ opacity: 0.8; /* 少し薄く */
506
  }
507
 
508
+ .option-button:disabled { /* 正解・不正解表示後の状態 */
509
+ cursor: default;
510
+ opacity: 0.7; /* すべての無効ボタンを少し薄く */
511
+ }
512
+ .option-button.correct:disabled {
513
+ opacity: 1; /* 正解ボタンは薄くしない */
514
+ }
515
+ .option-button.incorrect:disabled {
516
+ opacity: 0.7; /* 不正解ボタンは薄くする */
517
+ }
518
+ /* 選ばれなかった他の選択肢(不正解)のスタイル */
519
+ .option-button.other-disabled:disabled {
520
  background-color: #e9ecef;
521
  color: #6c757d;
522
  border-color: #ced4da;
523
+ opacity: 0.6;
524
+ }
525
+
526
+
527
+ /* --- ★★★ 正解エフェクトのスタイルを追加 ★★★ --- */
528
+ .correct-effect {
529
+ position: fixed; /* 画面に固定 */
530
+ top: 45%; /* 少し上に調整 (フッターを考慮) */
531
+ left: 50%;
532
+ transform: translate(-50%, -50%) scale(0.8); /* 中央揃え & 少し小さめから開始 */
533
+ font-size: 15rem; /* 〇の大きさ */
534
+ color: rgba(0, 190, 0, 0.8); /* 〇の色 (例: 少し透明な明るい緑) */
535
+ font-weight: bold;
536
+ display: none; /* 初期状態では非表示 */
537
+ align-items: center;
538
+ justify-content: center;
539
+ z-index: 1001; /* フッターより手前に表示 */
540
+ opacity: 0; /* 初期状態では透明 */
541
+ transition: opacity 0.3s ease-out, transform 0.3s ease-out; /* フェードインと拡大のアニメーション */
542
+ pointer-events: none; /* エフェクトがクリック操作を妨げないように */
543
+ width: 20rem; /* 幅と高さを指定 */
544
+ height: 20rem;
545
+ /* background-color: rgba(255, 255, 255, 0.1); */ /* 必要なら背景 */
546
+ /* border-radius: 50%; */ /* 円形背景にしたい場合 */
547
+ line-height: 1; /* 〇の縦位置調整 */
548
  }
549
 
550
+ /* エフェクト表示時のスタイル */
551
+ .correct-effect.show {
552
+ display: flex; /* 表示状態にする (flexで中央揃え) */
553
+ opacity: 1; /* 不透明にする */
554
+ transform: translate(-50%, -50%) scale(1); /* 元のサイズに戻す */
555
+ }
556
+ /* --- ★★★ ここまで追加 ★★★ --- */
557
+
558
 
559
  /* --- ダークモード用スタイル (一部) --- */
560
  body.dark-mode {
561
  background-color: #121212;
562
  color: #e0e0e0;
563
  }
564
+ body.dark-mode .screen { background-color: #1e1e1e; border-color: #333; }
565
  body.dark-mode .header { background-color: #1f1f1f; border-bottom-color: #333; }
566
+ body.dark-mode .header .title, body.dark-mode .header .menu-btn, body.dark-mode .header .action-btn { color: #e0e0e0; }
567
  body.dark-mode .footer-nav { background-color: #1f1f1f; border-top-color: #333; }
568
  body.dark-mode .footer-nav button { color: #888; }
569
  body.dark-mode .footer-nav button.active { color: #58a6ff; } /* ダークモードでのアクティブ色 */
570
  body.dark-mode .card { background-color: #2c2c2c; border-color: #444; color: #e0e0e0; }
571
+ body.dark-mode .card:active { background-color: #3a3a3a; }
572
+ body.dark-mode .list-item-button:hover { background-color: #3a3a3a; }
573
+ body.dark-mode li.list-item { border-bottom-color: #444; }
574
+ body.dark-mode .list-item-text h3 { color: #e0e0e0; }
575
+ body.dark-mode .list-item-text p { color: #aaa; }
576
  body.dark-mode .list-arrow { color: #aaa; }
577
  body.dark-mode .settings-item span { color: #e0e0e0; }
578
  body.dark-mode .section-title { color: #aaa; }
579
  body.dark-mode input[type="text"] { background-color: #333; border-color: #555; color: #e0e0e0; }
580
+ body.dark-mode .generate-button { background-color: #e53e3e; color: white; } /* 少し調整した赤 */
581
+ body.dark-mode .generate-button:hover { background-color: #c53030; }
582
+ body.dark-mode .generate-button:disabled { background-color: #e53e3e; opacity: 0.6; }
583
+ body.dark-mode .error-message { color: #ff7f7f; }
584
  body.dark-mode .option-button { background-color: #444; color: #e0e0e0; border-color: #666; }
585
+ body.dark-mode .option-button:hover:not(:disabled) { background-color: #555; }
586
+ body.dark-mode .option-button:active:not(:disabled) { background-color: #666; }
587
  body.dark-mode .option-button.correct { background-color: #2a6831; color: #e0e0e0; border-color: #41984b; }
588
+ body.dark-mode .option-button.incorrect { background-color: #8b3a3e; color: #ffdddd; border-color: #a85055; opacity: 0.8; }
589
+ body.dark-mode .option-button.other-disabled:disabled { background-color: #333; color: #888; border-color: #555; opacity: 0.6; }
590
+ body.dark-mode .pagination button { color: #58a6ff; }
591
+ body.dark-mode .pagination button:active:not(:disabled) { background-color: rgba(88, 166, 255, 0.15); }
592
  body.dark-mode .pagination button:disabled { color: #666; }
593
+ body.dark-mode .tap-to-show { color: #aaa; }
594
+ body.dark-mode .mode-indicator { color: #bbb; }
595
  body.dark-mode .toggle-switch .slider { background-color: #555; }
596
  body.dark-mode .toggle-switch input:checked + .slider { background-color: #58a6ff; }
597
+ /* ダークモードの正解エフェクトの色 */
598
+ body.dark-mode .correct-effect { color: rgba(50, 220, 50, 0.85); }
599
+
600
+ /* --- END OF FILE style.css --- */
templates/learning.html CHANGED
@@ -11,6 +11,7 @@
11
  <header class="header">
12
  <button class="menu-btn" aria-label="メニュー" onclick="openMenu()">☰</button>
13
  <h1 class="title" id="learning-title">学習セット: 面白い動画の分析</h1>
 
14
  <button class="action-btn" aria-label="アクション"></button>
15
  </header>
16
  <main>
@@ -23,37 +24,43 @@
23
 
24
  <p class="tap-to-show" id="tap-to-show" onclick="revealAnswer()">タップして解答を表示</p>
25
 
 
26
  <div id="options-area" style="margin-top: 15px;">
27
- <!-- クイズの選択肢がJSでここに追加されます -->
 
 
 
 
28
  </div>
29
 
30
  <div class="pagination">
31
  <button id="prev-button" aria-label="前へ" onclick="goToPrev()"><</button>
32
- <span id="page-info">5 / 10</span>
33
  <button id="next-button" aria-label="次へ" onclick="goToNext()">></button>
34
  </div>
35
  </main>
36
  </div>
37
 
38
- <!-- ここから追加 -->
39
  <div id="correct-effect" class="correct-effect">〇</div>
40
- <!-- ここまで追加 -->
41
 
42
  <script src="{{ url_for('static', filename='script.js') }}"></script>
43
 
 
44
  <footer class="footer-nav">
45
- <button onclick="goToInput()" aria-label="入力">
46
- <span class="nav-icon">➕</span>
47
- <span class="nav-text">入力</span>
48
- </button>
49
- <button onclick="goToHistory()" aria-label="履歴">
50
- <span class="nav-icon">🕒</span>
51
- <span class="nav-text">履歴</span>
52
- </button>
53
- <button onclick="goToSettings()" aria-label="設定">
54
- <span class="nav-icon">⚙️</span>
55
- <span class="nav-text">設定</span>
56
- </button>
57
  </footer>
58
  </body>
59
  </html>
 
11
  <header class="header">
12
  <button class="menu-btn" aria-label="メニュー" onclick="openMenu()">☰</button>
13
  <h1 class="title" id="learning-title">学習セット: 面白い動画の分析</h1>
14
+ <!-- 右上のボタンは不要なら削除 -->
15
  <button class="action-btn" aria-label="アクション"></button>
16
  </header>
17
  <main>
 
24
 
25
  <p class="tap-to-show" id="tap-to-show" onclick="revealAnswer()">タップして解答を表示</p>
26
 
27
+ <!-- クイズの選択肢表示エリア (JSで動的に生成) -->
28
  <div id="options-area" style="margin-top: 15px;">
29
+ <!-- 例:
30
+ <button class="option-button">A: 選択肢A</button>
31
+ <button class="option-button">B: 選択肢B</button>
32
+ ...
33
+ -->
34
  </div>
35
 
36
  <div class="pagination">
37
  <button id="prev-button" aria-label="前へ" onclick="goToPrev()"><</button>
38
+ <span id="page-info">? / ?</span> <!-- 初期表示 -->
39
  <button id="next-button" aria-label="次へ" onclick="goToNext()">></button>
40
  </div>
41
  </main>
42
  </div>
43
 
44
+ <!-- ★★★ ここから追加 ★★★ -->
45
  <div id="correct-effect" class="correct-effect">〇</div>
46
+ <!-- ★★★ ここまで追加 ★★★ -->
47
 
48
  <script src="{{ url_for('static', filename='script.js') }}"></script>
49
 
50
+ <!-- 例: フッターナビゲーション -->
51
  <footer class="footer-nav">
52
+ <button onclick="goToInput()" aria-label="入力">
53
+ <span class="nav-icon">➕</span> <!-- アイコンは好みで変更 -->
54
+ <span class="nav-text">入力</span>
55
+ </button>
56
+ <button onclick="goToHistory()" aria-label="履歴">
57
+ <span class="nav-icon">🕒</span>
58
+ <span class="nav-text">履歴</span>
59
+ </button>
60
+ <button onclick="goToSettings()" aria-label="設定">
61
+ <span class="nav-icon">⚙️</span>
62
+ <span class="nav-text">設定</span>
63
+ </button>
64
  </footer>
65
  </body>
66
  </html>