index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=5.0,user-scalable=yes,viewport-fit=cover">
<title>汎用暗記修行システム</title>
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<style>
:root {
--bg: #030712;
--container: #111827;
--card-bg: #1f2937;
--text: #f9fafb;
--primary: #3b82f6;
--success: #10b981;
--danger: #ef4444;
--gray: #6b7280;
--accent: #8b5cf6;
--gold: #fbbf24;
--silver: #94a3b8;
}
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; outline: none; }
html, body {
margin: 0; width: 100%; height: 100%; background: var(--bg);
font-family: 'Inter', system-ui, -apple-system, sans-serif; color: var(--text);
overflow-x: hidden; touch-action: manipulation;
}
#setup, #report { padding: 20px; min-height: 100vh; overflow-y: auto; display: none; }
#setup.active, #report.active { display: block; }
#app { display: none; position: fixed; inset: 0; background: var(--bg); flex-direction: column; padding: 12px; z-index: 100; }
#app.active { display: flex; }
.container { max-width: 650px; margin: 0 auto; background: var(--container); border-radius: 28px; padding: 20px; border: 1px solid #374151; box-shadow: 0 20px 40px rgba(0,0,0,0.6); }
h1 { font-size: 24px; text-align: center; margin: 0 0 15px 0; color: var(--primary); font-weight: 900; letter-spacing: 1px; }
.dashboard { background: rgba(31, 41, 55, 0.5); border-radius: 24px; padding: 18px; margin-bottom: 20px; border: 1px solid #374151; }
.db-bar { height: 14px; display: flex; border-radius: 7px; overflow: hidden; background: #030712; margin-bottom: 12px; }
.db-seg { height: 100%; transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1); }
.seg-a { background: var(--gold); }
.seg-b { background: var(--accent); }
.seg-c { background: var(--danger); }
.seg-new { background: #4b5563; }
.db-legend { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
.lg-item { text-align: center; }
.lg-label { font-size: 10px; color: var(--gray); display: block; margin-bottom: 2px; }
.lg-val { font-size: 12px; font-weight: 900; }
.label { font-size: 11px; font-weight: 800; color: var(--silver); margin: 16px 0 6px 4px; display: flex; justify-content: space-between; align-items: center; }
.param-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 14px; }
@media (max-width: 420px) { .param-grid { grid-template-columns: 1fr; } }
.input-group { background: #1f2937; padding: 12px; border-radius: 18px; border: 1px solid #374151; }
.input-group span { font-size: 10px; margin-bottom: 6px; color: #9ca3af; display: block; font-weight: bold; }
.adj-container { display: flex; align-items: center; justify-content: center; gap: 10px; }
.adj-btn { background: #334155; color: white; border: none; padding: 10px 14px; border-radius: 12px; font-size: 13px; font-weight: 800; cursor: pointer; min-width: 44px; }
.adj-btn:active { transform: scale(0.9); background: var(--primary); }
input { width: 75px; background: transparent; border: none; color: white; font-size: 20px; font-weight: 900; text-align: center; }
input::-webkit-outer-spin-button, input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.range-box { background: linear-gradient(145deg, #1e1b4b, #111827); padding: 18px; border-radius: 24px; border: 1px solid var(--accent); margin-bottom: 18px; }
textarea { width: 100%; height: 55px; background: rgba(0,0,0,0.4); color: #94a3b8; border: 1px solid #374151; border-radius: 14px; padding: 12px; font-size: 12px; margin-top: 6px; outline: none; resize: none; word-break: break-all; }
button.main-btn { width: 100%; height: 64px; border: none; border-radius: 22px; font-size: 22px; font-weight: 900; cursor: pointer; background: var(--primary); color: white; transition: 0.2s; box-shadow: 0 12px 24px -6px rgba(59, 130, 246, 0.5); }
button.main-btn:active { transform: scale(0.97); }
.sm-btn { background: #374151; color: white; border: none; padding: 7px 14px; border-radius: 10px; font-size: 11px; cursor: pointer; font-weight: 800; }
.io-btn { background: var(--accent); color: white; border: none; padding: 7px 14px; border-radius: 10px; font-size: 11px; font-weight: 800; cursor: pointer; }
#card { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 25px; gap: 10px; background: var(--card-bg); border-radius: 48px; margin: 10px 0; position: relative; border: 1px solid #374151; box-shadow: inset 0 0 40px rgba(0,0,0,0.4); }
#itemBadge { position: absolute; top: 30px; font-size: 20px; font-weight: 900; padding: 4px 18px; border-radius: 12px; background: rgba(0,0,0,0.3); border: 2px solid currentColor; }
#questionText { font-size: 40px; font-weight: 900; line-height: 1.1; color: var(--text); word-break: break-all; max-width: 95%; margin-top: 30px; }
#answerText { font-size: 24px; color: var(--primary); font-weight: 800; visibility: hidden; min-height: 1.5em; margin-top: 15px; }
#answerText.visible { visibility: visible; }
#timerTrack { width: 100%; height: 10px; background: #030712; margin-top: 25px; border-radius: 10px; overflow: hidden; opacity: 0; }
#timerTrack.active { opacity: 1; }
#timerBar { height: 100%; background: var(--danger); width: 0%; }
.progress-container { width: 100%; height: 8px; background: #1f2937; border-radius: 10px; overflow: hidden; margin: 8px 0; }
#progressBar { height: 100%; background: var(--primary); width: 0%; transition: width 0.3s linear; }
.ans-btns { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; width: 100%; margin-bottom: 12px; }
.ans-btn { height: 90px; border: none; border-radius: 32px; color: white; font-size: 26px; font-weight: 900; cursor: pointer; transition: 0.15s; }
.ans-btn:active { transform: scale(0.94); }
#correctBtn { background: var(--success); }
#wrongBtn { background: var(--danger); }
</style>
</head>
<body>
<div id="setup" class="active">
<div class="container">
<h1 id="titleLabel">修行システム</h1>
<div class="dashboard">
<div class="db-bar">
<div id="bar-a" class="db-seg seg-a"></div>
<div id="bar-b" class="db-seg seg-b"></div>
<div id="bar-c" class="db-seg seg-c"></div>
<div id="bar-new" class="db-seg seg-new"></div>
</div>
<div class="db-legend">
<div class="lg-item"><span class="lg-label">完了(a)</span><span id="val-a" class="lg-val" style="color:var(--gold)">0</span></div>
<div class="lg-item"><span class="lg-label">継続(b)</span><span id="val-b" class="lg-val" style="color:var(--accent)">0</span></div>
<div class="lg-item"><span class="lg-label">苦手(c)</span><span id="val-c" class="lg-val" style="color:var(--danger)">0</span></div>
<div class="lg-item"><span class="lg-label">未着手</span><span id="val-new" class="lg-val" style="color:var(--silver)">0</span></div>
</div>
</div>
<div class="input-group" style="margin-bottom:15px; background: #1e1b4b;">
<div style="display:flex; justify-content: space-between; align-items: center; margin-bottom:6px;">
<span style="color:#fff">進捗復元 / 書き出し</span>
<div style="display:flex; gap:6px;">
<button class="io-btn" style="background: var(--success);" onclick="importData()">復元</button>
<button class="io-btn" onclick="exportData()">出す</button>
</div>
</div>
<textarea id="importArea" oninput="updateDashboard()" placeholder="保存用データを貼り付けて「復元」"></textarea>
</div>
<div class="range-box">
<div class="label">修行範囲と出題数</div>
<div class="param-grid">
<div class="input-group">
<span>開始ID</span>
<div class="adj-container"><button class="adj-btn" onclick="adj('rangeStart', -100)">-</button><input type="number" id="rangeStart" value="1"><button class="adj-btn" onclick="adj('rangeStart', 100)">+</button></div>
</div>
<div class="input-group">
<span>終了ID</span>
<div class="adj-container"><button class="adj-btn" onclick="adj('rangeEnd', -100)">-</button><input type="number" id="rangeEnd" value="100"><button class="adj-btn" onclick="adj('rangeEnd', 100)">+</button></div>
</div>
</div>
<div class="input-group" style="background: rgba(255,255,255,0.05); margin-top:8px;">
<span>今回の修行数</span>
<div class="adj-container"><button class="adj-btn" onclick="adj('testLimit', -10)">-10</button><input type="number" id="testLimit" value="20"><button class="adj-btn" onclick="adj('testLimit', 10)">+10</button></div>
</div>
</div>
<div class="label">テンポ設定</div>
<div class="param-grid">
<div class="input-group"><span>解答猶予(秒)</span><input type="number" id="limitTime" value="1.5" step="0.1"></div>
<div class="input-group"><span>自動読み上げ</span><input type="checkbox" id="voiceToggle" checked></div>
</div>
<button class="main-btn" onclick="startTraining()">修行開始</button>
</div>
</div>
<div id="app">
<div id="infoText" style="text-align:center; font-size:11px; color:var(--gray); margin-bottom:4px; font-weight:bold;"></div>
<div class="progress-container"><div id="progressBar"></div></div>
<div id="card">
<div id="itemBadge">NEW</div>
<div id="questionText"></div>
<div id="answerText"></div>
<div id="timerTrack"><div id="timerBar"></div></div>
</div>
<div class="ans-btns">
<button id="wrongBtn" class="ans-btn" onclick="processAnswer(false)">苦手 (A)</button>
<button id="correctBtn" class="ans-btn" onclick="processAnswer(true)">わかる (Z)</button>
</div>
<div style="text-align:center;">
<button onclick="finishTraining()" style="background:none; border:none; color:var(--gray); font-size:11px; font-weight:bold; cursor:pointer;">中断 (Space)</button>
</div>
</div>
<div id="report">
<div class="container" style="text-align:center;">
<h1>修行完了</h1>
<div id="resultScore" style="font-size:32px; font-weight:900; color:var(--success); margin-bottom:20px;"></div>
<textarea id="numericalArea" style="height:70px;" readonly></textarea>
<button class="main-btn" onclick="location.reload()" style="margin-top:25px; background:var(--gray);">トップへ戻る</button>
</div>
</div>
<script>
// --- [設定エリア] アプリ名やデータを定義 ---
const APP_TITLE = "一般暗記修行システム";
const STORAGE_KEY = 'memorize_mastery_template';
// ----------------------------------------
let ITEMS = [];
let mastery = {};
let queue = [];
let results = [];
let activeItem = null;
let stressTimer = null;
let audioCtx = null;
function playSound(type) {
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.type = type === 'correct' ? 'sine' : 'square';
osc.frequency.setValueAtTime(type === 'correct' ? 880 : 220, audioCtx.currentTime);
gain.gain.setValueAtTime(0.05, audioCtx.currentTime);
gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 0.2);
osc.start();
osc.stop(audioCtx.currentTime + 0.2);
}
function loadData() {
const raw = document.getElementById('raw-data').textContent.trim();
ITEMS = raw.split('\n').filter(l => l.includes(',')).map((line, idx) => {
const parts = line.split(',');
return { id: idx + 1, q: parts[0].trim(), a: parts[1].trim() };
});
document.getElementById('rangeEnd').value = ITEMS.length;
document.getElementById('titleLabel').textContent = APP_TITLE;
}
function updateDashboard() {
const total = ITEMS.length || 1;
const counts = { a: 0, b: 0, c: 0, new: 0 };
Object.values(mastery).forEach(v => counts[v]++);
counts.new = Math.max(0, total - (counts.a + counts.b + counts.c));
['a', 'b', 'c', 'new'].forEach(k => {
const val = counts[k];
const pct = (val / total * 100).toFixed(1);
document.getElementById(`bar-${k}`).style.width = pct + '%';
document.getElementById(`val-${k}`).textContent = `${val} (${pct}%)`;
});
}
function speak(txt, lang) {
if (!document.getElementById('voiceToggle').checked) return;
window.speechSynthesis.cancel();
const ut = new SpeechSynthesisUtterance(txt);
ut.lang = lang;
ut.rate = 1.2;
window.speechSynthesis.speak(ut);
}
function startTraining() {
const start = parseInt(document.getElementById('rangeStart').value);
const end = parseInt(document.getElementById('rangeEnd').value);
const limit = parseInt(document.getElementById('testLimit').value);
let available = ITEMS.filter(item => item.id >= start && item.id <= end);
if (available.length === 0) return alert("範囲内にデータがありません");
available.sort(() => Math.random() - 0.5);
queue = available.slice(0, limit);
results = [];
document.getElementById("setup").classList.remove('active');
document.getElementById("app").classList.add('active');
nextItem();
}
function nextItem() {
clearTimeout(stressTimer);
const track = document.getElementById("timerTrack");
const bar = document.getElementById("timerBar");
track.classList.remove('active');
bar.style.transition = "none";
bar.style.width = "0%";
document.getElementById("answerText").classList.remove('visible');
if (queue.length === 0) return finishTraining();
activeItem = queue.shift();
document.getElementById("infoText").textContent = `ID:${activeItem.id} | ${results.length+1} / ${results.length + queue.length + 1}`;
document.getElementById("progressBar").style.width = `${(results.length / (results.length + queue.length + 1)) * 100}%`;
const status = mastery[activeItem.id];
const badge = document.getElementById("itemBadge");
badge.textContent = status ? status.toUpperCase() : "NEW";
badge.style.color = status === 'c' ? "var(--danger)" : (status === 'b' ? "var(--accent)" : (status === 'a' ? "var(--gold)" : "var(--gray)"));
document.getElementById("questionText").textContent = activeItem.q;
document.getElementById("answerText").textContent = activeItem.a;
speak(activeItem.q, 'en-US'); // 必要に応じて言語設定を変更
// 解答表示とタイマー開始
setTimeout(() => {
document.getElementById("answerText").classList.add('visible');
speak(activeItem.a, 'ja-JP');
startStressTimer();
}, 800);
}
function startStressTimer() {
const sec = parseFloat(document.getElementById('limitTime').value) || 1.5;
const track = document.getElementById("timerTrack");
const bar = document.getElementById("timerBar");
track.classList.add('active');
bar.style.transition = `width ${sec}s linear`;
bar.style.width = "100%";
stressTimer = setTimeout(() => processAnswer(false), sec * 1000);
}
function processAnswer(isOk) {
clearTimeout(stressTimer);
if (!activeItem) return;
playSound(isOk ? 'correct' : 'wrong');
const id = activeItem.id;
if (isOk) {
mastery[id] = (mastery[id] === 'c') ? 'b' : 'a';
} else {
mastery[id] = 'c';
}
results.push({ ...activeItem, ok: isOk });
localStorage.setItem(STORAGE_KEY, JSON.stringify(mastery));
nextItem();
}
function finishTraining() {
window.speechSynthesis.cancel();
document.getElementById("app").classList.remove('active');
document.getElementById("report").classList.add('active');
const okCount = results.filter(r => r.ok).length;
document.getElementById("resultScore").textContent = `${okCount} / ${results.length}`;
document.getElementById("numericalArea").value = Object.keys(mastery).map(id=>`${id}${mastery[id]}`).join(',');
}
function adj(id, delta) {
const el = document.getElementById(id);
el.value = Math.max(1, parseInt(el.value) + delta);
updateDashboard();
}
function exportData() {
const s = Object.keys(mastery).map(id=>`${id}${mastery[id]}`).join(',');
document.getElementById('importArea').value = s;
navigator.clipboard.writeText(s).then(() => alert("進捗をコピーしました"));
}
function importData() {
const txt = document.getElementById('importArea').value.trim();
mastery = {};
txt.split(',').forEach(t => {
const m = t.match(/(\d+)([abc])/);
if (m) mastery[parseInt(m[1])] = m[2];
});
localStorage.setItem(STORAGE_KEY, JSON.stringify(mastery));
updateDashboard();
alert("進捗を復元しました");
}
window.onload = () => {
loadData();
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) mastery = JSON.parse(saved);
updateDashboard();
};
document.onkeydown = (e) => {
if (!document.getElementById("app").classList.contains('active')) return;
if (e.key.toLowerCase() === 'z') processAnswer(true);
if (e.key.toLowerCase() === 'a') processAnswer(false);
if (e.code === 'Space') { e.preventDefault(); finishTraining(); }
};
</script>
<script type="text/plain" id="raw-data">
質問,回答
Apple,りんご
Book,本
Car,車
Dog,犬
</script>
</body>
</html>