Files
chengyu/app.js
M1ngdaXie fc1eb2040c
All checks were successful
Deploy / deploy (push) Successful in 2s
Refactor code structure for improved readability and maintainability
2026-03-27 01:39:09 -07:00

886 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const DAILY_GOAL = 5,
ROUND_SIZE = 5;
// === AUDIO with mobile unlock ===
var AudioCtx = window.AudioContext || window.webkitAudioContext;
var audioCtx = null,
audioUnlocked = false;
function unlockAudio() {
if (audioUnlocked || !audioCtx) return;
var buf = audioCtx.createBuffer(1, 1, 22050);
var src = audioCtx.createBufferSource();
src.buffer = buf;
src.connect(audioCtx.destination);
src.start(0);
if (audioCtx.state === "suspended") audioCtx.resume();
audioUnlocked = true;
}
function ensureAudio() {
if (!audioCtx) audioCtx = new AudioCtx();
unlockAudio();
}
document.addEventListener(
"touchstart",
function () {
ensureAudio();
},
{ once: true },
);
document.addEventListener(
"click",
function () {
ensureAudio();
},
{ once: true },
);
function playTone(f, d, t, v) {
if (!state.sound) return;
t = t || "sine";
v = v || 0.3;
try {
ensureAudio();
var o = audioCtx.createOscillator(),
g = audioCtx.createGain();
o.type = t;
o.frequency.value = f;
g.gain.setValueAtTime(v, audioCtx.currentTime);
g.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + d);
o.connect(g);
g.connect(audioCtx.destination);
o.start();
o.stop(audioCtx.currentTime + d);
} catch (e) {}
}
function sfxCorrect() {
playTone(523, 0.12);
setTimeout(function () {
playTone(659, 0.12);
}, 80);
setTimeout(function () {
playTone(784, 0.2);
}, 160);
}
function sfxWrong() {
playTone(200, 0.15, "square", 0.2);
setTimeout(function () {
playTone(160, 0.2, "square", 0.2);
}, 120);
}
function sfxClick() {
playTone(440, 0.06, "sine", 0.15);
}
function sfxCombo() {
playTone(523, 0.1);
setTimeout(function () {
playTone(659, 0.1);
}, 60);
setTimeout(function () {
playTone(784, 0.1);
}, 120);
setTimeout(function () {
playTone(1047, 0.25);
}, 180);
}
function sfxComplete() {
[523, 587, 659, 784, 1047].forEach(function (n, i) {
setTimeout(function () {
playTone(n, 0.2, "sine", 0.25);
}, i * 100);
});
}
// === STATE ===
const APP_VERSION = 2;
var state = {
sound: true,
theme: "ink",
difficulty: 1,
score: 0,
combo: 0,
maxCombo: 0,
todayCount: 0,
totalCount: 0,
streak: 0,
checkinDays: [],
round: 0,
roundResults: [],
currentIdiom: null,
blankIndices: [],
selectedBlank: -1,
filledChars: {},
hintUsed: false,
answered: false,
usedIdiomIndices: {},
wrongBook: [],
reviewMode: false,
};
var currentOptions = [];
function getToday() {
return new Date().toISOString().slice(0, 10);
}
function saveState() {
localStorage.setItem(
"chengyu_s",
JSON.stringify({
sound: state.sound,
theme: state.theme,
difficulty: state.difficulty,
totalCount: state.totalCount,
streak: state.streak,
checkinDays: state.checkinDays,
maxCombo: state.maxCombo,
}),
);
}
function saveWrongBook() {
localStorage.setItem("chengyu_wb", JSON.stringify(state.wrongBook));
}
function loadWrongBook() {
try {
state.wrongBook = JSON.parse(localStorage.getItem("chengyu_wb") || "[]");
} catch (e) {
state.wrongBook = [];
}
}
function addToWrongBook(idiom) {
var ex = null;
for (var i = 0; i < state.wrongBook.length; i++) {
if (state.wrongBook[i].w === idiom.w) {
ex = state.wrongBook[i];
break;
}
}
if (ex) {
ex.count++;
ex.last = getToday();
} else {
state.wrongBook.push({
w: idiom.w,
p: idiom.p,
e: idiom.e,
l: idiom.l,
count: 1,
last: getToday(),
});
}
saveWrongBook();
renderWbBadge();
}
function removeFromWrongBook(w) {
state.wrongBook = state.wrongBook.filter(function (x) {
return x.w !== w;
});
saveWrongBook();
renderWbBadge();
}
function renderWbBadge() {
var b = document.getElementById("wbBadge");
if (!b) return;
b.textContent = state.wrongBook.length;
b.className = "wb-badge" + (state.wrongBook.length === 0 ? " empty" : "");
}
function showWrongBookModal() {
document.getElementById("wrongBookModal").classList.add("show");
document.getElementById("wbCount").textContent = state.wrongBook.length;
var list = document.getElementById("wbList");
var startBtn = document.getElementById("btnStartReview");
if (!state.wrongBook.length) {
list.innerHTML = '<div class="wb-empty">🎉 暂无错题,梅子很棒!</div>';
startBtn.style.display = "none";
} else {
startBtn.style.display = "";
list.innerHTML = "";
var sorted = state.wrongBook.slice().sort(function (a, b) {
return b.count - a.count;
});
sorted.forEach(function (item) {
var d = document.createElement("div");
d.className = "wb-item";
d.innerHTML =
'<div class="wb-word">' +
item.w +
'</div><div class="wb-pinyin">' +
item.p +
'</div><div class="wb-explain">' +
item.e +
'</div><div class="wb-meta">错误 ' +
item.count +
" 次</div>";
list.appendChild(d);
});
}
}
function startReview() {
document.getElementById("wrongBookModal").classList.remove("show");
state.reviewMode = true;
state.usedIdiomIndices = {};
startRound();
}
function checkWhatsNew() {
var v = parseInt(localStorage.getItem("chengyu_v") || "0");
if (v < APP_VERSION) {
document.getElementById("whatsNewModal").classList.add("show");
}
}
function saveTodayState() {
localStorage.setItem(
"chengyu_t",
JSON.stringify({
d: getToday(),
s: state.score,
c: state.todayCount,
cb: state.combo,
}),
);
}
function loadState() {
try {
var s = JSON.parse(localStorage.getItem("chengyu_s") || "{}");
state.sound = s.sound !== undefined ? s.sound : true;
state.theme = s.theme || "ink";
state.totalCount = s.totalCount || 0;
state.streak = s.streak || 0;
state.checkinDays = s.checkinDays || [];
state.maxCombo = s.maxCombo || 0;
state.difficulty = s.difficulty || 1;
var t = JSON.parse(localStorage.getItem("chengyu_t") || "{}");
if (t.d === getToday()) {
state.score = t.s || 0;
state.todayCount = t.c || 0;
state.combo = t.cb || 0;
}
updateStreak();
} catch (e) {}
}
function updateStreak() {
var today = getToday(),
days = state.checkinDays;
if (!days.length) {
state.streak = 0;
return;
}
var sorted = days.slice().sort().reverse();
var streak = 0,
cd = new Date(today + "T00:00:00");
if (sorted[0] === today) {
for (var i = 0; i < sorted.length; i++) {
var e = new Date(cd);
e.setDate(e.getDate() - i);
if (sorted[i] === e.toISOString().slice(0, 10)) streak++;
else break;
}
} else {
var y = new Date(cd);
y.setDate(y.getDate() - 1);
var ys = y.toISOString().slice(0, 10);
if (sorted[0] === ys) {
for (var i = 0; i < sorted.length; i++) {
var e = new Date(y);
e.setDate(e.getDate() - i);
if (sorted[i] === e.toISOString().slice(0, 10)) streak++;
else break;
}
}
}
state.streak = streak;
}
function tryCheckin() {
var t = getToday();
if (state.todayCount >= DAILY_GOAL && state.checkinDays.indexOf(t) === -1) {
state.checkinDays.push(t);
if (state.checkinDays.length > 60)
state.checkinDays = state.checkinDays.slice(-60);
updateStreak();
saveState();
return true;
}
return false;
}
// === GAME LOGIC ===
function pickIdiom() {
if (state.reviewMode && state.wrongBook.length) {
var pool = state.wrongBook.filter(function (x) {
return !state.usedIdiomIndices[x.w];
});
if (!pool.length) {
state.usedIdiomIndices = {};
pool = state.wrongBook.slice();
}
var item = pool[Math.floor(Math.random() * pool.length)];
state.usedIdiomIndices[item.w] = 1;
return item;
}
var pool = IDIOMS.filter(function (i) {
return i.l <= state.difficulty && !state.usedIdiomIndices[i.w];
});
if (!pool.length) {
state.usedIdiomIndices = {};
pool = IDIOMS.filter(function (i) { return i.l <= state.difficulty; });
}
var item = pool[Math.floor(Math.random() * pool.length)];
state.usedIdiomIndices[item.w] = 1;
return item;
}
function generateBlanks(w, d) {
var n = d === 1 ? 1 : d === 2 ? 2 : 3;
var idx = [0, 1, 2, 3];
for (var i = 3; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var tmp = idx[i];
idx[i] = idx[j];
idx[j] = tmp;
}
return idx.slice(0, n).sort();
}
function generateOptions(idiom, blanks) {
var correct = blanks.map(function (i) {
return idiom.w[i];
});
var decoyN = blanks.length <= 1 ? 3 : blanks.length === 2 ? 4 : 5;
var allC = {};
IDIOMS.forEach(function (it) {
for (var i = 0; i < it.w.length; i++) allC[it.w[i]] = 1;
});
var pool = Object.keys(allC).filter(function (c) {
return correct.indexOf(c) === -1;
});
var decoys = [],
used = {};
correct.forEach(function (c) {
used[c] = 1;
});
while (decoys.length < decoyN && pool.length) {
var i = Math.floor(Math.random() * pool.length);
if (!used[pool[i]]) {
decoys.push(pool[i]);
used[pool[i]] = 1;
}
pool.splice(i, 1);
}
var opts = correct.concat(decoys);
for (var i = opts.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var tmp = opts[i];
opts[i] = opts[j];
opts[j] = tmp;
}
return opts;
}
// === RENDER ===
function renderStats() {
document.getElementById("statScore").textContent = state.score;
document.getElementById("statCombo").textContent = state.combo;
document.getElementById("statTotal").textContent = state.totalCount;
document.getElementById("statStreak").textContent = state.streak;
var flame = document.getElementById("flameIcon");
flame.className = "";
if (state.combo >= 8) flame.className = "flame-high";
else if (state.combo >= 5) flame.className = "flame-mid";
else if (state.combo >= 3) flame.className = "flame-low";
}
function renderProgress() {
var bar = document.getElementById("progressBar");
bar.innerHTML = "";
for (var i = 0; i < ROUND_SIZE; i++) {
var d = document.createElement("div");
d.className =
"progress-dot" +
(i < state.round
? state.roundResults[i]
? " done"
: " skip"
: i === state.round
? " current"
: "");
bar.appendChild(d);
}
}
function renderIdiom() {
var el = document.getElementById("idiomDisplay");
el.innerHTML = "";
var idiom = state.currentIdiom,
pp = idiom.p.split(" ");
for (var i = 0; i < idiom.w.length; i++) {
var box = document.createElement("div");
box.className = "char-box";
if (state.blankIndices.indexOf(i) !== -1) {
box.classList.add("blank");
if (state.filledChars[i]) {
box.classList.remove("blank");
box.classList.add("filled");
box.textContent = state.filledChars[i];
} else {
box.textContent = "?";
if (state.selectedBlank === i) box.classList.add("selected");
}
(function (idx) {
box.addEventListener("click", function () {
selectBlank(idx);
});
})(i);
} else {
box.textContent = idiom.w[i];
var py = document.createElement("span");
py.className = "pinyin-hint";
py.textContent = pp[i] || "";
box.appendChild(py);
}
el.appendChild(box);
}
}
function renderOptions(opts) {
var grid = document.getElementById("optionsGrid");
grid.innerHTML = "";
var filled = Object.keys(state.filledChars).map(function (k) {
return state.filledChars[k];
});
opts.forEach(function (ch) {
var btn = document.createElement("button");
btn.className = "option-btn";
btn.textContent = ch;
if (filled.indexOf(ch) !== -1) btn.classList.add("used");
if (state.answered) btn.classList.add("disabled");
btn.addEventListener("click", function () {
selectOption(ch);
});
grid.appendChild(btn);
});
}
function renderCombo() {
var el = document.getElementById("comboDisplay");
var toast = document.getElementById("comboToast");
if (state.combo >= 3) {
var msgs = [
"梅子不错哦~",
"太棒了梅子!",
"梅子真厉害!",
"学富五车!",
"梅子无人能挡!",
"登峰造极!",
"出口成章!",
"满腹经纶!",
"博古通今!",
"梅子开挂了!",
"无可匹敌!",
"梅子是成语王!",
];
var i = Math.min(Math.floor((state.combo - 3) / 2), msgs.length - 1);
var txt = "🔥 连击 ×" + state.combo + " " + msgs[i];
el.innerHTML = '<span class="combo-text">' + txt + "</span>";
if (toast) {
toast.textContent = txt;
toast.classList.add("show");
clearTimeout(window._comboTimer);
window._comboTimer = setTimeout(function () {
toast.classList.remove("show");
}, 2200);
}
} else {
el.innerHTML = "";
if (toast) toast.classList.remove("show");
}
}
// === ACTIONS ===
function startRound() {
state.round = 0;
state.roundResults = [];
state.usedIdiomIndices = {};
nextQuestion();
}
function nextQuestion() {
if (state.round >= ROUND_SIZE) {
finishRound();
return;
}
state.currentIdiom = pickIdiom();
state.blankIndices = generateBlanks(state.currentIdiom.w, state.difficulty);
state.selectedBlank = state.blankIndices[0];
state.filledChars = {};
state.hintUsed = false;
state.answered = false;
currentOptions = generateOptions(state.currentIdiom, state.blankIndices);
document.getElementById("btnNext").style.display = "none";
document.getElementById("btnHint").style.display = "";
document.getElementById("btnHint").style.opacity = "1";
document.getElementById("btnHint").style.pointerEvents = "";
document.getElementById("btnSkip").style.display = "";
document.getElementById("resultArea").innerHTML = "";
document.getElementById("optLabel").style.display = "";
renderProgress();
renderIdiom();
renderOptions(currentOptions);
renderCombo();
renderStats();
}
function selectBlank(i) {
if (state.answered) return;
if (state.filledChars[i]) {
delete state.filledChars[i];
state.selectedBlank = i;
sfxClick();
renderIdiom();
renderOptions(currentOptions);
return;
}
state.selectedBlank = i;
sfxClick();
renderIdiom();
}
function selectOption(ch) {
if (state.answered || state.selectedBlank === -1) return;
sfxClick();
state.filledChars[state.selectedBlank] = ch;
var next = -1;
for (var j = 0; j < state.blankIndices.length; j++) {
if (!state.filledChars[state.blankIndices[j]]) {
next = state.blankIndices[j];
break;
}
}
state.selectedBlank = next;
renderIdiom();
renderOptions(currentOptions);
var allFilled = true;
for (var j = 0; j < state.blankIndices.length; j++) {
if (!state.filledChars[state.blankIndices[j]]) {
allFilled = false;
break;
}
}
if (allFilled) checkAnswer();
}
function checkAnswer() {
state.answered = true;
var idiom = state.currentIdiom,
ok = true;
state.blankIndices.forEach(function (i) {
if (state.filledChars[i] !== idiom.w[i]) ok = false;
});
var boxes = document.querySelectorAll(".char-box");
state.blankIndices.forEach(function (i) {
boxes[i].classList.remove("filled");
boxes[i].classList.add(
state.filledChars[i] === idiom.w[i] ? "correct" : "wrong",
);
});
if (ok) {
state.combo++;
if (state.combo > state.maxCombo) state.maxCombo = state.combo;
state.score += Math.round(
state.difficulty *
10 *
(state.hintUsed ? 0.5 : 1) *
(1 + state.combo * 0.1),
);
state.todayCount++;
state.totalCount++;
state.roundResults.push(true);
state.combo >= 5 ? sfxCombo() : sfxCorrect();
if (state.reviewMode) removeFromWrongBook(idiom.w);
var nc = tryCheckin();
showResult(true, nc);
} else {
state.combo = 0;
state.roundResults.push(false);
sfxWrong();
addToWrongBook(idiom);
setTimeout(function () {
state.blankIndices.forEach(function (i) {
boxes[i].textContent = idiom.w[i];
boxes[i].classList.remove("wrong");
boxes[i].classList.add("correct");
});
}, 800);
showResult(false, false);
}
renderOptions(currentOptions);
renderCombo();
renderStats();
renderProgress();
saveTodayState();
saveState();
document.getElementById("btnNext").style.display = "";
document.getElementById("btnHint").style.display = "none";
document.getElementById("btnSkip").style.display = "none";
}
function showResult(ok, nc) {
var idiom = state.currentIdiom,
a = document.getElementById("resultArea");
var ch = "";
if (nc)
ch =
'<div style="margin-top:12px;padding:10px;background:linear-gradient(135deg,#FFF8E1,#FFECB3);border-radius:10px;text-align:center"><span style="font-size:1.2rem">🎉</span> <b>梅子今日打卡成功!</b> 连续 ' +
state.streak +
" 天</div>";
var pg = "";
if (!nc && state.todayCount < DAILY_GOAL)
pg =
'<div style="margin-top:8px;font-size:.8rem;color:var(--text2)">今日进度:' +
state.todayCount +
"/" +
DAILY_GOAL +
" 题(完成" +
DAILY_GOAL +
"题即可打卡)</div>";
a.innerHTML =
'<div class="result-card' +
(ok ? "" : " wrong-result") +
'"><h3 style="color:' +
(ok ? "var(--green)" : "var(--red)") +
'">' +
(ok ? [
"✅ 回答正确!",
"✅ 太棒了!",
"✅ 梅子答对了!",
"✅ 完全正确!",
"✅ 厉害!",
"✅ 对!梅子真棒!",
][Math.floor(Math.random() * 6)] : [
"❌ 答错了~",
"❌ 没关系,记住它!",
"❌ 这次没中,下次加油!",
"❌ 差一点点~",
][Math.floor(Math.random() * 4)]) +
(ok && state.combo >= 3 ? " 🔥" : "") +
'</h3><div class="idiom-word">' +
idiom.w +
'</div><div class="idiom-pinyin">' +
idiom.p +
'</div><div class="idiom-explain"><b>释义:</b>' +
idiom.e +
"</div>" +
ch +
pg +
"</div>";
}
function finishRound() {
var c = state.roundResults.filter(function (r) {
return r;
}).length;
sfxComplete();
var wasReview = state.reviewMode;
state.reviewMode = false;
var msg =
c === ROUND_SIZE
? [
"满分!梅子太厉害了!🎉",
"完美!梅子真是成语达人!✨",
"全对!梅子今天状态绝佳!🌟",
"漂亮!一题不差,梅子威武!👏",
][Math.floor(Math.random() * 4)]
: c >= 3
? [
"梅子表现不错!继续加油~",
"答得很好,梅子越来越厉害了!",
"不错不错,梅子进步了!🌸",
"梅子答题越来越顺了,加油!",
][Math.floor(Math.random() * 4)]
: [
"没关系梅子,多练习就会进步!💪",
"继续努力,梅子肯定能行!",
"不怕不怕,熟能生巧!加油梅子~",
"错了没关系,下次梅子一定更好!",
][Math.floor(Math.random() * 4)];
var reviewBtn =
wasReview && state.wrongBook.length
? '<button class="action-btn secondary" onclick="showWrongBookModal()" style="margin-top:8px;width:100%">再次练习错题 📖</button>'
: "";
document.getElementById("resultArea").innerHTML =
'<div class="result-card" style="border-top-color:var(--gold);text-align:center"><h3 style="color:var(--gold)">' +
(wasReview ? "📖 错题练习结束!" : "🏆 本轮结束!") +
'</h3><div style="font-size:2rem;font-weight:900;color:var(--accent);margin:12px 0;font-family:ZCOOL KuaiLe,cursive">' +
c +
" / " +
ROUND_SIZE +
'</div><div style="color:var(--text2);margin-bottom:16px">' +
msg +
'</div><button class="action-btn primary" onclick="startRound()" style="font-size:1rem;padding:12px 0;width:100%">再来一轮 →</button>' +
reviewBtn +
"</div>";
document.getElementById("btnNext").style.display = "none";
document.getElementById("btnHint").style.display = "none";
document.getElementById("btnSkip").style.display = "none";
document.getElementById("idiomDisplay").innerHTML = "";
document.getElementById("optionsGrid").innerHTML = "";
document.getElementById("optLabel").style.display = "none";
}
function useHint() {
if (state.answered || state.hintUsed) return;
state.hintUsed = true;
var ub = [];
for (var j = 0; j < state.blankIndices.length; j++) {
if (!state.filledChars[state.blankIndices[j]])
ub.push(state.blankIndices[j]);
}
if (!ub.length) return;
var hi = ub[0],
pp = state.currentIdiom.p.split(" "),
boxes = document.querySelectorAll(".char-box"),
py = document.createElement("span");
py.className = "pinyin-hint";
py.textContent = pp[hi] || "";
py.style.color = "var(--accent)";
boxes[hi].appendChild(py);
sfxClick();
document.getElementById("btnHint").style.opacity = ".5";
document.getElementById("btnHint").style.pointerEvents = "none";
}
function skipQuestion() {
if (state.answered) return;
state.combo = 0;
state.roundResults.push(false);
state.round++;
renderCombo();
renderProgress();
nextQuestion();
}
function showCheckinModal() {
document.getElementById("checkinModal").classList.add("show");
document.getElementById("modalStreak").textContent = state.streak;
var tc = document.getElementById("modalTodayCount");
var gf = document.getElementById("modalGoalFill");
if (tc) tc.textContent = state.todayCount;
if (gf)
gf.style.width =
Math.min(100, Math.round((state.todayCount / DAILY_GOAL) * 100)) + "%";
var cal = document.getElementById("modalCalendar");
cal.innerHTML = "";
var today = new Date(),
ts = getToday();
["一", "二", "三", "四", "五", "六", "日"].forEach(function (l) {
var d = document.createElement("div");
d.style.cssText =
"font-size:.65rem;color:#8B6914;display:flex;align-items:center;justify-content:center";
d.textContent = l;
cal.appendChild(d);
});
var start = new Date(today);
start.setDate(start.getDate() - 27);
while (start.getDay() !== 1) start.setDate(start.getDate() - 1);
var end = new Date(today);
end.setDate(end.getDate() + ((7 - today.getDay()) % 7));
var cur = new Date(start);
while (cur <= end) {
var ds = cur.toISOString().slice(0, 10),
d = document.createElement("div");
d.className = "cal-day";
d.textContent = cur.getDate();
if (state.checkinDays.indexOf(ds) !== -1) d.classList.add("checked");
if (ds === ts) d.classList.add("today");
if (cur > today) d.style.opacity = ".3";
cal.appendChild(d);
cur.setDate(cur.getDate() + 1);
}
}
// === INIT ===
function init() {
loadState();
loadWrongBook();
renderWbBadge();
document.documentElement.setAttribute("data-theme", state.theme);
document.getElementById("btnSound").classList.toggle("active", state.sound);
document.getElementById("btnSound").textContent = state.sound ? "🔊" : "🔇";
document.getElementById("btnTheme").textContent = state.theme === "ink" ? "🌙" : "☀️";
showThemeTooltip();
document.querySelectorAll(".diff-btn").forEach(function (b) {
b.classList.toggle("active", +b.dataset.diff === state.difficulty);
});
renderStats();
startRound();
setTimeout(checkWhatsNew, 800);
}
document.getElementById("btnSound").addEventListener("click", function () {
state.sound = !state.sound;
this.classList.toggle("active", state.sound);
this.textContent = state.sound ? "🔊" : "🔇";
saveState();
});
function showThemeTooltip() {
if (localStorage.getItem("chengyu_theme_tip")) return;
var tip = document.getElementById("themeTooltip");
if (!tip) return;
setTimeout(function () {
tip.classList.add("show");
var timer = setTimeout(dismissThemeTooltip, 5000);
tip.addEventListener("click", function () {
clearTimeout(timer);
dismissThemeTooltip();
}, { once: true });
}, 1200);
}
function dismissThemeTooltip() {
var tip = document.getElementById("themeTooltip");
if (tip) tip.classList.remove("show");
localStorage.setItem("chengyu_theme_tip", "1");
}
document.getElementById("btnTheme").addEventListener("click", function () {
dismissThemeTooltip();
state.theme = state.theme === "ink" ? "paper" : "ink";
document.documentElement.setAttribute("data-theme", state.theme);
this.textContent = state.theme === "ink" ? "🌙" : "☀️";
saveState();
});
document
.getElementById("btnCheckin")
.addEventListener("click", showCheckinModal);
document.getElementById("closeModal").addEventListener("click", function () {
document.getElementById("checkinModal").classList.remove("show");
});
document.getElementById("checkinModal").addEventListener("click", function (e) {
if (e.target === document.getElementById("checkinModal"))
document.getElementById("checkinModal").classList.remove("show");
});
document
.getElementById("btnWrongBook")
.addEventListener("click", showWrongBookModal);
document
.getElementById("closeWrongBook")
.addEventListener("click", function () {
document.getElementById("wrongBookModal").classList.remove("show");
});
document
.getElementById("wrongBookModal")
.addEventListener("click", function (e) {
if (e.target === document.getElementById("wrongBookModal"))
document.getElementById("wrongBookModal").classList.remove("show");
});
document
.getElementById("btnStartReview")
.addEventListener("click", startReview);
document.getElementById("closeWhatsNew").addEventListener("click", function () {
document.getElementById("whatsNewModal").classList.remove("show");
localStorage.setItem("chengyu_v", APP_VERSION);
});
document
.getElementById("whatsNewModal")
.addEventListener("click", function (e) {
if (e.target === document.getElementById("whatsNewModal")) {
document.getElementById("whatsNewModal").classList.remove("show");
localStorage.setItem("chengyu_v", APP_VERSION);
}
});
document.querySelectorAll(".diff-btn").forEach(function (b) {
b.addEventListener("click", function () {
document.querySelectorAll(".diff-btn").forEach(function (x) {
x.classList.remove("active");
});
this.classList.add("active");
state.difficulty = +this.dataset.diff;
state.combo = 0;
saveState();
startRound();
});
});
document.getElementById("btnHint").addEventListener("click", useHint);
document.getElementById("btnSkip").addEventListener("click", skipQuestion);
document.getElementById("btnNext").addEventListener("click", function () {
state.round++;
document.getElementById("optLabel").style.display = "";
nextQuestion();
});
init();