Files
jap_web_ng/dist/assets/index-Ban37OPN.js
T

111 lines
270 KiB
JavaScript
Raw Normal View History

2026-09-15 16:10:52 +03:00
(function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const s of document.querySelectorAll('link[rel="modulepreload"]'))o(s);new MutationObserver(s=>{for(const c of s)if(c.type==="childList")for(const u of c.addedNodes)u.tagName==="LINK"&&u.rel==="modulepreload"&&o(u)}).observe(document,{childList:!0,subtree:!0});function a(s){const c={};return s.integrity&&(c.integrity=s.integrity),s.referrerPolicy&&(c.referrerPolicy=s.referrerPolicy),s.crossOrigin==="use-credentials"?c.credentials="include":s.crossOrigin==="anonymous"?c.credentials="omit":c.credentials="same-origin",c}function o(s){if(s.ep)return;s.ep=!0;const c=a(s);fetch(s.href,c)}})();const Se=new Worker(new URL("/assets/db-worker-DFFhVJkA.js",import.meta.url),{type:"module"});let dr=0,je=null;const We=new Map;function _n(){return je||(je=new Promise((e,n)=>{Se.onmessage=o=>{const s=o.data;if(s.type==="ready")return;const c=We.get(s.id);c&&(We.delete(s.id),s.type=="success"?c.resolve(s.data):c.reject(new Error(s.error??s.message??"Неизвестная ошибка Worker")))},Se.onerror=o=>{console.error("[SQLite Worker] Ошибка:",o),n(new Error("Ошибка Worker"))};const a=++dr;We.set(a,{resolve:e,reject:n}),Se.postMessage({type:"start",id:a})}),je)}function fr(e,n){return new Promise((a,o)=>{if(!Se){o(new Error("Worker не создан"));return}const s=++dr;We.set(s,{resolve:a,reject:o}),Se.postMessage({type:e,id:s,...n})})}async function pe(e,n){return await fr("query",{sql:e,params:n??[]})}async function Pn(e,n){await fr("command",{sql:e,params:n??[]})}function Sn(e){if(e==null)return[];const n=String(e).trim();return n?n.split(/[,\s;]+/).map(a=>a.trim()).filter(a=>a.length>0):[]}function Rn(e){const n=new Map;if(e==null)return n;const a=String(e);if(!a)return n;const o=a.split("");for(let s=0;s+1<o.length;s+=2){const c=Number(o[s]),u=o[s+1]??"";!Number.isFinite(c)||!Number.isInteger(c)||u&&n.set(c,u)}return n}function pr(e){const n=Number(e.id??0),a=String(e.key??""),o=String(e.value??""),s=e.tags==null?"":String(e.tags),c=Number(e.group_id??1);return{id:n,key:a,value:o,tags:s,tagsList:Sn(s),more:Rn(e.more),group_id:Number.isFinite(c)?c:1}}function hr(e){const n=[],a=[],o=(e.search??"").trim();if(o){n.push("(key LIKE ? ESCAPE '\\' OR value LIKE ? ESCAPE '\\' OR tags LIKE ? ESCAPE '\\')");const s=`%${o.replace(/\\/g,"\\\\").replace(/%/g,"\\%").replace(/_/g,"\\_")}%`;a.push(s,s,s)}return e.groupId!=null&&(n.push("group_id = ?"),a.push(e.groupId)),{clause:n.length>0?`WHERE ${n.join(" AND ")}`:"",params:a}}async function In(e={}){const{clause:n,params:a}=hr(e),s=(await pe(`SELECT COUNT(*) as cnt FROM words ${n}`,a))[0]?.cnt;return typeof s=="number"?s:Number(s??0)}async function er(e={}){const{clause:n,params:a}=hr(e),o=Math.max(1,Math.min(e.limit??50,500)),s=Math.max(0,e.offset??0);return(await pe(`SELECT id, key, value, tags, more, group_id FROM words ${n} ORDER BY id LIMIT ? OFFSET ?`,[...a,o,s])).map(pr)}async function Jn(){return(await pe("SELECT id, name FROM word_groups ORDER BY id")).map(n=>({id:Number(n.id),name:String(n.name??"")}))}async function On(){return(await pe("SELECT id, name, forward, backward, filter FROM decks ORDER BY id")).map(n=>({id:Number(n.id),name:String(n.name??""),forward:String(n.forward??""),backward:String(n.backward??""),filter:String(n.filter??"")}))}async function Kn(e,n){return await Pn("UPDATE decks SET forward = ?, backward = ?, filter = ? WHERE id = ?",[n.forward,n.backward,n.filter,e]),Et(e)}async function Et(e){const a=(await pe("SELECT id, name, forward, backward, filter FROM decks WHERE id = ? LIMIT 1",[e]))[0];return a?{id:Number(a.id),name:String(a.name??""),forward:String(a.forward??""),backward:String(a.backward??""),filter:String(a.filter??"")}:null}async function Xn(){const e=await pe("SELECT deck_id, COUNT(*) as cnt FROM states WHERE deck_id IS NOT NULL GROUP BY deck_id"),n=new Map;for(const a of e){if(a.deck_id==null)continue;const o=a.cnt;n.set(Number(a.deck_id),typeof o=="number"?o:Number(o??0))}return n}async function zn(e){const a=(a
<div id="home-db-banner" class="db-banner" role="status" hidden>
<span id="home-db-text" class="db-banner-text">Проверка локальной базы…</span>
<button id="home-db-go" class="db-banner-link" type="button">Синхронизация →</button>
</div>
<section class="card" aria-labelledby="words-title">
<h2 id="words-title" class="card-title">📚 Слова</h2>
<div class="words-toolbar">
<input id="words-search" class="words-search" type="search" placeholder="Поиск: ключ, перевод, тег…"
autocomplete="off" aria-label="Поиск по словам" />
<select id="words-group" class="words-group" aria-label="Фильтр по группе">
<option value="">Все группы</option>
</select>
</div>
<p id="words-count" class="card-hint" role="status" aria-live="polite"></p>
<div id="words-status" class="status-bar" role="status">База ещё не проверена.</div>
<div id="words-list" class="words-list" aria-live="polite"></div>
<div class="btn-row"><button id="words-more" class="btn" type="button" hidden>Показать ещё</button></div>
</section>
`;const a=e.querySelector("#home-db-banner"),o=e.querySelector("#home-db-text"),s=e.querySelector("#home-db-go"),c=e.querySelector("#words-search"),u=e.querySelector("#words-group"),g=e.querySelector("#words-count"),y=e.querySelector("#words-status"),m=e.querySelector("#words-list"),C=e.querySelector("#words-more");function N(w,x){a.hidden=!1,o.textContent=w,s.hidden=!1}function E(){a.hidden=!0}s.addEventListener("click",()=>{document.querySelector('.drawer-btn[data-page="sync"]')?.click()});let $=!1,T=new Map,v=0,M=[],S=null,O=0,Q=!1,L=0,U=0;function b(){const w=u.value;if(w==="")return{search:c.value.trim(),groupId:null};const x=Number(w);return{search:c.value.trim(),groupId:Number.isFinite(x)?x:null}}function z(){C.hidden=!($&&M.length<v),C.hidden||(C.textContent=`Показать ещё (${v-M.length} осталось)`)}function B(){m.innerHTML=M.map(w=>sr(w,T,w.id===S)).join(""),g.textContent=v>0?`Показано ${M.length} из ${v}`:"",z()}function ee(w){if(S===w){w!=null&&D(w),S=null;return}S!=null&&D(S),S=w,w!=null&&re(w)}function D(w){const x=m.querySelector(`.word-row-wrap[data-word-id="${w}"]`);if(!x)return;const q=x.querySelector(".word-row"),P=x.querySelector(".word-extra");q&&(q.classList.remove("open"),q.setAttribute("aria-expanded","false")),P&&(P.hidden=!0,P.innerHTML="")}function re(w){const x=M.find(he=>he.id===w),q=m.querySelector(`.word-row-wrap[data-word-id="${w}"]`);if(!x||!q)return;const P=q.querySelector(".word-row"),R=q.querySelector(".word-extra");P&&(P.classList.add("open"),P.setAttribute("aria-expanded","true")),R&&(R.hidden=!1,R.innerHTML=mr(x,T))}function Z(w){y.textContent=w,y.hidden=w===""}async function j(){if(!$||Q)return;Q=!0;const w=++L,x=b();C.disabled=!0,M.length===0&&Z("Загрузка слов…");try{const[q,P]=await Promise.all([In({search:x.search,groupId:x.groupId??void 0}),er({search:x.search,groupId:x.groupId??void 0,limit:ir,offset:0})]);if(w!==L)return;v=q,M=P,O=P.length,S!=null&&!M.some(R=>R.id===S)&&(S=null),B(),Z(P.length===0?"По этому запросу слов нет.":""),P.length===0&&v===0&&(g.textContent="Слов в базе пока нет.")}catch(q){if(w!==L)return;Z(`Не удалось загрузить слова: ${q instanceof Error?q.message:String(q)}`)}finally{w===L&&(Q=!1,C.disabled=!1,z())}}async function _(){if(!$||Q||M.length>=v)return;Q=!0;const w=++L,x=b();C.disabled=!0,C.textContent="Загрузка…";try{const q=await er({search:x.search,groupId:x.groupId??void 0,limit:ir,offset:O});if(w!==L)return;M=M.concat(q),O+=q.length,m.insertAdjacentHTML("beforeend",q.map(P=>sr(P,T,P.id===S)).join("")),g.textContent=`Показано ${M.length} из ${v}`,z()}catch(q){if(w!==L)return;Z(`Не удалось догрузить: ${q instanceof Error?q.message:String(q)}`)}finally{w===L&&(Q=!1,C.disabled=!1,z())}}c.addEventListener("input",()=>{window.clearTimeout(U),U=window.setTimeout(()=>{j()},300)}),c.addEventListener("keydown",w=>{w.key==="Enter"&&(window.clearTimeout(U),j())}),u.addEventListener("change",()=>{j()}),C.addEventListener("click",()=>{_()}),m.addEventListener("click",w=>{const x=w.target.closest(".word-row");if(!x||x.disabled)return;const q=Number(x.dataset.wordId);Number.isFinite(q)&&ee(S===q?null:q)}),(async()=>{if(!await tt()){N("Локальной базы нет — скачай сохранение."),Z("Нет локальной базы — скачай сохранение на странице «Синхронизация»."),g.textContent="";return}try{const x=await Vn();if(!Ct(x)){N("Файл в OPFS не похож на SQLite — перекачай сохранение.",!0),Z("Файл в OPFS не похож на SQLite — перекачай сохранение.");return}E();try{const[P,R]=await Promise.all([Jn(),gr()]),he=P;T=new Map(R.map(le=>[le.id,le])),u.innerHTML='<option value="">Все группы</option>'+he.map(le=>`<option value="${le.id}">${Ce(le.name)}</option>`).join(""),$=!0,await j(),u.options[0].textContent=`Все группы (${v})`}catch(P){Z(`БД есть, но слова не п
<section class="card" aria-labelledby="decks-title">
<h2 id="decks-title" class="card-title">🗂️ Колоды</h2>
<p id="decks-count" class="card-hint" role="status" aria-live="polite"></p>
<div id="decks-status" class="status-bar" role="status">Проверка локальной базы…</div>
<div id="decks-list" class="words-list" aria-live="polite"></div>
</section>
`;const a=e.querySelector("#decks-count"),o=e.querySelector("#decks-status"),s=e.querySelector("#decks-list");function c(u){o.textContent=u,o.hidden=u===""}s.addEventListener("click",u=>{const g=u.target.closest("[data-act]");if(!g)return;const y=Number(g.dataset.deckId);Number.isFinite(y)&&(g.dataset.act==="review"?n.goReview(y):n.goDeck(y))}),(async()=>{if(!await tt()){c("Нет локальной базы — скачай сохранение на странице «Синхронизация»."),a.textContent="";return}c("Загрузка колод…");try{const[g,y]=await Promise.all([On(),Xn()]);if(g.length===0){a.textContent="Колоды пока отсутствуют.",c(""),s.innerHTML="";return}a.textContent=`Всего: ${g.length}`,c(""),s.innerHTML=g.map(m=>$a(m,y.get(m.id)??0)).join("")}catch(g){c(`Не удалось загрузить колоды: ${g instanceof Error?g.message:String(g)}`)}})()}function Ma(e){return e?Ta(e):'<span class="empty-val">—</span>'}function Ta(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}function mt(e){return e.replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}function Na(e){const n=e%10,a=e%100;return n===1&&a!==11?"слово":n>=2&&n<=4&&(a<12||a>14)?"слова":"слов"}function qa(e,n,a){e.innerHTML=`
<button id="deck-back" class="btn btn-ghost" type="button">← Назад к колодам</button>
<section class="card" aria-labelledby="deck-title">
<h2 id="deck-title" class="card-title">Колода…</h2>
<div id="deck-status" class="status-bar" role="status">Загрузка колоды…</div>
<div id="deck-body"></div>
</section>
`;const o=e.querySelector("#deck-back"),s=e.querySelector("#deck-title"),c=e.querySelector("#deck-status"),u=e.querySelector("#deck-body");o.addEventListener("click",()=>a.goDecks()),(async()=>{if(!await tt()){s.textContent="Колода недоступна",c.textContent="Нет локальной базы — скачай сохранение на странице «Синхронизация».";return}let y=null;try{y=await Et(n)}catch(U){s.textContent="Колода недоступна",c.textContent=`Не удалось загрузить колоду: ${U instanceof Error?U.message:String(U)}`;return}if(!y){s.textContent="Колода не найдена",c.textContent=`Колоды с id=${n} нет в локальной базе. Возможно, база обновилась — вернись к списку.`;return}const m=y;s.textContent=m.name||`Колода #${m.id}`,c.hidden=!0;let C=null;try{C=await zn(m.id)}catch(U){c.hidden=!1,c.textContent=`Данные колоды загружены, а количество слов — нет: ${U instanceof Error?U.message:String(U)}`}u.innerHTML=`
<dl class="word-extra-list">
<div class="word-extra-row"><dt>id</dt><dd>${m.id}</dd></div>
<div class="word-extra-row"><dt>name</dt><dd>${Ma(m.name)}</dd></div>
</dl>
<form id="deck-form" class="deck-form" autocomplete="off">
<label class="field-label" for="deck-forward">forward</label>
<input id="deck-forward" class="token-input" type="text" value="${mt(m.forward)}" />
<label class="field-label" for="deck-backward">backward</label>
<input id="deck-backward" class="token-input" type="text" value="${mt(m.backward)}" />
<label class="field-label" for="deck-filter">filter</label>
<input id="deck-filter" class="token-input" type="text" value="${mt(m.filter)}" />
<div class="btn-row">
<button id="deck-save" class="btn btn-primary" type="submit">💾 Сохранить</button>
</div>
<p id="deck-form-status" class="card-hint" role="status" aria-live="polite"></p>
</form>
<h3 class="deck-sub">📚 ${C==null?"Количество слов неизвестно":`${C} ${Na(C)}`}</h3>
<div class="btn-row">
<button id="deck-review" class="btn btn-primary" type="button"${C===0?" disabled":""}>▶ Повторять</button>
</div>
`;const N=u.querySelector("#deck-form"),E=u.querySelector("#deck-forward"),$=u.querySelector("#deck-backward"),T=u.querySelector("#deck-filter"),v=u.querySelector("#deck-save"),M=u.querySelector("#deck-form-status");u.querySelector("#deck-review").addEventListener("click",()=>a.goReview(m.id));const O=()=>`${E.value}
${$.value}
${T.value}`;let Q=O();function L(){v.disabled=O()===Q||v.dataset.busy==="1"}for(const U of[E,$,T])U.addEventListener("input",L);L(),N.addEventListener("submit",U=>{U.preventDefault(),!v.disabled&&(v.dataset.busy="1",v.disabled=!0,v.textContent="Сохранение…",M.textContent="",(async()=>{try{const b=await Kn(m.id,{forward:E.value,backward:$.value,filter:T.value});if(!b){M.textContent="Колода пропала из базы — вернись к списку.";return}E.value=b.forward,$.value=b.backward,T.value=b.filter,Q=O(),M.textContent="✅ Сохранено в локальную базу."}catch(b){M.textContent=`Не удалось сохранить: ${b instanceof Error?b.message:String(b)}`}finally{delete v.dataset.busy,v.textContent="💾 Сохранить",L()}})())})})()}function Ha(e){return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#39;")}const Fa=[{label:"😞 Не получилось",cls:"grade-fail"},{label:"😟 Тяжело",cls:"grade-hard"},{label:"🙂 Хорошо",cls:"grade-good"},{label:"🤩 Легко",cls:"grade-easy"}];function La(e,n,a){e.innerHTML=`
<button id="review-back" class="btn btn-ghost" type="button">← Назад к колодам</button>
<section class="card" aria-labelledby="review-title">
<h2 id="review-title" class="card-title">Повторение…</h2>
<p id="review-progress" class="card-hint" role="status" aria-live="polite"></p>
<div id="review-status" class="status-bar" role="status">Загрузка карточек…</div>
<div id="review-body"></div>
</section>
`;const o=e.querySelector("#review-back"),s=e.querySelector("#review-title"),c=e.querySelector("#review-progress"),u=e.querySelector("#review-status"),g=e.querySelector("#review-body");o.addEventListener("click",()=>a.goDecks());function y(L){u.textContent=L,u.hidden=L===""}let m=[],C=new Map,N=[],E="",$="",T=0,v=!1;function M(L,U){return L.length===0?`<div class="review-side ${U}"><span class="empty-val">—</span></div>`:`<div class="review-side ${U}">`+L.map(b=>`<p class="review-line">${Ha(b)}</p>`).join("")+"</div>"}function S(){const L=C.get(m[T]);if(!L){g.innerHTML="",c.textContent="",y("Слово пропало из базы — пропускаю.");return}const U=Dn(L,N),b=rr(E,U),z=v?rr($,U):null;c.textContent=`Карточка ${T+1} из ${m.length}`,y("");const B=v?M(z??[],"review-back-side"):'<p class="review-tap">Тап — показать ответ</p>',ee=v?`<div class="review-grades" role="group" aria-label="Оценка повторения">
${Fa.map(D=>`<button class="btn review-grade ${D.cls}" type="button">${D.label}</button>`).join("")}
</div>`:"";g.innerHTML=`
<button id="review-card" class="review-card" type="button"
aria-expanded="${v}" aria-label="${v?"Карточка открыта":"Показать ответ"}">
${M(b,"review-front")}
${v?'<hr class="review-hr" />':""}
${B}
</button>
${ee}
`,g.querySelector("#review-card").addEventListener("click",()=>{v||(v=!0,S())}),g.querySelectorAll(".review-grade").forEach(D=>{D.addEventListener("click",()=>{T+=1,v=!1,T>=m.length?O():S()})})}function O(){c.textContent=`Готово: ${m.length} из ${m.length}`,y(""),g.innerHTML=`
<p class="card-hint">Все карточки сессии просмотрены. Запись оценок — следующим шагом.</p>
<div class="btn-row">
<button id="review-again" class="btn btn-primary" type="button">🔀 Перемешать заново</button>
</div>
`,g.querySelector("#review-again").addEventListener("click",()=>{Q()})}async function Q(){if(y("Перемешиваю…"),g.innerHTML="",c.textContent="",m=await nr(n),T=0,v=!1,m.length===0){y("В колоде нет слов для повторения.");return}C=await tr(m),S()}(async()=>{if(!await tt()){s.textContent="Повторение недоступно",y("Нет локальной базы — скачай сохранение на странице «Синхронизация».");return}try{const[U,b]=await Promise.all([Et(n),gr()]);if(!U){s.textContent="Колода не найдена",y(`Колоды с id=${n} нет в локальной базе. Вернись к списку.`);return}if(N=b,E=U.forward,$=U.backward,s.textContent=U.name||`Колода #${U.id}`,m=await nr(U.id),m.length===0){c.textContent="",y("В колоде нет слов для повторения.");return}C=await tr(m),T=0,v=!1,S()}catch(U){s.textContent="Повторение недоступно",y(`Не удалось загрузить карточки: ${U instanceof Error?U.message:String(U)}`)}})()}const cr=24,Qa=/^[A-Za-z0-9]{24}$/,yr="japlearn_sync_token";function lr(e){return e.trim().replace(/\s+/g,"")}function Qe(e){return Qa.test(e)}function _a(){try{return localStorage.getItem(yr)??""}catch{return""}}function ur(e){try{localStorage.setItem(yr,e)}catch{}}const br="https://learning.micialware.ru";function Pa(e,n=br){return`${n.replace(/\/+$/,"")}/download/${encodeURIComponent(e)}`}async function Sa(e,n,a=br){const o=Pa(e,a);let s;try{s=await fetch(o,{method:"GET",...n})}catch(u){throw new Error(`Нет связи с сервером: ${u instanceof Error?u.message:String(u)}`)}if(!s.ok)throw s.status===404?new Error("Сохранение не найдено (404). Проверь токен."):new Error(`Сервер ответил ${s.status} ${s.statusText}`);const c=await s.arrayBuffer();if(c.byteLength===0)throw new Error("Сервер вернул пустой файл");return new Uint8Array(c)}function Ra(e){e.innerHTML=`
<main class="card" aria-labelledby="sync-title">
<h2 id="sync-title" class="card-title">📡 Синхронизация</h2>
<p class="card-hint">Токен сохранения — 24 символа: латинские буквы и цифры.</p>
<label class="field-label" for="token-input">Токен сохранения</label>
<input
id="token-input"
class="token-input"
type="text"
inputmode="latin"
autocomplete="off"
autocapitalize="off"
autocorrect="off"
spellcheck="false"
maxlength="24"
minlength="24"
placeholder="Например: aB3dEf7hJk9mN2pQr5tUvWx1z"
aria-describedby="token-error token-counter"
/>
<div class="token-meta">
<span id="token-error" class="token-error" role="alert"></span>
<span id="token-counter" class="token-counter" aria-live="polite">0 / 24</span>
</div>
<div class="btn-row">
<button id="btn-download" class="btn btn-primary" type="button" disabled>📥 Скачать</button>
</div>
<p id="status" class="status-bar" role="status">Введите токен из 24 символов</p>
</main>
`;const n=e.querySelector("#token-input"),a=e.querySelector("#token-error"),o=e.querySelector("#token-counter"),s=e.querySelector("#btn-download"),c=e.querySelector("#status");function u(E){c.textContent=E,console.log(`[JapLearn] ${E}`)}function g(){return lr(n.value)}function y(E){s.disabled=E||!Qe(g()),n.disabled=E}function m(){const E=g(),$=E.length;o.textContent=`${$} / ${cr}`;const T=Qe(E),v=$===0;n.classList.toggle("invalid",!v&&!T),n.setAttribute("aria-invalid",String(!v&&!T)),v?a.textContent="":$!==cr?a.textContent=`Нужно 24 символа, сейчас ${$}`:a.textContent=T?"":"Только латинские буквы и цифры",s.disabled=!T}async function C(){const E=g();if(Qe(E)){ur(E),y(!0),u("Скачивание сохранения...");try{const $=await Sa(E);u(`Скачано ${($.length/1024).toFixed(1)} КБ, распаковка...`);const T=await Ca($);u(`Распаковано ${(T.length/1024).toFixed(1)} КБ, сохранение в OPFS...`),await Bn(T),u(`Готово: база ${(T.length/1024).toFixed(1)} КБ сохранена локально (OPFS).`)}catch($){u(`Ошибка скачивания: ${$ instanceof Error?$.message:String($)}`)}finally{y(!1),m()}}}n.addEventListener("input",()=>{m()}),n.addEventListener("change",()=>{const E=g();n.value=E,Qe(E)&&(ur(E),u("Токен сохранён локально. Можно скачивать.")),m()}),s.addEventListener("click",()=>{C()});const N=_a();N&&(n.value=N),m(),Qe(lr(N))&&u("Токен восстановлен. Нажми «Скачать», чтобы получить базу.")}const Ia={home:"Главная",decks:"Колоды",deck:"Колода",review:"Повторение",sync:"Синхронизация"};function Je(e){const n=document.getElementById(e);if(!n)throw new Error(`[JapLearn] Не найден элемент #${e}`);return n}const kr=Je("burger-btn"),et=Je("drawer"),Cr=Je("drawer-overlay"),Ja=Je("page-title"),_e=Je("page");let xt=!1;function Oe(e){xt=e,et.classList.toggle("open",e),Cr.hidden=!e,kr.setAttribute("aria-expanded",String(e)),et.setAttribute("aria-hidden",String(!e)),document.body.classList.toggle("drawer-open",e)}function Oa(e){et.querySelectorAll(".drawer-btn").forEach(n=>{n.classList.toggle("active",n.dataset.page===e)})}function ke(e,n){Ja.textContent=Ia[e],Oa(e==="deck"||e==="review"?"decks":e),e==="home"?Ea(_e):e==="decks"?xa(_e,{goDeck:a=>ke("deck",a),goReview:a=>ke("review",a)}):e==="deck"?qa(_e,n??0,{goDecks:()=>ke("decks"),goReview:a=>ke("review",a)}):e==="review"?La(_e,n??0,{goDecks:()=>ke("decks")}):Ra(_e)}kr.addEventListener("click",()=>{Oe(!xt)});Cr.addEventListener("click",()=>{Oe(!1)});document.addEventListener("keydown",e=>{e.key==="Escape"&&xt&&Oe(!1)});et.querySelectorAll(".drawer-btn").forEach(e=>{e.addEventListener("click",()=>{const n=e.dataset.page;(n==="home"||n==="decks"||n==="sync")&&ke(n),Oe(!1)})});await _n();Oe(!1);ke("home");console.log("[JapLearn] crossOriginIsolated =",window.crossOriginIsolated);