syurein
commited on
Commit
·
a09ca12
1
Parent(s):
016f062
First
Browse files- static/script.js +474 -273
- static/style.css +159 -1
- templates/history.html +14 -0
- templates/input.html +14 -0
- templates/learning.html +14 -0
- templates/settings.html +14 -0
static/script.js
CHANGED
@@ -3,65 +3,109 @@
|
|
3 |
"use strict"; // より厳格なエラーチェック
|
4 |
|
5 |
// --- グローバル変数 ---
|
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 |
|
14 |
/**
|
15 |
-
* 指定されたURL
|
16 |
* @param {string} url - 遷移先のURL
|
17 |
*/
|
18 |
function navigateTo(url) {
|
19 |
-
|
|
|
|
|
|
|
|
|
20 |
}
|
21 |
|
22 |
/**
|
23 |
-
*
|
24 |
-
* TODO: 実際のメニューUIを実装する
|
25 |
*/
|
26 |
function openMenu() {
|
27 |
-
console.log("
|
28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
38 |
-
if (!generateButton) return;
|
39 |
|
40 |
-
|
41 |
-
|
|
|
|
|
42 |
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
|
|
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 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
if (
|
64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
65 |
}
|
66 |
}
|
67 |
|
@@ -74,13 +118,19 @@ function displayErrorMessage(message, elementId = 'error-message') {
|
|
74 |
const errorElement = document.getElementById(elementId);
|
75 |
if (errorElement) {
|
76 |
errorElement.textContent = message;
|
|
|
77 |
errorElement.style.display = message ? 'block' : 'none';
|
|
|
|
|
|
|
|
|
|
|
78 |
}
|
79 |
}
|
80 |
|
81 |
// --- 画面遷移用関数 ---
|
82 |
function goToInput() {
|
83 |
-
navigateTo('/
|
84 |
}
|
85 |
|
86 |
function goToHistory() {
|
@@ -93,10 +143,12 @@ function goToSettings() {
|
|
93 |
|
94 |
function goToLearning(contentId) {
|
95 |
if (contentId) {
|
|
|
96 |
navigateTo(`/learning?id=${encodeURIComponent(contentId)}`);
|
97 |
} else {
|
98 |
console.error('goToLearning requires a content ID.');
|
99 |
-
|
|
|
100 |
}
|
101 |
}
|
102 |
|
@@ -109,96 +161,118 @@ function goToLearning(contentId) {
|
|
109 |
async function handleGenerateSubmit() {
|
110 |
const urlInput = document.getElementById('youtube-url');
|
111 |
const youtubeUrl = urlInput.value.trim();
|
112 |
-
|
|
|
113 |
|
114 |
if (!youtubeUrl) {
|
115 |
-
displayErrorMessage('YouTubeリンクを入力してください。');
|
116 |
-
return false;
|
117 |
}
|
118 |
|
119 |
-
// 簡単なURL
|
120 |
try {
|
121 |
-
const urlObj = new URL(youtubeUrl);
|
122 |
-
|
123 |
-
|
|
|
124 |
}
|
125 |
-
//
|
126 |
-
if (urlObj.hostname === 'youtu.be' &&
|
127 |
-
|
128 |
}
|
129 |
-
if (urlObj.hostname.includes('youtube.com')
|
130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
138 |
-
|
|
|
|
|
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', //
|
150 |
},
|
151 |
-
body: JSON.stringify({ url: youtubeUrl }),
|
152 |
});
|
153 |
|
154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
155 |
|
156 |
-
|
157 |
-
|
158 |
-
|
|
|
|
|
|
|
159 |
goToLearning(result.data.id);
|
160 |
} else {
|
161 |
-
//
|
162 |
-
console.error('Generation failed:', result);
|
163 |
-
//
|
164 |
-
|
165 |
-
|
|
|
166 |
}
|
167 |
|
168 |
} catch (error) {
|
|
|
169 |
console.error('Error during generation request:', error);
|
170 |
-
//
|
171 |
-
if (error
|
172 |
-
displayErrorMessage('
|
173 |
-
} else
|
174 |
-
displayErrorMessage(
|
175 |
-
}
|
176 |
-
else {
|
177 |
-
displayErrorMessage(`通信エラーが発生しました: ${error.message}`);
|
178 |
}
|
179 |
} finally {
|
180 |
-
toggleLoading(false, 'generate-button'); //
|
181 |
}
|
182 |
|
183 |
-
return false; //
|
184 |
}
|
185 |
|
186 |
|
187 |
// --- learning.html 用の処理 ---
|
188 |
|
189 |
/**
|
190 |
-
* learning.html
|
191 |
*/
|
192 |
async function initializeLearningScreen() {
|
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('
|
202 |
return;
|
203 |
}
|
204 |
console.log('Content ID:', contentId);
|
@@ -209,61 +283,68 @@ async function initializeLearningScreen() {
|
|
209 |
const response = await fetch(`/api/learning/${contentId}`);
|
210 |
if (!response.ok) {
|
211 |
// エラーレスポンスがJSON形式でない場合も考慮
|
212 |
-
let errorMessage =
|
213 |
try {
|
214 |
const errorData = await response.json();
|
215 |
errorMessage = errorData.message || errorMessage;
|
216 |
} catch (e) {
|
217 |
-
|
218 |
}
|
219 |
throw new Error(errorMessage);
|
220 |
}
|
221 |
|
222 |
-
|
223 |
-
|
|
|
224 |
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
if (titleElement) {
|
233 |
-
titleElement.textContent = learningData.title || '学習コンテンツ';
|
234 |
-
}
|
235 |
|
236 |
-
|
237 |
-
|
238 |
-
|
|
|
|
|
239 |
|
|
|
|
|
|
|
|
|
240 |
} else {
|
241 |
-
|
242 |
}
|
243 |
|
|
|
|
|
|
|
|
|
244 |
} catch (error) {
|
|
|
245 |
console.error('Error initializing learning screen:', error);
|
246 |
-
|
|
|
|
|
|
|
|
|
|
|
247 |
} finally {
|
248 |
toggleLoading(false); // ローディング表示終了
|
249 |
}
|
250 |
}
|
251 |
|
252 |
/**
|
253 |
-
* 現在の学習アイテムをカードに表示
|
254 |
*/
|
255 |
function displayCurrentItem() {
|
256 |
-
//
|
257 |
-
|
258 |
-
clearTimeout(correctEffectTimeout);
|
259 |
|
260 |
-
|
261 |
-
console.error('Invalid learning data or index');
|
262 |
-
displayLearningError('学習データを表示できません。');
|
263 |
-
return;
|
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');
|
@@ -271,188 +352,242 @@ function displayCurrentItem() {
|
|
271 |
const optionsArea = document.getElementById('options-area');
|
272 |
const modeIndicator = document.getElementById('mode-indicator');
|
273 |
|
274 |
-
//
|
275 |
-
cardTextElement
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
|
|
283 |
currentMode = 'quiz';
|
284 |
modeIndicator.textContent = 'クイズモード';
|
285 |
-
|
286 |
-
|
287 |
-
|
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 |
-
|
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) {
|
314 |
currentMode = 'summary';
|
315 |
modeIndicator.textContent = '要約モード';
|
316 |
-
//
|
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 |
-
|
325 |
-
|
326 |
-
|
327 |
-
|
|
|
|
|
|
|
328 |
tapToShowElement.style.display = 'none';
|
|
|
329 |
}
|
330 |
|
331 |
-
updatePagination();
|
332 |
}
|
333 |
|
334 |
-
|
|
|
|
|
|
|
335 |
function handleOptionClick(selectedOption) {
|
336 |
-
|
|
|
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
|
363 |
-
|
364 |
-
|
365 |
-
|
366 |
-
|
367 |
-
|
368 |
-
// 既に表示済みなら何もしない(二重実行防止)
|
369 |
-
if (answerTextElement && answerTextElement.style.display === 'block') {
|
370 |
-
return;
|
371 |
-
}
|
372 |
|
|
|
|
|
|
|
|
|
373 |
|
374 |
-
|
375 |
-
|
376 |
-
|
377 |
-
|
378 |
-
|
379 |
-
|
380 |
-
|
381 |
-
|
382 |
-
|
383 |
-
|
|
|
|
|
384 |
|
385 |
-
|
386 |
-
|
387 |
-
|
388 |
-
|
389 |
-
|
390 |
-
|
391 |
-
|
392 |
-
|
393 |
-
|
394 |
-
|
395 |
-
|
396 |
-
|
397 |
-
|
398 |
-
|
399 |
-
|
|
|
|
|
|
|
|
|
|
|
400 |
}
|
401 |
-
}
|
402 |
}
|
403 |
}
|
404 |
|
405 |
|
406 |
/**
|
407 |
-
*
|
408 |
*/
|
409 |
function goToNext() {
|
410 |
-
|
|
|
411 |
currentItemIndex++;
|
412 |
-
displayCurrentItem();
|
|
|
|
|
|
|
|
|
|
|
|
|
413 |
}
|
414 |
}
|
415 |
|
416 |
/**
|
417 |
-
*
|
418 |
*/
|
419 |
function goToPrev() {
|
420 |
-
|
|
|
421 |
currentItemIndex--;
|
422 |
-
displayCurrentItem();
|
|
|
|
|
423 |
}
|
424 |
}
|
425 |
|
426 |
/**
|
427 |
-
*
|
428 |
*/
|
429 |
function updatePagination() {
|
430 |
const pageInfo = document.getElementById('page-info');
|
431 |
const prevButton = document.getElementById('prev-button');
|
432 |
const nextButton = document.getElementById('next-button');
|
433 |
|
434 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
435 |
const totalItems = learningData.items.length;
|
436 |
-
|
437 |
-
|
438 |
-
|
439 |
-
|
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 |
-
|
449 |
-
|
450 |
-
|
451 |
}
|
452 |
}
|
453 |
|
454 |
/**
|
455 |
-
* learning.html
|
|
|
456 |
*/
|
457 |
function displayLearningError(message) {
|
458 |
const cardElement = document.getElementById('learning-card');
|
@@ -462,177 +597,240 @@ function displayLearningError(message) {
|
|
462 |
const modeIndicator = document.getElementById('mode-indicator');
|
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)
|
473 |
-
|
474 |
-
|
|
|
|
|
|
|
|
|
|
|
475 |
}
|
476 |
|
477 |
-
|
|
|
|
|
478 |
function showCorrectEffect() {
|
479 |
if (correctEffect) {
|
480 |
-
//
|
481 |
-
|
482 |
-
|
483 |
-
correctEffect.classList.add('show'); // 表示用クラスを追加
|
484 |
|
485 |
-
//
|
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 |
|
507 |
/**
|
508 |
-
*
|
|
|
|
|
509 |
*/
|
510 |
function handleToggleChange(checkbox, type) {
|
511 |
-
|
|
|
|
|
512 |
if (type === 'dark') {
|
513 |
-
|
|
|
|
|
514 |
try {
|
515 |
-
localStorage.setItem('darkModeEnabled',
|
|
|
516 |
} catch (e) {
|
517 |
-
console.warn('Could not save dark mode preference to localStorage
|
|
|
|
|
518 |
}
|
|
|
|
|
|
|
|
|
|
|
519 |
}
|
520 |
-
//
|
521 |
}
|
522 |
|
523 |
/**
|
524 |
-
*
|
525 |
*/
|
526 |
function handleLogout() {
|
527 |
-
console.log("Logout clicked");
|
528 |
-
// TODO:
|
529 |
-
|
530 |
-
//
|
531 |
-
//
|
532 |
-
|
533 |
-
|
|
|
|
|
|
|
534 |
}
|
535 |
|
536 |
/**
|
537 |
-
*
|
538 |
*/
|
539 |
function applyDarkModePreference() {
|
540 |
try {
|
|
|
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 |
}
|
549 |
}
|
550 |
|
551 |
|
552 |
// --- ページの初期化処理 ---
|
553 |
-
const pathname = window.location.pathname; //
|
554 |
|
555 |
-
//
|
556 |
applyDarkModePreference();
|
557 |
|
|
|
558 |
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
559 |
|
560 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
561 |
const form = document.getElementById('generate-form');
|
562 |
if (form) {
|
|
|
563 |
form.addEventListener('submit', (event) => {
|
564 |
-
event.preventDefault();
|
565 |
-
handleGenerateSubmit();
|
566 |
});
|
|
|
|
|
567 |
}
|
568 |
-
//
|
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 |
-
|
580 |
-
|
581 |
-
|
582 |
-
//
|
583 |
-
|
584 |
-
|
585 |
-
|
586 |
-
|
587 |
-
console.log("Settings page loaded.");
|
588 |
// ダークモードトグルの状態をlocalStorageに合わせて更新
|
589 |
try {
|
590 |
const darkModeEnabled = localStorage.getItem('darkModeEnabled') === 'true';
|
591 |
-
|
|
|
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 |
|
604 |
|
605 |
/**
|
606 |
-
*
|
607 |
*/
|
608 |
function updateFooterNavActiveState(currentPath) {
|
609 |
const footerNav = document.querySelector('.footer-nav');
|
610 |
-
if (!footerNav)
|
|
|
|
|
|
|
611 |
|
612 |
const buttons = footerNav.querySelectorAll('button');
|
|
|
|
|
613 |
buttons.forEach(button => {
|
614 |
-
button.classList.remove('active'); //
|
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,
|
@@ -640,18 +838,21 @@ window.debug = {
|
|
640 |
goToSettings,
|
641 |
goToLearning,
|
642 |
openMenu,
|
|
|
643 |
handleGenerateSubmit,
|
644 |
initializeLearningScreen,
|
645 |
-
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 ---
|
|
|
3 |
"use strict"; // より厳格なエラーチェック
|
4 |
|
5 |
// --- グローバル変数 ---
|
6 |
+
let learningData = null; // 学習データ (learning.html用) - { title: '...', items: [...] } の形式を想定
|
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 |
+
let sideMenu = null; // サイドメニュー要素
|
12 |
+
let menuOverlay = null; // メニューオーバーレイ要素
|
13 |
|
14 |
// --- 共通関数 ---
|
15 |
|
16 |
/**
|
17 |
+
* 指定されたURLに遷移します。遷移前にメニューを閉じます。
|
18 |
* @param {string} url - 遷移先のURL
|
19 |
*/
|
20 |
function navigateTo(url) {
|
21 |
+
closeMenu(); // 遷移前にメニューを閉じる
|
22 |
+
// 少し遅延させてから遷移する(メニューが閉じるアニメーションを見せるため、任意)
|
23 |
+
setTimeout(() => {
|
24 |
+
window.location.href = url;
|
25 |
+
}, 100); // 100ミリ秒後に遷移
|
26 |
}
|
27 |
|
28 |
/**
|
29 |
+
* サイドメニューを開きます。
|
|
|
30 |
*/
|
31 |
function openMenu() {
|
32 |
+
console.log("Opening menu...");
|
33 |
+
if (sideMenu && menuOverlay) {
|
34 |
+
sideMenu.classList.add('open');
|
35 |
+
menuOverlay.classList.add('open');
|
36 |
+
document.body.classList.add('menu-open'); // 背景スクロール禁止
|
37 |
+
} else {
|
38 |
+
console.error("Side menu or overlay element not found. Cannot open menu.");
|
39 |
+
}
|
40 |
+
}
|
41 |
+
|
42 |
+
/**
|
43 |
+
* サイドメニューを閉じます。
|
44 |
+
*/
|
45 |
+
function closeMenu() {
|
46 |
+
// console.log("Closing menu..."); // デバッグ時以外は不要かも
|
47 |
+
if (sideMenu && menuOverlay) {
|
48 |
+
sideMenu.classList.remove('open');
|
49 |
+
menuOverlay.classList.remove('open');
|
50 |
+
document.body.classList.remove('menu-open'); // 背景スクロール許可
|
51 |
+
}
|
52 |
+
// メニュー要素が見つからない場合のエラーログはここでは不要
|
53 |
}
|
54 |
|
55 |
+
|
56 |
/**
|
57 |
* ローディングスピナーを表示/非表示します。
|
58 |
* @param {boolean} show - trueで表示、falseで非表示
|
59 |
* @param {string} buttonId - 操作対象のボタンID (input.html用)
|
60 |
*/
|
61 |
function toggleLoading(show, buttonId = 'generate-button') {
|
62 |
+
const targetButton = document.getElementById(buttonId);
|
|
|
63 |
|
64 |
+
// input.html の generate-button の処理
|
65 |
+
if (targetButton && buttonId === 'generate-button') {
|
66 |
+
const spinner = targetButton.querySelector('.loading-spinner');
|
67 |
+
const buttonText = targetButton.querySelector('.button-text');
|
68 |
|
69 |
+
if (show) {
|
70 |
+
targetButton.disabled = true;
|
71 |
+
if (spinner) spinner.style.display = 'inline-block';
|
72 |
+
if (buttonText) buttonText.textContent = '生成中...';
|
73 |
+
} else {
|
74 |
+
targetButton.disabled = false;
|
75 |
+
if (spinner) spinner.style.display = 'none';
|
76 |
+
if (buttonText) buttonText.textContent = '生成する'; // 元のテキストに戻す
|
77 |
+
}
|
78 |
}
|
79 |
|
80 |
+
// learning.html 用の汎用ローディング表示 (mode-indicatorなどを利用)
|
81 |
const loadingIndicator = document.getElementById('mode-indicator');
|
82 |
const cardElement = document.getElementById('learning-card');
|
83 |
const paginationElement = document.querySelector('.pagination');
|
84 |
const optionsArea = document.getElementById('options-area');
|
85 |
+
const tapToShowElement = document.getElementById('tap-to-show'); // 追加
|
86 |
+
|
87 |
+
// 現在のパスがlearningページか確認
|
88 |
+
const currentPathname = window.location.pathname;
|
89 |
+
if (currentPathname.endsWith('/learning') || currentPathname.endsWith('/learning.html')) {
|
90 |
+
if (show) {
|
91 |
+
if (loadingIndicator) {
|
92 |
+
loadingIndicator.textContent = '読み込み中...';
|
93 |
+
loadingIndicator.classList.add('loading');
|
94 |
+
}
|
95 |
+
if (cardElement) cardElement.style.opacity = '0.5';
|
96 |
+
if (paginationElement) paginationElement.style.display = 'none';
|
97 |
+
if (optionsArea) optionsArea.style.display = 'none';
|
98 |
+
if (tapToShowElement) tapToShowElement.style.display = 'none'; // ローディング中は非表示
|
99 |
+
} else {
|
100 |
+
// ローディング終了時の表示は displayCurrentItem で制御されるため、
|
101 |
+
// ここではローディングインジケータのテキストクリア程度で良い
|
102 |
+
if (loadingIndicator) {
|
103 |
+
// loadingIndicator.textContent = ''; // displayCurrentItemでモードが表示される
|
104 |
+
loadingIndicator.classList.remove('loading');
|
105 |
+
}
|
106 |
+
if (cardElement) cardElement.style.opacity = '1';
|
107 |
+
// pagination, optionsArea, tapToShowElement の表示は displayCurrentItem に任せる
|
108 |
+
}
|
109 |
}
|
110 |
}
|
111 |
|
|
|
118 |
const errorElement = document.getElementById(elementId);
|
119 |
if (errorElement) {
|
120 |
errorElement.textContent = message;
|
121 |
+
// メッセージがあれば表示、なければ非表示 (display: block/none)
|
122 |
errorElement.style.display = message ? 'block' : 'none';
|
123 |
+
} else {
|
124 |
+
// エラー表示要素自体が見つからない場合
|
125 |
+
if (message) { // メッセージがある場合のみコンソールにエラー表示
|
126 |
+
console.error(`Error element with ID "${elementId}" not found. Cannot display message: ${message}`);
|
127 |
+
}
|
128 |
}
|
129 |
}
|
130 |
|
131 |
// --- 画面遷移用関数 ---
|
132 |
function goToInput() {
|
133 |
+
navigateTo('/'); // input.html はルートパスを想定
|
134 |
}
|
135 |
|
136 |
function goToHistory() {
|
|
|
143 |
|
144 |
function goToLearning(contentId) {
|
145 |
if (contentId) {
|
146 |
+
// navigateTo内でメニューは閉じられる
|
147 |
navigateTo(`/learning?id=${encodeURIComponent(contentId)}`);
|
148 |
} else {
|
149 |
console.error('goToLearning requires a content ID.');
|
150 |
+
// ユーザーへのフィードバック
|
151 |
+
alert('学習コンテンツのIDが見つかりません。履歴画面から再度お試しください。');
|
152 |
}
|
153 |
}
|
154 |
|
|
|
161 |
async function handleGenerateSubmit() {
|
162 |
const urlInput = document.getElementById('youtube-url');
|
163 |
const youtubeUrl = urlInput.value.trim();
|
164 |
+
const errorMsgElementId = 'error-message'; // エラーメッセージ表示用ID
|
165 |
+
displayErrorMessage('', errorMsgElementId); // 前のエラーメッセージをクリア
|
166 |
|
167 |
if (!youtubeUrl) {
|
168 |
+
displayErrorMessage('YouTubeリンクを入力してください。', errorMsgElementId);
|
169 |
+
return false; // 送信中断
|
170 |
}
|
171 |
|
172 |
+
// 簡単なURL形式チェックと YouTube ドメイン検証
|
173 |
try {
|
174 |
+
const urlObj = new URL(youtubeUrl); // URLオブジェクト生成試行
|
175 |
+
const validHostnames = ['www.youtube.com', 'youtube.com', 'youtu.be'];
|
176 |
+
if (!validHostnames.includes(urlObj.hostname)) {
|
177 |
+
throw new Error('Invalid hostname'); // YouTube以外のドメイン
|
178 |
}
|
179 |
+
// さらに詳細なパスチェック(例)
|
180 |
+
if (urlObj.hostname === 'youtu.be' && urlObj.pathname.length <= 1) {
|
181 |
+
throw new Error('Missing video ID for youtu.be'); // youtu.be/ の後にIDがない
|
182 |
}
|
183 |
+
if (urlObj.hostname.includes('youtube.com')) {
|
184 |
+
if (urlObj.pathname === '/watch' && !urlObj.searchParams.has('v')) {
|
185 |
+
throw new Error('Missing video ID parameter (v=...) for youtube.com/watch'); // vパラメータがない
|
186 |
+
}
|
187 |
+
if (urlObj.pathname.startsWith('/shorts/') && urlObj.pathname.length <= 8) {
|
188 |
+
throw new Error('Missing video ID for youtube.com/shorts/'); // shorts/ の後にIDがない
|
189 |
+
}
|
190 |
+
// 他の有効な形式があれば追加 (例: /live/ など)
|
191 |
}
|
|
|
|
|
|
|
|
|
192 |
} catch (e) {
|
193 |
+
// URLパースエラーまたは上記バリデーションエラー
|
194 |
+
console.warn("Invalid URL format:", e.message);
|
195 |
+
displayErrorMessage('有効なYouTube動画のリンクを入力してください。(例: https://www.youtube.com/watch?v=...)', errorMsgElementId);
|
196 |
+
return false; // 送信中断
|
197 |
}
|
198 |
|
199 |
|
200 |
toggleLoading(true, 'generate-button'); // ローディング開始
|
201 |
|
202 |
try {
|
203 |
+
// APIエンドポイント '/api/generate' へPOSTリクエスト
|
204 |
const response = await fetch('/api/generate', {
|
205 |
method: 'POST',
|
206 |
headers: {
|
207 |
'Content-Type': 'application/json',
|
208 |
+
'Accept': 'application/json', // サーバーからのJSON応答を期待
|
209 |
},
|
210 |
+
body: JSON.stringify({ url: youtubeUrl }), // URLをJSON形式で送信
|
211 |
});
|
212 |
|
213 |
+
// レスポンスボディをJSONとしてパース試行
|
214 |
+
let result;
|
215 |
+
try {
|
216 |
+
result = await response.json();
|
217 |
+
} catch (jsonError) {
|
218 |
+
// JSONパース失敗
|
219 |
+
console.error('Failed to parse JSON response:', jsonError);
|
220 |
+
// サーバーエラーの可能性が高いが、レスポンスステータスも確認
|
221 |
+
if (!response.ok) {
|
222 |
+
throw new Error(`サーバーエラー (${response.status})。応答形式が不正です。`);
|
223 |
+
} else {
|
224 |
+
// 成功ステータスなのにJSONでない場合(通常はありえない)
|
225 |
+
throw new Error('サーバーからの応答形式が予期せぬ形式です。');
|
226 |
+
}
|
227 |
+
}
|
228 |
|
229 |
+
|
230 |
+
// ★★★ サーバーが返すJSON構造に合わせて修正 ★★★
|
231 |
+
// サーバーが { success: true, data: { id: '...' } } のような形式を返す想定
|
232 |
+
if (response.ok && result && typeof result === 'object' && result.success && result.data && result.data.id) {
|
233 |
+
// 生成成功: 取得したIDを使って学習画面へ遷移
|
234 |
+
console.log("Generation successful, navigating to learning page with ID:", result.data.id);
|
235 |
goToLearning(result.data.id);
|
236 |
} else {
|
237 |
+
// APIがエラーを返した場合、または期待する構造でなかった場合
|
238 |
+
console.error('Generation API call failed or returned unexpected structure:', result);
|
239 |
+
const serverMessage = (result && typeof result === 'object' && result.message) || // resultがオブジェクトでmessageがあればそれを表示
|
240 |
+
(result && typeof result === 'object' && result.error && result.error.message) || // error.messageがあればそれを表示
|
241 |
+
(response.ok ? '生成に失敗しました (不明な応答形式)。' : `サーバーエラー (${response.status})`); // それ以外の場合
|
242 |
+
displayErrorMessage(serverMessage, errorMsgElementId);
|
243 |
}
|
244 |
|
245 |
} catch (error) {
|
246 |
+
// fetch自体が失敗した場合(ネットワークエラーなど)または上記のthrow new Error
|
247 |
console.error('Error during generation request:', error);
|
248 |
+
// ユーザーフレンドリーなメッセージ表示
|
249 |
+
if (error.message.includes('Failed to fetch')) {
|
250 |
+
displayErrorMessage('サーバーに接続できませんでした。ネットワーク接続を確認してください。', errorMsgElementId);
|
251 |
+
} else {
|
252 |
+
displayErrorMessage(`エラーが発生しました: ${error.message}`, errorMsgElementId);
|
|
|
|
|
|
|
253 |
}
|
254 |
} finally {
|
255 |
+
toggleLoading(false, 'generate-button'); // ローディング終了(成功・失敗問わず)
|
256 |
}
|
257 |
|
258 |
+
return false; // formのデフォルト送信を常にキャンセル
|
259 |
}
|
260 |
|
261 |
|
262 |
// --- learning.html 用の処理 ---
|
263 |
|
264 |
/**
|
265 |
+
* learning.html の初期化: コンテンツデータを取得し、最初のアイテムを表示
|
266 |
*/
|
267 |
async function initializeLearningScreen() {
|
268 |
console.log('Initializing Learning Screen...');
|
269 |
const params = new URLSearchParams(window.location.search);
|
270 |
const contentId = params.get('id');
|
271 |
|
272 |
+
// 正解エフェクト要素はDOMContentLoadedで取得済みのはず
|
|
|
273 |
|
274 |
if (!contentId) {
|
275 |
+
displayLearningError('学習コンテンツのIDが見つかりません。');
|
276 |
return;
|
277 |
}
|
278 |
console.log('Content ID:', contentId);
|
|
|
283 |
const response = await fetch(`/api/learning/${contentId}`);
|
284 |
if (!response.ok) {
|
285 |
// エラーレスポンスがJSON形式でない場合も考慮
|
286 |
+
let errorMessage = `サーバーからのデータ取得に失敗 (${response.status})`;
|
287 |
try {
|
288 |
const errorData = await response.json();
|
289 |
errorMessage = errorData.message || errorMessage;
|
290 |
} catch (e) {
|
291 |
+
console.warn('Failed to parse error response as JSON.');
|
292 |
}
|
293 |
throw new Error(errorMessage);
|
294 |
}
|
295 |
|
296 |
+
// ★★★ サーバーからの応答が配列であると想定して処理 ★★★
|
297 |
+
const itemsArray = await response.json();
|
298 |
+
console.log('Fetched data (items array):', itemsArray);
|
299 |
|
300 |
+
// 配列であるか、空でないかをチェック
|
301 |
+
if (!Array.isArray(itemsArray)) {
|
302 |
+
throw new Error('サーバーからのデータ形式が不正です (配列ではありません)。');
|
303 |
+
}
|
304 |
+
if (itemsArray.length === 0) {
|
305 |
+
throw new Error('学習データが見つかりませんでした (空の配列)。');
|
306 |
+
}
|
|
|
|
|
|
|
307 |
|
308 |
+
// learningData オブジェクトを構築
|
309 |
+
learningData = {
|
310 |
+
title: `学習セット (${contentId})`, // タイトルは固定文字列またはcontentIdから生成
|
311 |
+
items: itemsArray // 取得した配列を items プロパティに設定
|
312 |
+
};
|
313 |
|
314 |
+
// タイトルを設定 (learningData.title を使用)
|
315 |
+
const titleElement = document.getElementById('learning-title');
|
316 |
+
if (titleElement) {
|
317 |
+
titleElement.textContent = learningData.title || '学習セット'; // デフォルトタイトル
|
318 |
} else {
|
319 |
+
console.warn("Title element ('learning-title') not found.");
|
320 |
}
|
321 |
|
322 |
+
// 最初のアイテムを表示
|
323 |
+
currentItemIndex = 0;
|
324 |
+
displayCurrentItem();
|
325 |
+
|
326 |
} catch (error) {
|
327 |
+
// fetchエラーまたは上記のthrow new Error
|
328 |
console.error('Error initializing learning screen:', error);
|
329 |
+
// JSONパースエラーの場合のメッセージを改善
|
330 |
+
if (error instanceof SyntaxError) {
|
331 |
+
displayLearningError(`サーバー応答の解析エラー: ${error.message}`);
|
332 |
+
} else {
|
333 |
+
displayLearningError(`読み込みエラー: ${error.message}`);
|
334 |
+
}
|
335 |
} finally {
|
336 |
toggleLoading(false); // ローディング表示終了
|
337 |
}
|
338 |
}
|
339 |
|
340 |
/**
|
341 |
+
* 現在の学習アイテムをカードに表示 (クイズ or 要約)
|
342 |
*/
|
343 |
function displayCurrentItem() {
|
344 |
+
hideCorrectEffect(); // 前のエフェクトを隠す
|
345 |
+
clearTimeout(correctEffectTimeout); // タイマーもクリア
|
|
|
346 |
|
347 |
+
// 要素取得 (毎回取得する方が確実)
|
|
|
|
|
|
|
|
|
|
|
|
|
348 |
const cardElement = document.getElementById('learning-card');
|
349 |
const cardTextElement = document.getElementById('card-text');
|
350 |
const answerTextElement = document.getElementById('answer-text');
|
|
|
352 |
const optionsArea = document.getElementById('options-area');
|
353 |
const modeIndicator = document.getElementById('mode-indicator');
|
354 |
|
355 |
+
// 要素が存在しない場合はエラーを出して終了
|
356 |
+
if (!cardElement || !cardTextElement || !answerTextElement || !tapToShowElement || !optionsArea || !modeIndicator) {
|
357 |
+
console.error("One or more required learning elements are missing from the DOM.");
|
358 |
+
displayLearningError("画面の表示に必要な要素が見つかりません。");
|
359 |
+
return;
|
360 |
+
}
|
361 |
+
|
362 |
+
// データ存在チェック (learningData と learningData.items をチェック)
|
363 |
+
if (!learningData || !learningData.items || currentItemIndex < 0 || currentItemIndex >= learningData.items.length) {
|
364 |
+
console.error('Invalid learning data or index:', learningData, currentItemIndex);
|
365 |
+
displayLearningError('表示する学習データが見つかりません。');
|
366 |
+
return;
|
367 |
+
}
|
368 |
+
|
369 |
+
// ★★★ learningData.items から現在のアイテムを取得 ★★★
|
370 |
+
const item = learningData.items[currentItemIndex];
|
371 |
+
|
372 |
+
// 表示内容リセット
|
373 |
+
cardTextElement.innerHTML = ''; // textContentではなくinnerHTMLでリセット
|
374 |
answerTextElement.style.display = 'none';
|
375 |
+
answerTextElement.textContent = ''; // 内容もクリア
|
376 |
tapToShowElement.style.display = 'none';
|
377 |
+
optionsArea.innerHTML = ''; // 選択肢エリアクリア
|
378 |
+
optionsArea.style.display = 'none'; // 一旦非表示
|
379 |
modeIndicator.classList.remove('loading'); // ローディングクラス除去
|
380 |
|
381 |
+
// itemタイプに基づいて表示を分岐
|
382 |
+
// ★★★ 問題文のキーを 'text' に修正 ★★★
|
383 |
+
if (item.type === 'question' && item.text && item.answer) { // `item.question` -> `item.text` に変更
|
384 |
currentMode = 'quiz';
|
385 |
modeIndicator.textContent = 'クイズモード';
|
386 |
+
// ★★★ 問題文には item.text を使用 ★★★
|
387 |
+
cardTextElement.textContent = item.text; // 問題文はtextContentで安全に設定
|
388 |
+
answerTextElement.textContent = `答え: ${item.answer}`; // 答えは事前に設定(非表示)
|
389 |
|
390 |
// 選択肢ボタンを生成
|
391 |
if (item.options && Array.isArray(item.options) && item.options.length > 0) {
|
392 |
optionsArea.style.display = 'block'; // 選択肢があれば表示
|
|
|
|
|
393 |
const optionsToDisplay = item.options; // シャッフルしない場合
|
394 |
|
395 |
optionsToDisplay.forEach(option => {
|
396 |
const button = document.createElement('button');
|
397 |
button.classList.add('option-button');
|
398 |
button.textContent = option;
|
399 |
+
// クリックしたら handleOptionClick を呼ぶ
|
400 |
+
button.onclick = () => handleOptionClick(option);
|
401 |
optionsArea.appendChild(button);
|
402 |
});
|
403 |
+
tapToShowElement.style.display = 'block'; // 選択肢がある場合もタップ表示は有効
|
404 |
} else {
|
405 |
+
// 選択肢がないクイズの場合
|
406 |
+
console.warn(`Quiz item ${currentItemIndex} has no options.`);
|
407 |
+
tapToShowElement.style.display = 'block'; // 答え表示は可能にする
|
408 |
}
|
409 |
+
// カード自体 or タップ表示テキストのクリックで解答表示
|
410 |
+
cardElement.onclick = () => revealAnswer(); // 引数なしで呼び出し
|
411 |
+
tapToShowElement.onclick = () => revealAnswer(); // 引数なしで呼び出し
|
412 |
|
413 |
+
} else if (item.type === 'summary' && item.text) {
|
414 |
currentMode = 'summary';
|
415 |
modeIndicator.textContent = '要約モード';
|
416 |
+
// 改行(\n)を<br>に置換して表示 (innerHTMLを使うので注意)
|
|
|
417 |
cardTextElement.innerHTML = item.text.replace(/\n/g, '<br>');
|
418 |
|
419 |
+
// 要約モードではクリックイベント不要
|
420 |
cardElement.onclick = null;
|
421 |
tapToShowElement.style.display = 'none';
|
422 |
+
optionsArea.style.display = 'none'; // 念のため
|
423 |
+
|
424 |
} else {
|
425 |
+
// 不明なタイプまたは必要なデータ (text や answer) が欠けている場合
|
426 |
+
console.warn('Unknown or invalid item type/data:', item);
|
427 |
+
currentMode = 'unknown';
|
428 |
+
modeIndicator.textContent = 'データエラー';
|
429 |
+
// ★★★ `item.text` を表示試行するように変更 ★★★
|
430 |
+
cardTextElement.textContent = `[不正なデータ形式] ${item.text || 'この項目を表示できません。'}`;
|
431 |
+
cardElement.onclick = null;
|
432 |
tapToShowElement.style.display = 'none';
|
433 |
+
optionsArea.style.display = 'none';
|
434 |
}
|
435 |
|
436 |
+
updatePagination(); // ページネーション表示を更新
|
437 |
}
|
438 |
|
439 |
+
/**
|
440 |
+
* クイズの選択肢がクリックされたときの処理
|
441 |
+
* @param {string} selectedOption - ユーザーが選択した選択肢のテキスト
|
442 |
+
*/
|
443 |
function handleOptionClick(selectedOption) {
|
444 |
+
// ★★★ learningData.items の存在もチェック ★★★
|
445 |
+
if (currentMode !== 'quiz' || !learningData || !learningData.items || !learningData.items[currentItemIndex]) return;
|
446 |
|
447 |
+
// 解答表示がすでに行われていないかチェック(二重処理防止)
|
448 |
+
const answerTextElement = document.getElementById('answer-text');
|
449 |
+
if (answerTextElement && answerTextElement.style.display === 'block') {
|
450 |
+
console.log('Answer already revealed, ignoring option click.');
|
451 |
+
return;
|
452 |
+
}
|
453 |
+
|
454 |
+
// ★★★ learningData.items から現在のアイテムを取得 ★★★
|
455 |
const currentItem = learningData.items[currentItemIndex];
|
456 |
const correctAnswer = currentItem.answer;
|
457 |
const isCorrect = selectedOption === correctAnswer;
|
458 |
|
|
|
459 |
if (isCorrect) {
|
460 |
+
console.log("Correct!");
|
461 |
+
showCorrectEffect(); // ★★★ 正解エフェクト表示 ★★★
|
462 |
} else {
|
463 |
+
console.log("Incorrect...");
|
464 |
+
// 不正解時のフィードバック(例: ボタンを赤くするなど)はrevealAnswerで行う
|
465 |
}
|
466 |
|
467 |
+
// 選択された選択肢を引数に渡して、解答表示とボタンの状態更新を行う
|
468 |
+
revealAnswer(selectedOption);
|
469 |
}
|
470 |
|
471 |
|
472 |
/**
|
473 |
+
* クイズの解答を表示し、選択肢ボタンの状態を更新する
|
474 |
+
* @param {string|null} [selectedOption=null] - ユーザーが選択した選択肢。nullの場合はカードタップ等による表示。
|
475 |
*/
|
476 |
function revealAnswer(selectedOption = null) {
|
477 |
+
// ★★★ learningData.items の存在もチェック ★★★
|
478 |
+
if (currentMode !== 'quiz' || !learningData || !learningData.items || !learningData.items[currentItemIndex]) return;
|
479 |
+
|
480 |
+
const answerTextElement = document.getElementById('answer-text');
|
481 |
+
const tapToShowElement = document.getElementById('tap-to-show');
|
482 |
+
const optionsArea = document.getElementById('options-area');
|
483 |
+
const cardElement = document.getElementById('learning-card'); // カード要素も取得
|
|
|
|
|
|
|
|
|
484 |
|
485 |
+
// 既に表示済みなら何もしない
|
486 |
+
if (answerTextElement && answerTextElement.style.display === 'block') {
|
487 |
+
return;
|
488 |
+
}
|
489 |
|
490 |
+
// 解答テキストを表示
|
491 |
+
if (answerTextElement) {
|
492 |
+
answerTextElement.style.display = 'block';
|
493 |
+
}
|
494 |
+
// 「タップして表示」を隠す
|
495 |
+
if (tapToShowElement) {
|
496 |
+
tapToShowElement.style.display = 'none';
|
497 |
+
}
|
498 |
+
// カード自体のクリックイベントを無効化(解答表示後は不要)
|
499 |
+
if (cardElement) {
|
500 |
+
cardElement.onclick = null;
|
501 |
+
}
|
502 |
|
503 |
+
// 選択肢ボタンがあれば状態を更新
|
504 |
+
if (optionsArea) {
|
505 |
+
// ★★★ learningData.items から現在のアイテムを取得 ★★★
|
506 |
+
const correctAnswer = learningData.items[currentItemIndex].answer;
|
507 |
+
const buttons = optionsArea.querySelectorAll('.option-button'); // querySelectorAll推奨
|
508 |
+
|
509 |
+
buttons.forEach(button => {
|
510 |
+
button.disabled = true; // 全てのボタンを無効化
|
511 |
+
button.onclick = null; // クリックイベント解除
|
512 |
+
|
513 |
+
const buttonText = button.textContent;
|
514 |
+
|
515 |
+
if (buttonText === correctAnswer) {
|
516 |
+
button.classList.add('correct'); // 正解ボタンにクラス付与
|
517 |
+
} else if (buttonText === selectedOption) {
|
518 |
+
// 不正解で、かつユーザーが選択したボタン
|
519 |
+
button.classList.add('incorrect');
|
520 |
+
} else {
|
521 |
+
// 正解でもなく、ユーザーが選択したものでもないボタン
|
522 |
+
button.classList.add('other-disabled'); // 他の選択肢用のスタイル
|
523 |
}
|
524 |
+
});
|
525 |
}
|
526 |
}
|
527 |
|
528 |
|
529 |
/**
|
530 |
+
* 次の学習アイテムへ移動
|
531 |
*/
|
532 |
function goToNext() {
|
533 |
+
// ★★★ learningData.items の存在もチェック ★★★
|
534 |
+
if (learningData && learningData.items && currentItemIndex < learningData.items.length - 1) {
|
535 |
currentItemIndex++;
|
536 |
+
displayCurrentItem(); // 次のアイテムを表示
|
537 |
+
} else {
|
538 |
+
console.log("Already at the last item or no data.");
|
539 |
+
// 最後のアイテムの場合、完了メッセージなどを表示しても良い
|
540 |
+
if (learningData && learningData.items && currentItemIndex === learningData.items.length - 1) {
|
541 |
+
alert("学習セットが完了しました!");
|
542 |
+
}
|
543 |
}
|
544 |
}
|
545 |
|
546 |
/**
|
547 |
+
* 前の学習アイテムへ移動
|
548 |
*/
|
549 |
function goToPrev() {
|
550 |
+
// ★★★ learningData.items の存在もチェック ★★★
|
551 |
+
if (learningData && learningData.items && currentItemIndex > 0) {
|
552 |
currentItemIndex--;
|
553 |
+
displayCurrentItem(); // 前のアイテムを表示
|
554 |
+
} else {
|
555 |
+
console.log("Already at the first item or no data.");
|
556 |
}
|
557 |
}
|
558 |
|
559 |
/**
|
560 |
+
* ページネーション(ページ番号とボタンの状態)を更新
|
561 |
*/
|
562 |
function updatePagination() {
|
563 |
const pageInfo = document.getElementById('page-info');
|
564 |
const prevButton = document.getElementById('prev-button');
|
565 |
const nextButton = document.getElementById('next-button');
|
566 |
|
567 |
+
// 要素の存在チェック
|
568 |
+
if (!pageInfo || !prevButton || !nextButton) {
|
569 |
+
console.warn("Pagination elements not found.");
|
570 |
+
return;
|
571 |
+
}
|
572 |
+
|
573 |
+
// ★★★ learningData.items の存在をチェック ★★★
|
574 |
+
if (learningData && learningData.items && learningData.items.length > 0) {
|
575 |
const totalItems = learningData.items.length;
|
576 |
+
pageInfo.textContent = `${currentItemIndex + 1} / ${totalItems}`;
|
577 |
+
// ボタンの有効/無効を設定
|
578 |
+
prevButton.disabled = currentItemIndex === 0;
|
579 |
+
nextButton.disabled = currentItemIndex === totalItems - 1;
|
|
|
|
|
|
|
|
|
|
|
|
|
580 |
} else {
|
581 |
+
// データがない場合や空の場合
|
582 |
+
pageInfo.textContent = '0 / 0';
|
583 |
+
prevButton.disabled = true;
|
584 |
+
nextButton.disabled = true;
|
585 |
}
|
586 |
}
|
587 |
|
588 |
/**
|
589 |
+
* learning.html でエラーが発生した場合の表示処理
|
590 |
+
* @param {string} message - 表示するエラーメッセージ
|
591 |
*/
|
592 |
function displayLearningError(message) {
|
593 |
const cardElement = document.getElementById('learning-card');
|
|
|
597 |
const modeIndicator = document.getElementById('mode-indicator');
|
598 |
const tapToShow = document.getElementById('tap-to-show');
|
599 |
|
600 |
+
// 各要素が存在すればエラー表示に切り替える
|
601 |
if (titleElement) titleElement.textContent = 'エラー';
|
602 |
+
if (modeIndicator) modeIndicator.textContent = 'エラー発生';
|
603 |
if (cardElement) {
|
604 |
+
// カードの内容をエラーメッセージで置き換え
|
605 |
cardElement.innerHTML = `<p class="main-text" style="color: red; text-align: center; padding: 20px;">${message}</p>`;
|
606 |
+
cardElement.onclick = null; // クリックイベント解除
|
607 |
}
|
608 |
+
if (paginationElement) paginationElement.style.display = 'none'; // ページネーション非表示
|
609 |
+
if (optionsArea) {
|
610 |
+
optionsArea.innerHTML = ''; // 選択肢クリア
|
611 |
+
optionsArea.style.display = 'none';
|
612 |
+
}
|
613 |
+
if (tapToShow) tapToShow.style.display = 'none'; // タップ表示も隠す
|
614 |
+
|
615 |
+
// エラー時はローディング表示も確実に解除
|
616 |
+
toggleLoading(false);
|
617 |
}
|
618 |
|
619 |
+
/**
|
620 |
+
* 正解時に「〇」エフェクトを表示する
|
621 |
+
*/
|
622 |
function showCorrectEffect() {
|
623 |
if (correctEffect) {
|
624 |
+
clearTimeout(correctEffectTimeout); // 既存のタイマーをクリア
|
625 |
+
correctEffect.classList.add('show'); // 表示クラスを追加
|
|
|
|
|
626 |
|
627 |
+
// 指定時間後 (例: 1秒) に非表示処理を開始
|
628 |
correctEffectTimeout = setTimeout(() => {
|
629 |
hideCorrectEffect();
|
630 |
+
}, 1000); // 表示時間 (ミリ秒)
|
631 |
+
} else {
|
632 |
+
console.warn("Correct effect element not found.");
|
633 |
}
|
634 |
}
|
635 |
|
636 |
+
/**
|
637 |
+
* 正解エフェクトを非表示にする
|
638 |
+
*/
|
639 |
function hideCorrectEffect() {
|
640 |
if (correctEffect && correctEffect.classList.contains('show')) {
|
641 |
+
correctEffect.classList.remove('show'); // 表示クラスを削除
|
|
|
|
|
|
|
|
|
|
|
642 |
}
|
643 |
+
// タイマーもクリアしておく(手動で非表示にする場合など)
|
644 |
+
clearTimeout(correctEffectTimeout);
|
645 |
}
|
646 |
|
647 |
|
648 |
// --- settings.html 用の処理 ---
|
649 |
|
650 |
/**
|
651 |
+
* トグルスイッチの状態が変更されたときの処理
|
652 |
+
* @param {HTMLInputElement} checkbox - 変更されたチェックボックス要素
|
653 |
+
* @param {string} type - トグルの種類 ('dark', 'notification' など)
|
654 |
*/
|
655 |
function handleToggleChange(checkbox, type) {
|
656 |
+
const isChecked = checkbox.checked;
|
657 |
+
console.log(`Toggle changed for ${type}: ${isChecked}`);
|
658 |
+
|
659 |
if (type === 'dark') {
|
660 |
+
// ダークモードの切り替え
|
661 |
+
document.body.classList.toggle('dark-mode', isChecked);
|
662 |
+
// 設定をlocalStorageに保存 (エラーハンドリング付き)
|
663 |
try {
|
664 |
+
localStorage.setItem('darkModeEnabled', isChecked);
|
665 |
+
console.log(`Dark mode preference saved: ${isChecked}`);
|
666 |
} catch (e) {
|
667 |
+
console.warn('Could not save dark mode preference to localStorage:', e);
|
668 |
+
// ユーザーに通知する (任意)
|
669 |
+
// alert('ダークモード設定の保存に失敗しました。');
|
670 |
}
|
671 |
+
} else if (type === 'notification') {
|
672 |
+
// 通知設定の切り替え (未実装)
|
673 |
+
console.log("Notification setting toggled:", isChecked);
|
674 |
+
alert(`通知設定は現在未実装です。(設定: ${isChecked ? 'ON' : 'OFF'})`);
|
675 |
+
// ここに通知設定のAPI呼び出しなどを実装
|
676 |
}
|
677 |
+
// 他のトグルスイッチの処理もここに追加
|
678 |
}
|
679 |
|
680 |
/**
|
681 |
+
* ログアウトボタンがクリックされたときの処理
|
682 |
*/
|
683 |
function handleLogout() {
|
684 |
+
console.log("Logout button clicked");
|
685 |
+
// TODO: 実際のログアウト処理を実装
|
686 |
+
// - サーバーAPIへのログアウト要求
|
687 |
+
// - ローカルストレージやセッションストレージの認証情報クリア
|
688 |
+
// - など
|
689 |
+
|
690 |
+
// 仮の動作: アラート表示と入力画面への遷移
|
691 |
+
alert("ログアウトしました。(この機能は現在開発中です)");
|
692 |
+
// navigateTo 内でメニューが閉じる
|
693 |
+
goToInput();
|
694 |
}
|
695 |
|
696 |
/**
|
697 |
+
* ローカルストレージからダークモード設定を読み込み、bodyに適用する
|
698 |
*/
|
699 |
function applyDarkModePreference() {
|
700 |
try {
|
701 |
+
// localStorageから 'darkModeEnabled' の値を取得し、'true' かどうかで判定
|
702 |
const darkModeEnabled = localStorage.getItem('darkModeEnabled') === 'true';
|
703 |
document.body.classList.toggle('dark-mode', darkModeEnabled);
|
704 |
+
console.log(`Applied dark mode preference from localStorage: ${darkModeEnabled}`);
|
|
|
|
|
705 |
} catch (e) {
|
706 |
+
console.warn('Could not load or apply dark mode preference from localStorage:', e);
|
707 |
+
// ここでエラーが発生しても処理は続行する
|
708 |
}
|
709 |
}
|
710 |
|
711 |
|
712 |
// --- ページの初期化処理 ---
|
713 |
+
const pathname = window.location.pathname; // 現在のパスをグローバルスコープで取得
|
714 |
|
715 |
+
// ★★★ 最初にダークモード設定を適用 (FOUC: Flash of Unstyled Content 対策) ★★★
|
716 |
applyDarkModePreference();
|
717 |
|
718 |
+
// ★★★ DOMContentLoaded イベントリスナー: DOM構築完了後に実行 ★★★
|
719 |
document.addEventListener('DOMContentLoaded', () => {
|
720 |
+
console.log('DOM fully loaded and parsed. Path:', pathname);
|
721 |
+
|
722 |
+
// メニュー要素を取得してグローバル変数に格納
|
723 |
+
sideMenu = document.getElementById('side-menu');
|
724 |
+
menuOverlay = document.getElementById('menu-overlay');
|
725 |
+
if (!sideMenu || !menuOverlay) {
|
726 |
+
console.warn("Side menu or overlay element not found on this page.");
|
727 |
+
}
|
728 |
|
729 |
+
// learning.html の場合のみ正解エフェクト要素を取得し、初期化関数を呼ぶ
|
730 |
+
if (pathname.endsWith('/learning') || pathname.endsWith('/learning.html')) {
|
731 |
+
correctEffect = document.getElementById('correct-effect');
|
732 |
+
if (!correctEffect) {
|
733 |
+
console.warn("Correct effect element ('correct-effect') not found on learning page.");
|
734 |
+
}
|
735 |
+
initializeLearningScreen(); // learningページ専用の初期化
|
736 |
+
}
|
737 |
+
|
738 |
+
// input.html の初期化処理
|
739 |
+
if (pathname === '/' || pathname.endsWith('/input') || pathname.endsWith('/input.html')) {
|
740 |
+
console.log("Initializing Input page...");
|
741 |
const form = document.getElementById('generate-form');
|
742 |
if (form) {
|
743 |
+
// フォーム送信イベントにリスナーを設定
|
744 |
form.addEventListener('submit', (event) => {
|
745 |
+
event.preventDefault(); // デフォルトの送信をキャンセル
|
746 |
+
handleGenerateSubmit(); // カスタム送信処理を呼び出し
|
747 |
});
|
748 |
+
} else {
|
749 |
+
console.warn("Generate form ('generate-form') not found.");
|
750 |
}
|
751 |
+
// URLパラメータから初期値を設定する例 (任意)
|
|
|
752 |
const urlParams = new URLSearchParams(window.location.search);
|
753 |
const initialUrl = urlParams.get('url');
|
754 |
if (initialUrl) {
|
755 |
const urlInput = document.getElementById('youtube-url');
|
756 |
if (urlInput) {
|
757 |
urlInput.value = initialUrl;
|
758 |
+
console.log("Set initial URL from query parameter:", initialUrl);
|
759 |
}
|
760 |
}
|
761 |
+
}
|
762 |
+
// history.html の初期化処理 (必要なら)
|
763 |
+
else if (pathname.endsWith('/history') || pathname.endsWith('/history.html')) {
|
764 |
+
console.log("Initializing History page...");
|
765 |
+
// 例: loadHistoryData(); // 履歴データを読み込んで表示する関数
|
766 |
+
}
|
767 |
+
// settings.html の初期化処理
|
768 |
+
else if (pathname.endsWith('/settings') || pathname.endsWith('/settings.html')) {
|
769 |
+
console.log("Initializing Settings page...");
|
|
|
770 |
// ダークモードトグルの状態をlocalStorageに合わせて更新
|
771 |
try {
|
772 |
const darkModeEnabled = localStorage.getItem('darkModeEnabled') === 'true';
|
773 |
+
// type='dark' を持つトグルスイッチを探す
|
774 |
+
const toggle = document.querySelector('input[type="checkbox"][onchange*="dark"]');
|
775 |
if (toggle) {
|
776 |
toggle.checked = darkModeEnabled;
|
777 |
+
console.log("Set dark mode toggle state to:", darkModeEnabled);
|
778 |
+
} else {
|
779 |
+
console.warn("Dark mode toggle switch not found on settings page.");
|
780 |
}
|
781 |
} catch (e) {
|
782 |
+
console.warn('Could not set dark mode toggle state on settings page:', e);
|
783 |
}
|
784 |
}
|
785 |
|
786 |
+
// フッターナビゲーションのアクティブ状態を更新
|
787 |
updateFooterNavActiveState(pathname);
|
788 |
+
|
789 |
+
// ★★★ ページ読み込み時にメニューが意図せず開いた状態なら閉じる ★★★
|
790 |
+
// (特にブラウザの履歴操作などで発生することがあるため)
|
791 |
+
closeMenu();
|
792 |
});
|
793 |
|
794 |
|
795 |
/**
|
796 |
+
* 現在のパスに基づいてフッターナビゲーションのアクティブ状態を更新
|
797 |
*/
|
798 |
function updateFooterNavActiveState(currentPath) {
|
799 |
const footerNav = document.querySelector('.footer-nav');
|
800 |
+
if (!footerNav) {
|
801 |
+
// フッターがないページなら何もしない
|
802 |
+
return;
|
803 |
+
}
|
804 |
|
805 |
const buttons = footerNav.querySelectorAll('button');
|
806 |
+
let foundActive = false; // アクティブなボタンが見つかったか
|
807 |
+
|
808 |
buttons.forEach(button => {
|
809 |
+
button.classList.remove('active'); // まず全て非アクティブに
|
810 |
const onclickAttr = button.getAttribute('onclick');
|
811 |
if (onclickAttr) {
|
812 |
+
// onclick属性の値から遷移先を判定
|
813 |
if ((currentPath === '/' || currentPath.endsWith('/input') || currentPath.endsWith('/input.html')) && onclickAttr.includes('goToInput')) {
|
814 |
button.classList.add('active');
|
815 |
+
foundActive = true;
|
816 |
} else if ((currentPath.endsWith('/history') || currentPath.endsWith('/history.html')) && onclickAttr.includes('goToHistory')) {
|
817 |
button.classList.add('active');
|
818 |
+
foundActive = true;
|
819 |
} else if ((currentPath.endsWith('/settings') || currentPath.endsWith('/settings.html')) && onclickAttr.includes('goToSettings')) {
|
820 |
button.classList.add('active');
|
821 |
+
foundActive = true;
|
822 |
}
|
823 |
+
// learning ページは通常フッターをアクティブにしない想定
|
|
|
|
|
|
|
824 |
}
|
825 |
});
|
826 |
+
|
827 |
+
// デバッグ用ログ
|
828 |
+
// console.log(`Footer nav active state updated for path "${currentPath}". Active button found: ${foundActive}`);
|
829 |
}
|
830 |
|
831 |
|
832 |
+
// --- デバッグ用: 一部関数/変数をグローバルスコープに公開 ---
|
833 |
+
// 注意: 本番環境では削除またはコメントアウトすることを強く推奨します
|
834 |
window.debug = {
|
835 |
navigateTo,
|
836 |
goToInput,
|
|
|
838 |
goToSettings,
|
839 |
goToLearning,
|
840 |
openMenu,
|
841 |
+
closeMenu,
|
842 |
handleGenerateSubmit,
|
843 |
initializeLearningScreen,
|
844 |
+
handleOptionClick,
|
845 |
revealAnswer,
|
846 |
goToNext,
|
847 |
goToPrev,
|
848 |
handleToggleChange,
|
849 |
handleLogout,
|
850 |
+
showCorrectEffect,
|
851 |
+
hideCorrectEffect,
|
852 |
learningData, // 現在の学習データ確認用
|
853 |
+
currentItemIndex, // 現在のインデックス確認用
|
854 |
+
sideMenu, // メニュー要素確認用
|
855 |
+
menuOverlay // オーバーレイ要素確認用
|
856 |
};
|
857 |
|
858 |
// --- END OF FILE script.js ---
|
static/style.css
CHANGED
@@ -597,4 +597,162 @@ body.dark-mode .toggle-switch input:checked + .slider { background-color: #58a6f
|
|
597 |
/* ダークモードの正解エフェクトの色 */
|
598 |
body.dark-mode .correct-effect { color: rgba(50, 220, 50, 0.85); }
|
599 |
|
600 |
-
/* --- END OF FILE style.css --- */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
597 |
/* ダークモードの正解エフェクトの色 */
|
598 |
body.dark-mode .correct-effect { color: rgba(50, 220, 50, 0.85); }
|
599 |
|
600 |
+
/* --- END OF FILE style.css --- */
|
601 |
+
/* --- START OF FILE style.css (追記部分) --- */
|
602 |
+
|
603 |
+
/* --- サイドメニュー --- */
|
604 |
+
.side-menu {
|
605 |
+
position: fixed;
|
606 |
+
top: 0;
|
607 |
+
left: 0;
|
608 |
+
width: 250px; /* メニューの幅 */
|
609 |
+
height: 100%;
|
610 |
+
background-color: #ffffff; /* メニューの背景色 */
|
611 |
+
box-shadow: 2px 0 5px rgba(0,0,0,0.1);
|
612 |
+
transform: translateX(-100%); /* 初期状態は画面外(左側) */
|
613 |
+
transition: transform 0.3s ease-in-out;
|
614 |
+
z-index: 1100; /* オーバーレイより手前 */
|
615 |
+
padding: 20px;
|
616 |
+
box-sizing: border-box;
|
617 |
+
display: flex;
|
618 |
+
flex-direction: column;
|
619 |
+
overflow-y: auto; /* メニュー項目が多い場合にスクロール */
|
620 |
+
}
|
621 |
+
|
622 |
+
.side-menu.open {
|
623 |
+
transform: translateX(0); /* 表示状態 */
|
624 |
+
}
|
625 |
+
|
626 |
+
.side-menu h2 {
|
627 |
+
margin-top: 30px; /* 閉じるボタンとのスペース */
|
628 |
+
margin-bottom: 20px;
|
629 |
+
font-size: 20px;
|
630 |
+
color: #333;
|
631 |
+
border-bottom: 1px solid #eee;
|
632 |
+
padding-bottom: 10px;
|
633 |
+
}
|
634 |
+
|
635 |
+
.side-menu ul {
|
636 |
+
list-style: none;
|
637 |
+
padding: 0;
|
638 |
+
margin: 0;
|
639 |
+
flex-grow: 1;
|
640 |
+
}
|
641 |
+
|
642 |
+
.side-menu li {
|
643 |
+
margin-bottom: 5px;
|
644 |
+
}
|
645 |
+
|
646 |
+
.side-menu li button {
|
647 |
+
background: none;
|
648 |
+
border: none;
|
649 |
+
padding: 12px 10px;
|
650 |
+
width: 100%;
|
651 |
+
text-align: left;
|
652 |
+
font-size: 16px;
|
653 |
+
color: #333;
|
654 |
+
cursor: pointer;
|
655 |
+
border-radius: 6px;
|
656 |
+
transition: background-color 0.2s;
|
657 |
+
display: flex; /* アイコンとテキストのため */
|
658 |
+
align-items: center;
|
659 |
+
}
|
660 |
+
.side-menu li button:hover {
|
661 |
+
background-color: #f0f0f0;
|
662 |
+
}
|
663 |
+
.side-menu li button:active {
|
664 |
+
background-color: #e0e0e0;
|
665 |
+
}
|
666 |
+
|
667 |
+
.side-menu li hr {
|
668 |
+
border: none;
|
669 |
+
border-top: 1px solid #eee;
|
670 |
+
margin: 15px 0;
|
671 |
+
}
|
672 |
+
|
673 |
+
.side-menu .logout-menu-item {
|
674 |
+
color: red; /* ログアウトは赤字 */
|
675 |
+
}
|
676 |
+
.side-menu .logout-menu-item:hover {
|
677 |
+
background-color: rgba(255, 0, 0, 0.05);
|
678 |
+
}
|
679 |
+
|
680 |
+
|
681 |
+
.close-menu-btn {
|
682 |
+
position: absolute;
|
683 |
+
top: 10px;
|
684 |
+
right: 15px;
|
685 |
+
background: none;
|
686 |
+
border: none;
|
687 |
+
font-size: 30px;
|
688 |
+
font-weight: bold;
|
689 |
+
color: #888;
|
690 |
+
cursor: pointer;
|
691 |
+
padding: 5px;
|
692 |
+
line-height: 1;
|
693 |
+
}
|
694 |
+
.close-menu-btn:hover {
|
695 |
+
color: #333;
|
696 |
+
}
|
697 |
+
|
698 |
+
/* --- メニューオーバーレイ --- */
|
699 |
+
.menu-overlay {
|
700 |
+
position: fixed;
|
701 |
+
top: 0;
|
702 |
+
left: 0;
|
703 |
+
width: 100%;
|
704 |
+
height: 100%;
|
705 |
+
background-color: rgba(0, 0, 0, 0.5); /* 半透明の黒 */
|
706 |
+
opacity: 0;
|
707 |
+
visibility: hidden; /* 初期状態は非表示 */
|
708 |
+
transition: opacity 0.3s ease-in-out, visibility 0s 0.3s; /* visibilityは遅延させる */
|
709 |
+
z-index: 1050; /* サイドメニューより下、他コンテンツより上 */
|
710 |
+
}
|
711 |
+
|
712 |
+
.menu-overlay.open {
|
713 |
+
opacity: 1;
|
714 |
+
visibility: visible;
|
715 |
+
transition: opacity 0.3s ease-in-out, visibility 0s 0s; /* 表示時は遅延なし */
|
716 |
+
}
|
717 |
+
|
718 |
+
/* --- メニュー表示中に背景をスクロールさせない (任意) --- */
|
719 |
+
body.menu-open {
|
720 |
+
overflow: hidden; /* bodyのスクロールを禁止 */
|
721 |
+
}
|
722 |
+
|
723 |
+
/* --- ダークモード用サイドメニュー (追記) --- */
|
724 |
+
body.dark-mode .side-menu {
|
725 |
+
background-color: #2c2c2c; /* ダークモードの背景色 */
|
726 |
+
box-shadow: 2px 0 5px rgba(0,0,0,0.3);
|
727 |
+
}
|
728 |
+
body.dark-mode .side-menu h2 {
|
729 |
+
color: #e0e0e0;
|
730 |
+
border-bottom-color: #444;
|
731 |
+
}
|
732 |
+
body.dark-mode .side-menu li button {
|
733 |
+
color: #e0e0e0;
|
734 |
+
}
|
735 |
+
body.dark-mode .side-menu li button:hover {
|
736 |
+
background-color: #3a3a3a;
|
737 |
+
}
|
738 |
+
body.dark-mode .side-menu li button:active {
|
739 |
+
background-color: #4a4a4a;
|
740 |
+
}
|
741 |
+
body.dark-mode .side-menu li hr {
|
742 |
+
border-top-color: #444;
|
743 |
+
}
|
744 |
+
body.dark-mode .close-menu-btn {
|
745 |
+
color: #aaa;
|
746 |
+
}
|
747 |
+
body.dark-mode .close-menu-btn:hover {
|
748 |
+
color: #e0e0e0;
|
749 |
+
}
|
750 |
+
body.dark-mode .side-menu .logout-menu-item {
|
751 |
+
color: #ff7f7f; /* ダークモードでの赤 */
|
752 |
+
}
|
753 |
+
body.dark-mode .side-menu .logout-menu-item:hover {
|
754 |
+
background-color: rgba(255, 80, 80, 0.1);
|
755 |
+
}
|
756 |
+
|
757 |
+
|
758 |
+
/* --- END OF FILE style.css (追記部分) --- */
|
templates/history.html
CHANGED
@@ -86,6 +86,20 @@
|
|
86 |
<li class="list-item-empty">履歴はありません。</li>
|
87 |
{% endif %}
|
88 |
</ul>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
89 |
</main>
|
90 |
<!-- フッターナビゲーション -->
|
91 |
<footer class="footer-nav">
|
|
|
86 |
<li class="list-item-empty">履歴はありません。</li>
|
87 |
{% endif %}
|
88 |
</ul>
|
89 |
+
<!-- ★★★ ここからサイドメニューとオーバーレイを追加 ★★★ -->
|
90 |
+
<div id="menu-overlay" class="menu-overlay" onclick="closeMenu()"></div>
|
91 |
+
<nav id="side-menu" class="side-menu" aria-label="サイドメニュー">
|
92 |
+
<button class="close-menu-btn" onclick="closeMenu()" aria-label="メニューを閉じる">×</button>
|
93 |
+
<h2>メニュー</h2>
|
94 |
+
<ul>
|
95 |
+
<li><button onclick="goToInput()">➕ 入力</button></li>
|
96 |
+
<li><button onclick="goToHistory()">🕒 履歴</button></li>
|
97 |
+
<li><button onclick="goToSettings()">⚙️ 設定</button></li>
|
98 |
+
<!-- 他に必要なメニュー項目を追加 -->
|
99 |
+
<li><hr></li>
|
100 |
+
<li><button onclick="handleLogout()" class="logout-menu-item">ログアウト</button></li>
|
101 |
+
</ul>
|
102 |
+
</nav>
|
103 |
</main>
|
104 |
<!-- フッターナビゲーション -->
|
105 |
<footer class="footer-nav">
|
templates/input.html
CHANGED
@@ -41,6 +41,20 @@
|
|
41 |
<span>(イメージ表示エリア)</span>
|
42 |
<!-- <img src="..." alt="動画サムネイル" style="display: none;"> -->
|
43 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
44 |
</main>
|
45 |
<!-- フッターナビゲーションなどを追加する場合はここに -->
|
46 |
</div>
|
|
|
41 |
<span>(イメージ表示エリア)</span>
|
42 |
<!-- <img src="..." alt="動画サムネイル" style="display: none;"> -->
|
43 |
</div>
|
44 |
+
<!-- ★★★ ここからサイドメニューとオーバーレイを追加 ★★★ -->
|
45 |
+
<div id="menu-overlay" class="menu-overlay" onclick="closeMenu()"></div>
|
46 |
+
<nav id="side-menu" class="side-menu" aria-label="サイドメニュー">
|
47 |
+
<button class="close-menu-btn" onclick="closeMenu()" aria-label="メニューを閉じる">×</button>
|
48 |
+
<h2>メニュー</h2>
|
49 |
+
<ul>
|
50 |
+
<li><button onclick="goToInput()">➕ 入力</button></li>
|
51 |
+
<li><button onclick="goToHistory()">🕒 履歴</button></li>
|
52 |
+
<li><button onclick="goToSettings()">⚙️ 設定</button></li>
|
53 |
+
<!-- 他に必要なメニュー項目を追加 -->
|
54 |
+
<li><hr></li>
|
55 |
+
<li><button onclick="handleLogout()" class="logout-menu-item">ログアウト</button></li>
|
56 |
+
</ul>
|
57 |
+
</nav>
|
58 |
</main>
|
59 |
<!-- フッターナビゲーションなどを追加する場合はここに -->
|
60 |
</div>
|
templates/learning.html
CHANGED
@@ -38,6 +38,20 @@
|
|
38 |
<span id="page-info">? / ?</span> <!-- 初期表示 -->
|
39 |
<button id="next-button" aria-label="次へ" onclick="goToNext()">></button>
|
40 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
41 |
</main>
|
42 |
</div>
|
43 |
|
|
|
38 |
<span id="page-info">? / ?</span> <!-- 初期表示 -->
|
39 |
<button id="next-button" aria-label="次へ" onclick="goToNext()">></button>
|
40 |
</div>
|
41 |
+
<!-- ★★★ ここからサイドメニューとオーバーレイを追加 ★★★ -->
|
42 |
+
<div id="menu-overlay" class="menu-overlay" onclick="closeMenu()"></div>
|
43 |
+
<nav id="side-menu" class="side-menu" aria-label="サイドメニュー">
|
44 |
+
<button class="close-menu-btn" onclick="closeMenu()" aria-label="メニューを閉じる">×</button>
|
45 |
+
<h2>メニュー</h2>
|
46 |
+
<ul>
|
47 |
+
<li><button onclick="goToInput()">➕ 入力</button></li>
|
48 |
+
<li><button onclick="goToHistory()">🕒 履歴</button></li>
|
49 |
+
<li><button onclick="goToSettings()">⚙️ 設定</button></li>
|
50 |
+
<!-- 他に必要なメニュー項目を追加 -->
|
51 |
+
<li><hr></li>
|
52 |
+
<li><button onclick="handleLogout()" class="logout-menu-item">ログアウト</button></li>
|
53 |
+
</ul>
|
54 |
+
</nav>
|
55 |
</main>
|
56 |
</div>
|
57 |
|
templates/settings.html
CHANGED
@@ -77,6 +77,20 @@
|
|
77 |
</button>
|
78 |
</li>
|
79 |
</ul>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
80 |
</main>
|
81 |
</div>
|
82 |
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
|
|
77 |
</button>
|
78 |
</li>
|
79 |
</ul>
|
80 |
+
<!-- ★★★ ここからサイドメニューとオーバーレイを追加 ★★★ -->
|
81 |
+
<div id="menu-overlay" class="menu-overlay" onclick="closeMenu()"></div>
|
82 |
+
<nav id="side-menu" class="side-menu" aria-label="サイドメニュー">
|
83 |
+
<button class="close-menu-btn" onclick="closeMenu()" aria-label="メニューを閉じる">×</button>
|
84 |
+
<h2>メニュー</h2>
|
85 |
+
<ul>
|
86 |
+
<li><button onclick="goToInput()">➕ 入力</button></li>
|
87 |
+
<li><button onclick="goToHistory()">🕒 履歴</button></li>
|
88 |
+
<li><button onclick="goToSettings()">⚙️ 設定</button></li>
|
89 |
+
<!-- 他に必要なメニュー項目を追加 -->
|
90 |
+
<li><hr></li>
|
91 |
+
<li><button onclick="handleLogout()" class="logout-menu-item">ログアウト</button></li>
|
92 |
+
</ul>
|
93 |
+
</nav>
|
94 |
</main>
|
95 |
</div>
|
96 |
<script src="{{ url_for('static', filename='script.js') }}"></script>
|