|
|
|
|
|
"use strict"; |
|
|
|
|
|
let learningData = null; |
|
let currentItemIndex = 0; |
|
let currentMode = 'quiz'; |
|
let correctEffectTimeout; |
|
let correctEffect = null; |
|
let sideMenu = null; |
|
let menuOverlay = null; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function navigateTo(url) { |
|
closeMenu(); |
|
setTimeout(() => { |
|
window.location.href = url; |
|
}, 100); |
|
} |
|
|
|
|
|
|
|
|
|
function openMenu() { |
|
console.log("Opening menu..."); |
|
if (sideMenu && menuOverlay) { |
|
sideMenu.classList.add('open'); |
|
menuOverlay.classList.add('open'); |
|
document.body.classList.add('menu-open'); |
|
} else { |
|
console.error("Side menu or overlay element not found. Cannot open menu."); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
function closeMenu() { |
|
if (sideMenu && menuOverlay) { |
|
sideMenu.classList.remove('open'); |
|
menuOverlay.classList.remove('open'); |
|
document.body.classList.remove('menu-open'); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function toggleLoading(show, buttonId = 'generate-button') { |
|
const targetButton = document.getElementById(buttonId); |
|
|
|
|
|
if (targetButton && buttonId === 'generate-button') { |
|
const spinner = targetButton.querySelector('.loading-spinner'); |
|
const buttonText = targetButton.querySelector('.button-text'); |
|
if (show) { |
|
targetButton.disabled = true; |
|
if (spinner) spinner.style.display = 'inline-block'; |
|
if (buttonText) buttonText.textContent = '生成中...'; |
|
} else { |
|
targetButton.disabled = false; |
|
if (spinner) spinner.style.display = 'none'; |
|
if (buttonText) buttonText.textContent = '生成する'; |
|
} |
|
} |
|
|
|
|
|
const loadingIndicator = document.getElementById('mode-indicator'); |
|
const cardElement = document.getElementById('learning-card'); |
|
const paginationElement = document.querySelector('.pagination'); |
|
const optionsArea = document.getElementById('options-area'); |
|
const tapToShowElement = document.getElementById('tap-to-show'); |
|
|
|
const currentPathname = window.location.pathname; |
|
if (currentPathname.endsWith('/learning') || currentPathname.endsWith('/learning.html')) { |
|
if (show) { |
|
|
|
if (loadingIndicator) { |
|
loadingIndicator.textContent = '読み込み中...'; |
|
loadingIndicator.classList.add('loading'); |
|
} |
|
if (cardElement) cardElement.style.opacity = '0.5'; |
|
if (paginationElement) paginationElement.style.display = 'none'; |
|
if (optionsArea) optionsArea.style.display = 'none'; |
|
if (tapToShowElement) tapToShowElement.style.display = 'none'; |
|
} else { |
|
|
|
if (loadingIndicator) { |
|
loadingIndicator.classList.remove('loading'); |
|
|
|
} |
|
if (cardElement) cardElement.style.opacity = '1'; |
|
|
|
if (paginationElement) paginationElement.style.display = 'flex'; |
|
|
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function displayErrorMessage(message, elementId = 'error-message') { |
|
const errorElement = document.getElementById(elementId); |
|
if (errorElement) { |
|
errorElement.textContent = message; |
|
errorElement.style.display = message ? 'block' : 'none'; |
|
} else { |
|
if (message) { |
|
console.error(`Error element with ID "${elementId}" not found. Cannot display message: ${message}`); |
|
} |
|
} |
|
} |
|
|
|
|
|
function goToInput() { |
|
navigateTo('/input'); |
|
} |
|
|
|
function goToHistory() { |
|
navigateTo('/history'); |
|
} |
|
|
|
function goToSettings() { |
|
navigateTo('/settings'); |
|
} |
|
|
|
function goToLearning(contentId) { |
|
if (contentId) { |
|
navigateTo(`/learning?id=${encodeURIComponent(contentId)}`); |
|
} else { |
|
console.error('goToLearning requires a content ID.'); |
|
alert('学習コンテンツのIDが見つかりません。'); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function handleGenerateSubmit() { |
|
const urlInput = document.getElementById('youtube-url'); |
|
const youtubeUrl = urlInput.value.trim(); |
|
const errorMsgElementId = 'error-message'; |
|
displayErrorMessage('', errorMsgElementId); |
|
|
|
if (!youtubeUrl) { |
|
displayErrorMessage('YouTubeリンクを入力してください。', errorMsgElementId); |
|
return false; |
|
} |
|
|
|
try { |
|
const urlObj = new URL(youtubeUrl); |
|
const validHostnames = ['www.youtube.com', 'youtube.com', 'youtu.be']; |
|
if (!validHostnames.includes(urlObj.hostname)) throw new Error('Invalid hostname'); |
|
if (urlObj.hostname === 'youtu.be' && urlObj.pathname.length <= 1) throw new Error('Missing video ID for youtu.be'); |
|
if (urlObj.hostname.includes('youtube.com')) { |
|
if (urlObj.pathname === '/watch' && !urlObj.searchParams.has('v')) throw new Error('Missing video ID parameter (v=...) for youtube.com/watch'); |
|
if (urlObj.pathname.startsWith('/shorts/') && urlObj.pathname.length <= 8) throw new Error('Missing video ID for youtube.com/shorts/'); |
|
} |
|
} catch (e) { |
|
console.warn("Invalid URL format:", e.message); |
|
displayErrorMessage('有効なYouTube動画のリンクを入力してください。(例: https://www.youtube.com/watch?v=...)', errorMsgElementId); |
|
return false; |
|
} |
|
|
|
toggleLoading(true, 'generate-button'); |
|
|
|
try { |
|
const response = await fetch('/api/generate', { |
|
method: 'POST', |
|
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, |
|
body: JSON.stringify({ url: youtubeUrl }), |
|
}); |
|
|
|
let result; |
|
try { |
|
result = await response.json(); |
|
} catch (jsonError) { |
|
console.error('Failed to parse JSON response:', jsonError); |
|
throw new Error(response.ok ? 'サーバーからの応答形式が不正です。' : `サーバーエラー (${response.status})`); |
|
} |
|
|
|
|
|
if (response.ok && result && typeof result === 'object' && result.success && result.data && result.data.id) { |
|
console.log("Generation successful, navigating to learning page with ID:", result.data.id); |
|
goToLearning(result.data.id); |
|
} else { |
|
console.error('Generation API call failed or returned unexpected structure:', result); |
|
const serverMessage = (result && typeof result === 'object' && result.message) || |
|
(result && typeof result === 'object' && result.error && result.error.message) || |
|
(response.ok ? '生成に失敗しました (不明な応答形式)。' : `サーバーエラー (${response.status})`); |
|
displayErrorMessage(serverMessage, errorMsgElementId); |
|
} |
|
|
|
} catch (error) { |
|
console.error('Error during generation request:', error); |
|
const userMessage = error.message.includes('Failed to fetch') ? 'サーバーに接続できませんでした。' : `エラー: ${error.message}`; |
|
displayErrorMessage(userMessage, errorMsgElementId); |
|
} finally { |
|
toggleLoading(false, 'generate-button'); |
|
} |
|
return false; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function initializeLearningScreen() { |
|
console.log('Initializing Learning Screen...'); |
|
const params = new URLSearchParams(window.location.search); |
|
const contentId = params.get('id'); |
|
|
|
if (!contentId) { |
|
displayLearningError('学習コンテンツのIDが見つかりません。'); |
|
return; |
|
} |
|
console.log('Content ID:', contentId); |
|
toggleLoading(true); |
|
|
|
try { |
|
const response = await fetch(`/api/learning/${contentId}`); |
|
if (!response.ok) { |
|
let errorMessage = `サーバーからのデータ取得に失敗 (${response.status})`; |
|
try { |
|
const errorData = await response.json(); |
|
errorMessage = errorData.message || errorMessage; |
|
} catch (e) { console.warn('Failed to parse error response as JSON.'); } |
|
throw new Error(errorMessage); |
|
} |
|
|
|
|
|
const result = await response.json(); |
|
console.log('Fetched data object:', result); |
|
|
|
|
|
if (!result || typeof result !== 'object' || !result.success || !result.data || !Array.isArray(result.data.items)) { |
|
console.error('Invalid data structure received from server:', result); |
|
throw new Error('サーバーから受け取ったデータの形式が正しくありません。'); |
|
} |
|
|
|
|
|
learningData = { |
|
title: result.data.title || `学習セット (${contentId})`, |
|
items: result.data.items |
|
}; |
|
|
|
|
|
if (learningData.items.length === 0) { |
|
throw new Error('学習データが見つかりませんでした (アイテムが0件)。'); |
|
} |
|
|
|
|
|
const titleElement = document.getElementById('learning-title'); |
|
if (titleElement) { |
|
titleElement.textContent = learningData.title; |
|
} else { |
|
console.warn("Title element ('learning-title') not found."); |
|
} |
|
|
|
|
|
currentItemIndex = 0; |
|
displayCurrentItem(); |
|
|
|
} catch (error) { |
|
console.error('Error initializing learning screen:', error); |
|
const message = (error instanceof SyntaxError) ? `サーバー応答の解析エラー: ${error.message}` : `読み込みエラー: ${error.message}`; |
|
displayLearningError(message); |
|
} finally { |
|
|
|
|
|
toggleLoading(false); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
function displayCurrentItem() { |
|
hideCorrectEffect(); |
|
clearTimeout(correctEffectTimeout); |
|
|
|
const cardElement = document.getElementById('learning-card'); |
|
const cardTextElement = document.getElementById('card-text'); |
|
const answerTextElement = document.getElementById('answer-text'); |
|
const tapToShowElement = document.getElementById('tap-to-show'); |
|
const optionsArea = document.getElementById('options-area'); |
|
const modeIndicator = document.getElementById('mode-indicator'); |
|
|
|
if (!cardElement || !cardTextElement || !answerTextElement || !tapToShowElement || !optionsArea || !modeIndicator) { |
|
console.error("One or more required learning elements are missing."); |
|
displayLearningError("画面表示に必要な要素が見つかりません。"); |
|
return; |
|
} |
|
if (!learningData || !learningData.items || currentItemIndex < 0 || currentItemIndex >= learningData.items.length) { |
|
console.error('Invalid learning data or index:', learningData, currentItemIndex); |
|
displayLearningError('表示する学習データが見つかりません。'); |
|
return; |
|
} |
|
|
|
const item = learningData.items[currentItemIndex]; |
|
|
|
|
|
cardTextElement.innerHTML = ''; |
|
answerTextElement.style.display = 'none'; |
|
answerTextElement.textContent = ''; |
|
tapToShowElement.style.display = 'none'; |
|
optionsArea.innerHTML = ''; |
|
optionsArea.style.display = 'none'; |
|
modeIndicator.classList.remove('loading'); |
|
|
|
|
|
if (item.type === 'question' && item.text && item.answer) { |
|
currentMode = 'quiz'; |
|
modeIndicator.textContent = 'クイズモード'; |
|
cardTextElement.textContent = item.text; |
|
answerTextElement.textContent = `答え: ${item.answer}`; |
|
|
|
if (item.options && Array.isArray(item.options) && item.options.length > 0) { |
|
optionsArea.style.display = 'block'; |
|
item.options.forEach(option => { |
|
const button = document.createElement('button'); |
|
button.classList.add('option-button'); |
|
button.textContent = option; |
|
button.onclick = () => handleOptionClick(option); |
|
optionsArea.appendChild(button); |
|
}); |
|
tapToShowElement.style.display = 'block'; |
|
} else { |
|
console.warn(`Quiz item ${currentItemIndex} has no options.`); |
|
tapToShowElement.style.display = 'block'; |
|
} |
|
cardElement.onclick = () => revealAnswer(); |
|
tapToShowElement.onclick = () => revealAnswer(); |
|
|
|
} else if (item.type === 'summary' && item.text) { |
|
currentMode = 'summary'; |
|
modeIndicator.textContent = '要約モード'; |
|
cardTextElement.innerHTML = item.text.replace(/\n/g, '<br>'); |
|
cardElement.onclick = null; |
|
tapToShowElement.style.display = 'none'; |
|
optionsArea.style.display = 'none'; |
|
|
|
} else { |
|
console.warn('Unknown or invalid item type/data:', item); |
|
currentMode = 'unknown'; |
|
modeIndicator.textContent = 'データエラー'; |
|
cardTextElement.textContent = `[不正なデータ形式] ${item.text || 'この項目を表示できません。'}`; |
|
cardElement.onclick = null; |
|
tapToShowElement.style.display = 'none'; |
|
optionsArea.style.display = 'none'; |
|
} |
|
|
|
updatePagination(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function handleOptionClick(selectedOption) { |
|
if (currentMode !== 'quiz' || !learningData || !learningData.items || !learningData.items[currentItemIndex]) return; |
|
|
|
const answerTextElement = document.getElementById('answer-text'); |
|
if (answerTextElement && answerTextElement.style.display === 'block') return; |
|
|
|
const currentItem = learningData.items[currentItemIndex]; |
|
const correctAnswer = currentItem.answer; |
|
const isCorrect = selectedOption === correctAnswer; |
|
|
|
if (isCorrect) { |
|
console.log("Correct!"); |
|
showCorrectEffect(); |
|
} else { |
|
console.log("Incorrect..."); |
|
} |
|
revealAnswer(selectedOption); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
function revealAnswer(selectedOption = null) { |
|
if (currentMode !== 'quiz' || !learningData || !learningData.items || !learningData.items[currentItemIndex]) return; |
|
|
|
const answerTextElement = document.getElementById('answer-text'); |
|
const tapToShowElement = document.getElementById('tap-to-show'); |
|
const optionsArea = document.getElementById('options-area'); |
|
const cardElement = document.getElementById('learning-card'); |
|
|
|
if (answerTextElement && answerTextElement.style.display === 'block') return; |
|
|
|
if (answerTextElement) answerTextElement.style.display = 'block'; |
|
if (tapToShowElement) tapToShowElement.style.display = 'none'; |
|
if (cardElement) cardElement.onclick = null; |
|
|
|
if (optionsArea) { |
|
const correctAnswer = learningData.items[currentItemIndex].answer; |
|
const buttons = optionsArea.querySelectorAll('.option-button'); |
|
buttons.forEach(button => { |
|
button.disabled = true; |
|
button.onclick = null; |
|
const buttonText = button.textContent; |
|
if (buttonText === correctAnswer) button.classList.add('correct'); |
|
else if (buttonText === selectedOption) button.classList.add('incorrect'); |
|
else button.classList.add('other-disabled'); |
|
}); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function goToNext() { |
|
if (learningData && learningData.items && currentItemIndex < learningData.items.length - 1) { |
|
currentItemIndex++; |
|
displayCurrentItem(); |
|
} else { |
|
console.log("Already at the last item or no data."); |
|
if (learningData && learningData.items && currentItemIndex === learningData.items.length - 1) { |
|
alert("学習セットが完了しました!"); |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
function goToPrev() { |
|
if (learningData && learningData.items && currentItemIndex > 0) { |
|
currentItemIndex--; |
|
displayCurrentItem(); |
|
} else { |
|
console.log("Already at the first item or no data."); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
function updatePagination() { |
|
const pageInfo = document.getElementById('page-info'); |
|
const prevButton = document.getElementById('prev-button'); |
|
const nextButton = document.getElementById('next-button'); |
|
|
|
if (!pageInfo || !prevButton || !nextButton) { |
|
console.warn("Pagination elements not found."); |
|
return; |
|
} |
|
|
|
|
|
if (learningData && learningData.items && learningData.items.length > 0) { |
|
const totalItems = learningData.items.length; |
|
pageInfo.textContent = `${currentItemIndex + 1} / ${totalItems}`; |
|
prevButton.disabled = currentItemIndex === 0; |
|
nextButton.disabled = currentItemIndex === totalItems - 1; |
|
} else { |
|
pageInfo.textContent = '0 / 0'; |
|
prevButton.disabled = true; |
|
nextButton.disabled = true; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
function displayLearningError(message) { |
|
const cardElement = document.getElementById('learning-card'); |
|
const titleElement = document.getElementById('learning-title'); |
|
const paginationElement = document.querySelector('.pagination'); |
|
const optionsArea = document.getElementById('options-area'); |
|
const modeIndicator = document.getElementById('mode-indicator'); |
|
const tapToShow = document.getElementById('tap-to-show'); |
|
|
|
if (titleElement) titleElement.textContent = 'エラー'; |
|
if (modeIndicator) modeIndicator.textContent = 'エラー発生'; |
|
if (cardElement) { |
|
cardElement.innerHTML = `<p class="main-text error-text">${message}</p>`; |
|
cardElement.onclick = null; |
|
} |
|
|
|
if (paginationElement) paginationElement.style.display = 'none'; |
|
if (optionsArea) { |
|
optionsArea.innerHTML = ''; |
|
optionsArea.style.display = 'none'; |
|
} |
|
if (tapToShow) tapToShow.style.display = 'none'; |
|
|
|
toggleLoading(false); |
|
} |
|
|
|
|
|
|
|
|
|
function showCorrectEffect() { |
|
if (correctEffect) { |
|
clearTimeout(correctEffectTimeout); |
|
correctEffect.classList.add('show'); |
|
correctEffectTimeout = setTimeout(() => { hideCorrectEffect(); }, 1000); |
|
} else { console.warn("Correct effect element not found."); } |
|
} |
|
|
|
|
|
|
|
|
|
function hideCorrectEffect() { |
|
if (correctEffect && correctEffect.classList.contains('show')) { |
|
correctEffect.classList.remove('show'); |
|
} |
|
clearTimeout(correctEffectTimeout); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function handleToggleChange(checkbox, type) { |
|
const isChecked = checkbox.checked; |
|
console.log(`Toggle changed for ${type}: ${isChecked}`); |
|
if (type === 'dark') { |
|
document.body.classList.toggle('dark-mode', isChecked); |
|
try { localStorage.setItem('darkModeEnabled', isChecked); } |
|
catch (e) { console.warn('Could not save dark mode preference:', e); } |
|
} else if (type === 'notification') { |
|
console.log("Notification setting toggled:", isChecked); |
|
alert(`通知設定未実装 (設定: ${isChecked ? 'ON' : 'OFF'})`); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
function handleLogout() { |
|
console.log("Logout button clicked"); |
|
|
|
alert("ログアウトしました。(開発中)"); |
|
goToInput(); |
|
} |
|
|
|
|
|
|
|
|
|
function applyDarkModePreference() { |
|
try { |
|
const darkModeEnabled = localStorage.getItem('darkModeEnabled') === 'true'; |
|
document.body.classList.toggle('dark-mode', darkModeEnabled); |
|
|
|
} catch (e) { console.warn('Could not load/apply dark mode preference:', e); } |
|
} |
|
|
|
|
|
|
|
const pathname = window.location.pathname; |
|
applyDarkModePreference(); |
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
console.log('DOM fully loaded. Path:', pathname); |
|
|
|
sideMenu = document.getElementById('side-menu'); |
|
menuOverlay = document.getElementById('menu-overlay'); |
|
if (!sideMenu || !menuOverlay) console.warn("Menu elements not found."); |
|
|
|
|
|
if (pathname.endsWith('/learning') || pathname.endsWith('/learning.html')) { |
|
correctEffect = document.getElementById('correct-effect'); |
|
if (!correctEffect) console.warn("Correct effect element not found."); |
|
initializeLearningScreen(); |
|
} |
|
|
|
|
|
if (pathname === '/' || pathname.endsWith('/input') || pathname.endsWith('/input.html')) { |
|
console.log("Initializing Input page..."); |
|
const form = document.getElementById('generate-form'); |
|
if (form) form.addEventListener('submit', (e) => { e.preventDefault(); handleGenerateSubmit(); }); |
|
else console.warn("Generate form not found."); |
|
const urlParams = new URLSearchParams(window.location.search); |
|
const initialUrl = urlParams.get('url'); |
|
if (initialUrl) { |
|
const urlInput = document.getElementById('youtube-url'); |
|
if (urlInput) urlInput.value = initialUrl; |
|
} |
|
} |
|
|
|
else if (pathname.endsWith('/history') || pathname.endsWith('/history.html')) { |
|
console.log("Initializing History page..."); |
|
|
|
} |
|
|
|
else if (pathname.endsWith('/settings') || pathname.endsWith('/settings.html')) { |
|
console.log("Initializing Settings page..."); |
|
try { |
|
const darkModeEnabled = localStorage.getItem('darkModeEnabled') === 'true'; |
|
const toggle = document.querySelector('input[type="checkbox"][onchange*="dark"]'); |
|
if (toggle) toggle.checked = darkModeEnabled; |
|
else console.warn("Dark mode toggle not found."); |
|
} catch (e) { console.warn('Could not set dark mode toggle state:', e); } |
|
} |
|
|
|
updateFooterNavActiveState(pathname); |
|
closeMenu(); |
|
}); |
|
|
|
|
|
|
|
|
|
|
|
function updateFooterNavActiveState(currentPath) { |
|
const footerNav = document.querySelector('.footer-nav'); |
|
if (!footerNav) return; |
|
const buttons = footerNav.querySelectorAll('button'); |
|
buttons.forEach(button => { |
|
button.classList.remove('active'); |
|
const onclickAttr = button.getAttribute('onclick'); |
|
if (onclickAttr) { |
|
if ((currentPath === '/' || currentPath.endsWith('/input') || currentPath.endsWith('/input.html')) && onclickAttr.includes('goToInput')) button.classList.add('active'); |
|
else if ((currentPath.endsWith('/history') || currentPath.endsWith('/history.html')) && onclickAttr.includes('goToHistory')) button.classList.add('active'); |
|
else if ((currentPath.endsWith('/settings') || currentPath.endsWith('/settings.html')) && onclickAttr.includes('goToSettings')) button.classList.add('active'); |
|
} |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|