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

60 lines
257 KiB
JavaScript
Raw Normal View History

2026-09-15 10:41:24 +03:00
(function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))c(o);new MutationObserver(o=>{for(const l of o)if(l.type==="childList")for(const h of l.addedNodes)h.tagName==="LINK"&&h.rel==="modulepreload"&&c(h)}).observe(document,{childList:!0,subtree:!0});function i(o){const l={};return o.integrity&&(l.integrity=o.integrity),o.referrerPolicy&&(l.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?l.credentials="include":o.crossOrigin==="anonymous"?l.credentials="omit":l.credentials="same-origin",l}function c(o){if(o.ep)return;o.ep=!0;const l=i(o);fetch(o.href,l)}})();const _e=new Worker(new URL("/assets/db-worker-AHwpujSZ.js",import.meta.url),{type:"module"});let ir=0,Be=null;const De=new Map;function kn(){return Be||(Be=new Promise((r,n)=>{_e.onmessage=c=>{const o=c.data;if(o.type==="ready")return;const l=De.get(o.id);l&&(De.delete(o.id),o.type=="success"?l.resolve(o.data):l.reject(new Error(o.error??o.message??"Неизвестная ошибка Worker")))},_e.onerror=c=>{console.error("[SQLite Worker] Ошибка:",c),n(new Error("Ошибка Worker"))};const i=++ir;De.set(i,{resolve:r,reject:n}),_e.postMessage({type:"start",id:i})}),Be)}function $n(r,n){return new Promise((i,c)=>{if(!_e){c(new Error("Worker не создан"));return}const o=++ir;De.set(o,{resolve:i,reject:c}),_e.postMessage({type:r,id:o,...n})})}async function Ae(r,n){return await $n("query",{sql:r,params:n??[]})}function xn(r){if(r==null)return[];const n=String(r).trim();return n?n.split(/[,\s;]+/).map(i=>i.trim()).filter(i=>i.length>0):[]}function Qn(r){const n=new Map;if(r==null)return n;const i=String(r);if(!i)return n;const c=i.split("");for(let o=0;o+1<c.length;o+=2){const l=Number(c[o]),h=c[o+1]??"";!Number.isFinite(l)||!Number.isInteger(l)||h&&n.set(l,h)}return n}function Fn(r){const n=Number(r.id??0),i=String(r.key??""),c=String(r.value??""),o=r.tags==null?"":String(r.tags),l=Number(r.group_id??1);return{id:n,key:i,value:c,tags:o,tagsList:xn(o),more:Qn(r.more),group_id:Number.isFinite(l)?l:1}}function or(r){const n=[],i=[],c=(r.search??"").trim();if(c){n.push("(key LIKE ? ESCAPE '\\' OR value LIKE ? ESCAPE '\\' OR tags LIKE ? ESCAPE '\\')");const o=`%${c.replace(/\\/g,"\\\\").replace(/%/g,"\\%").replace(/_/g,"\\_")}%`;i.push(o,o,o)}return r.groupId!=null&&(n.push("group_id = ?"),i.push(r.groupId)),{clause:n.length>0?`WHERE ${n.join(" AND ")}`:"",params:i}}async function Nn(r={}){const{clause:n,params:i}=or(r),o=(await Ae(`SELECT COUNT(*) as cnt FROM words ${n}`,i))[0]?.cnt;return typeof o=="number"?o:Number(o??0)}async function Wt(r={}){const{clause:n,params:i}=or(r),c=Math.max(1,Math.min(r.limit??50,500)),o=Math.max(0,r.offset??0);return(await Ae(`SELECT id, key, value, tags, more, group_id FROM words ${n} ORDER BY id LIMIT ? OFFSET ?`,[...i,c,o])).map(Fn)}async function Tn(){return(await Ae("SELECT id, name FROM word_groups ORDER BY id")).map(n=>({id:Number(n.id),name:String(n.name??"")}))}async function Pn(){return(await Ae("SELECT id, name, identifier FROM custom_fields ORDER BY id")).map(n=>({id:Number(n.id),name:String(n.name??""),identifier:String(n.identifier??"")}))}const Ut="japanese_app.db";async function Ct(){return navigator.storage.getDirectory()}async function _n(r){r[18]=1,r[19]=1;const i=await(await Ct()).getFileHandle(Ut,{create:!0}),c=i.createSyncAccessHandle;if(typeof c=="function")try{const l=await c.call(i);try{l.truncate(0),l.write(r,{at:0}),l.flush()}finally{l.close()}return}catch{}const o=await i.createWritable();try{await o.truncate(0);const l=new Uint8Array(r.length);l.set(r),await o.write(l)}finally{await o.close()}}async function Hn(){const r=await Ct();let n;try{n=await r.getFileHandle(Ut)}catch{throw new Error("Локальная база не найдена — сначала скачай сохранение")}const i=await n.getFile();if(i.size===0)throw new Error("Локальная база пуста");return new Uint8Array(await i.arrayBuffer())}async function Ge(
<div class="word-card-top"><div class="word-key">${ye(r.key)}</div>`+(n?`<span class="word-group">${ye(n)}</span>`:"")+`</div><div class="word-value">${ye(r.value)}</div>`+(c?`<div class="tag-row">${c}</div>`:"")+l+"</article>"}function ua(r,n){r.innerHTML=`
<main class="card" aria-labelledby="home-title">
<h2 id="home-title" class="card-title">🏠 Главная</h2>
<p class="card-hint">Локальное сохранение и повторения — всё работает офлайн.</p>
<div id="home-db-status" class="status-bar" role="status">Проверка локальной базы…</div>
<div class="btn-row">
<button id="btn-go-sync" class="btn btn-primary" type="button">📡 К синхронизации</button>
</div>
<div id="home-actions" class="btn-row" hidden>
<button id="btn-start" class="btn" type="button">▶ Начать повторение</button>
</div>
</main>
<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 i=r.querySelector("#home-db-status"),c=r.querySelector("#btn-go-sync"),o=r.querySelector("#home-actions"),l=r.querySelector("#btn-start"),h=r.querySelector("#words-search"),g=r.querySelector("#words-group"),v=r.querySelector("#words-count"),k=r.querySelector("#words-status"),C=r.querySelector("#words-list"),M=r.querySelector("#words-more");c.addEventListener("click",()=>n.goSync()),l.addEventListener("click",()=>{i.textContent="Экран повторений — следующий шаг."});let F=!1,y=new Map,N=new Map,w=0,$=[],D=0,J=!1,H=0,te=0;function K(){const T=g.value;if(T==="")return{search:h.value.trim(),groupId:null};const _=Number(T);return{search:h.value.trim(),groupId:Number.isFinite(_)?_:null}}function b(){M.hidden=!(F&&$.length<w),M.hidden||(M.textContent=`Показать ещё (${w-$.length} осталось)`)}function B(){C.innerHTML=$.map(T=>tr(T,y.get(T.group_id),N)).join(""),v.textContent=w>0?`Показано ${$.length} из ${w}`:"",b()}function S(T){k.textContent=T,k.hidden=T===""}async function G(){if(!F||J)return;J=!0;const T=++H,_=K();M.disabled=!0,$.length===0&&S("Загрузка слов…");try{const[P,U]=await Promise.all([Nn({search:_.search,groupId:_.groupId??void 0}),Wt({search:_.search,groupId:_.groupId??void 0,limit:er,offset:0})]);if(T!==H)return;w=P,$=U,D=U.length,B(),S(U.length===0?"По этому запросу слов нет.":""),U.length===0&&w===0&&(v.textContent="Слов в базе пока нет.")}catch(P){if(T!==H)return;S(`Не удалось загрузить слова: ${P instanceof Error?P.message:String(P)}`)}finally{T===H&&(J=!1,M.disabled=!1,b())}}async function j(){if(!F||J||$.length>=w)return;J=!0;const T=++H,_=K();M.disabled=!0,M.textContent="Загрузка…";try{const P=await Wt({search:_.search,groupId:_.groupId??void 0,limit:er,offset:D});if(T!==H)return;$=$.concat(P),D+=P.length,C.insertAdjacentHTML("beforeend",P.map(U=>tr(U,y.get(U.group_id),N)).join("")),v.textContent=`Показано ${$.length} из ${w}`,b()}catch(P){if(T!==H)return;S(`Не удалось догрузить: ${P instanceof Error?P.message:String(P)}`)}finally{T===H&&(J=!1,M.disabled=!1,b())}}h.addEventListener("input",()=>{window.clearTimeout(te),te=window.setTimeout(()=>{G()},300)}),h.addEventListener("keydown",T=>{T.key==="Enter"&&(window.clearTimeout(te),G())}),g.addEventListener("change",()=>{G()}),M.addEventListener("click",()=>{j()}),(async()=>{if(!await Ge()){i.textContent="Локальной базы нет. Скачай сохранение на странице «Синхронизация».",o.hidden=!0,S("Нет локальной базы — скачай сохранение на странице «Синхронизация»."),v.textContent="";return}try{const _=await Hn(),P=bt(_);if(i.textContent=P?`Локальная база на месте: ${(_.length/1024).toFixed(1)} КБ, сигнатура SQLite OK.`:"Файл в OPFS есть, но это не похоже на SQLite — перекачай сохранение.",o.hidden=!P,!P){S("Файл в OPFS не похож на SQLite — перекачай сохранение.");return}try{const[U,E]=await Promise.all([Tn(),Pn()]),V=U;y=new Map(V.map(O=>[O.id,O.name])),N=new Map(E.map(O=>[O.id,O])),g.innerHTML='<option value="">Все группы</option>'+V.map(O=>`<option value="${O.id}">${ye(O.name)}</option>`).join(""),F=!0,await G(),g.options[0].textContent=`Все группы (${w})`}catch(U){S(`БД есть, но слова не прочитались: ${U instanceof Error?U.message:String(U)}`)}}catch(_){i.textContent=`Не удалось прочитать базу: ${_ instanceof Error?_.message:String(_)}`,o.hidden=!0}})()}const rr=24,da=/^[A-Za-z0-9]{24}$/,lr="japlearn_sync_token";function nr(r){return r.trim().replace(/\s+/g,"")}function ve(r){return da.test(r)}function pa(){try{return localStorage.getItem(lr)??""}catch{return""}}function wt(r){try{localStorage.setItem(lr,r)}catch{}}const ur="https://learning.micialware.ru";function fa(r,n=ur){return`${n.replace(/\/+$/,""
<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" type="button" disabled>📥 Скачать</button>
<button id="btn-upload" class="btn btn-primary" type="button" disabled>📤 Выгрузить</button>
</div>
<p id="status" class="status-bar" role="status">Введите токен из 24 символов</p>
</main>
`;const n=r.querySelector("#token-input"),i=r.querySelector("#token-error"),c=r.querySelector("#token-counter"),o=r.querySelector("#btn-download"),l=r.querySelector("#btn-upload"),h=r.querySelector("#status");function g(y){h.textContent=y,console.log(`[JapLearn] ${y}`)}function v(){return nr(n.value)}function k(y){o.disabled=y||!ve(v()),l.disabled=!0,n.disabled=y,y||Ge().then(N=>{!n.disabled&&ve(v())&&(l.disabled=!N)})}function C(){const y=v(),N=y.length;c.textContent=`${N} / ${rr}`;const w=ve(y),$=N===0;n.classList.toggle("invalid",!$&&!w),n.setAttribute("aria-invalid",String(!$&&!w)),$?i.textContent="":N!==rr?i.textContent=`Нужно 24 символа, сейчас ${N}`:i.textContent=w?"":"Только латинские буквы и цифры",o.disabled=!w,w?Ge().then(D=>{n.disabled||(l.disabled=!D)}):l.disabled=!0}async function M(){const y=v();if(ve(y)){wt(y),k(!0),g("Скачивание сохранения...");try{const N=await ha(y);g(`Скачано ${(N.length/1024).toFixed(1)} КБ, распаковка...`);const w=await la(N);g(`Распаковано ${(w.length/1024).toFixed(1)} КБ, сохранение в OPFS...`),await _n(w),g(`Готово: база ${(w.length/1024).toFixed(1)} КБ сохранена локально (OPFS).`)}catch(N){g(`Ошибка скачивания: ${N instanceof Error?N.message:String(N)}`)}finally{k(!1),C()}}}n.addEventListener("input",()=>{C()}),n.addEventListener("change",()=>{const y=v();n.value=y,ve(y)&&(wt(y),g("Токен сохранён локально. Можно скачивать или выгружать.")),C()}),o.addEventListener("click",()=>{M()}),l.addEventListener("click",()=>{const y=v();ve(y)&&(wt(y),g("Выгрузка — следующий шаг (POST /upload/stream/{id})."))});const F=pa();F&&(n.value=F),C(),ve(nr(F))&&Ge().then(y=>{g(y?"Токен восстановлен, локальная база на месте.":"Токен восстановлен. Нажми «Скачать», чтобы получить базу."),C()})}const wa={home:"Главная",sync:"Синхронизация"};function Le(r){const n=document.getElementById(r);if(!n)throw new Error(`[JapLearn] Не найден элемент #${r}`);return n}const dr=Le("burger-btn"),We=Le("drawer"),pr=Le("drawer-overlay"),ma=Le("page-title"),ar=Le("page");let Et=!1;function Re(r){Et=r,We.classList.toggle("open",r),pr.hidden=!r,dr.setAttribute("aria-expanded",String(r)),We.setAttribute("aria-hidden",String(!r)),document.body.classList.toggle("drawer-open",r)}function va(r){We.querySelectorAll(".drawer-btn").forEach(n=>{n.classList.toggle("active",n.dataset.page===r)})}function Mt(r){ma.textContent=wa[r],va(r),r==="home"?ua(ar,{goSync:()=>Mt("sync")}):ga(ar)}dr.addEventListener("click",()=>{Re(!Et)});pr.addEventListener("click",()=>{Re(!1)});document.addEventListener("keydown",r=>{r.key==="Escape"&&Et&&Re(!1)});We.querySelectorAll(".drawer-btn").forEach(r=>{r.addEventListener("click",()=>{const n=r.dataset.page;(n==="home"||n==="sync")&&Mt(n),Re(!1)})});await kn();Re(!1);Mt("home");console.log("[JapLearn] crossOriginIsolated =",window.crossOriginIsolated);