52 lines
258 KiB
JavaScript
52 lines
258 KiB
JavaScript
(function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))s(i);new MutationObserver(i=>{for(const u of i)if(u.type==="childList")for(const h of u.addedNodes)h.tagName==="LINK"&&h.rel==="modulepreload"&&s(h)}).observe(document,{childList:!0,subtree:!0});function o(i){const u={};return i.integrity&&(u.integrity=i.integrity),i.referrerPolicy&&(u.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?u.credentials="include":i.crossOrigin==="anonymous"?u.credentials="omit":u.credentials="same-origin",u}function s(i){if(i.ep)return;i.ep=!0;const u=o(i);fetch(i.href,u)}})();const _e=new Worker(new URL("/assets/db-worker-AHwpujSZ.js",import.meta.url),{type:"module"});let ar=0,Ye=null;const De=new Map;function kn(){return Ye||(Ye=new Promise((r,n)=>{_e.onmessage=s=>{const i=s.data;if(i.type==="ready")return;const u=De.get(i.id);u&&(De.delete(i.id),i.type=="success"?u.resolve(i.data):u.reject(new Error(i.error??i.message??"Неизвестная ошибка Worker")))},_e.onerror=s=>{console.error("[SQLite Worker] Ошибка:",s),n(new Error("Ошибка Worker"))};const o=++ar;De.set(o,{resolve:r,reject:n}),_e.postMessage({type:"start",id:o})}),Ye)}function $n(r,n){return new Promise((o,s)=>{if(!_e){s(new Error("Worker не создан"));return}const i=++ar;De.set(i,{resolve:o,reject:s}),_e.postMessage({type:r,id:i,...n})})}async function Ae(r,n){return await $n("query",{sql:r,params:n??[]})}function Qn(r){if(r==null)return[];const n=String(r).trim();return n?n.split(/[,\s;]+/).map(o=>o.trim()).filter(o=>o.length>0):[]}function Tn(r){const n=new Map;if(r==null)return n;const o=String(r);if(!o)return n;const s=o.split("");for(let i=0;i+1<s.length;i+=2){const u=Number(s[i]),h=s[i+1]??"";!Number.isFinite(u)||!Number.isInteger(u)||h&&n.set(u,h)}return n}function Fn(r){const n=Number(r.id??0),o=String(r.key??""),s=String(r.value??""),i=r.tags==null?"":String(r.tags),u=Number(r.group_id??1);return{id:n,key:o,value:s,tags:i,tagsList:Qn(i),more:Tn(r.more),group_id:Number.isFinite(u)?u:1}}function or(r){const n=[],o=[],s=(r.search??"").trim();if(s){n.push("(key LIKE ? ESCAPE '\\' OR value LIKE ? ESCAPE '\\' OR tags LIKE ? ESCAPE '\\')");const i=`%${s.replace(/\\/g,"\\\\").replace(/%/g,"\\%").replace(/_/g,"\\_")}%`;o.push(i,i,i)}return r.groupId!=null&&(n.push("group_id = ?"),o.push(r.groupId)),{clause:n.length>0?`WHERE ${n.join(" AND ")}`:"",params:o}}async function Nn(r={}){const{clause:n,params:o}=or(r),i=(await Ae(`SELECT COUNT(*) as cnt FROM words ${n}`,o))[0]?.cnt;return typeof i=="number"?i:Number(i??0)}async function jt(r={}){const{clause:n,params:o}=or(r),s=Math.max(1,Math.min(r.limit??50,500)),i=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 ?`,[...o,s,i])).map(Fn)}async function Hn(){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 o=await(await Ct()).getFileHandle(Ut,{create:!0}),s=o.createSyncAccessHandle;if(typeof s=="function")try{const u=await s.call(o);try{u.truncate(0),u.write(r,{at:0}),u.flush()}finally{u.close()}return}catch{}const i=await o.createWritable();try{await i.truncate(0);const u=new Uint8Array(r.length);u.set(r),await i.write(u)}finally{await i.close()}}async function qn(){const r=await Ct();let n;try{n=await r.getFileHandle(Ut)}catch{throw new Error("Локальная база не найдена — сначала скачай сохранение")}const o=await n.getFile();if(o.size===0)throw new Error("Локальная база пуста");return new Uint8Array(await o.arrayBuffer())}async function Ge(
|
|||
|
|
<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 o=r.querySelector("#home-db-banner"),s=r.querySelector("#home-db-text"),i=r.querySelector("#home-db-go"),u=r.querySelector("#words-search"),h=r.querySelector("#words-group"),w=r.querySelector("#words-count"),U=r.querySelector("#words-status"),M=r.querySelector("#words-list"),b=r.querySelector("#words-more");function F(g,C){o.hidden=!1,s.textContent=g,i.hidden=!1}function N(){o.hidden=!0}i.addEventListener("click",()=>{document.querySelector('.drawer-btn[data-page="sync"]')?.click()});let y=!1,Q=new Map,m=0,x=[],I=null,X=0,R=!1,Y=0,K=0;function E(){const g=h.value;if(g==="")return{search:u.value.trim(),groupId:null};const C=Number(g);return{search:u.value.trim(),groupId:Number.isFinite(C)?C:null}}function O(){b.hidden=!(y&&x.length<m),b.hidden||(b.textContent=`Показать ещё (${m-x.length} осталось)`)}function D(){M.innerHTML=x.map(g=>er(g,Q,g.id===I)).join(""),w.textContent=m>0?`Показано ${x.length} из ${m}`:"",O()}function ee(g){if(I===g){g!=null&&G(g),I=null;return}I!=null&&G(I),I=g,g!=null&&re(g)}function G(g){const C=M.querySelector(`.word-row-wrap[data-word-id="${g}"]`);if(!C)return;const k=C.querySelector(".word-row"),P=C.querySelector(".word-extra");k&&(k.classList.remove("open"),k.setAttribute("aria-expanded","false")),P&&(P.hidden=!0,P.innerHTML="")}function re(g){const C=x.find(fe=>fe.id===g),k=M.querySelector(`.word-row-wrap[data-word-id="${g}"]`);if(!C||!k)return;const P=k.querySelector(".word-row"),_=k.querySelector(".word-extra");P&&(P.classList.add("open"),P.setAttribute("aria-expanded","true")),_&&(_.hidden=!1,_.innerHTML=lr(C,Q))}function V(g){U.textContent=g,U.hidden=g===""}async function j(){if(!y||R)return;R=!0;const g=++Y,C=E();b.disabled=!0,x.length===0&&V("Загрузка слов…");try{const[k,P]=await Promise.all([Nn({search:C.search,groupId:C.groupId??void 0}),jt({search:C.search,groupId:C.groupId??void 0,limit:At,offset:0})]);if(g!==Y)return;m=k,x=P,X=P.length,I!=null&&!x.some(_=>_.id===I)&&(I=null),D(),V(P.length===0?"По этому запросу слов нет.":""),P.length===0&&m===0&&(w.textContent="Слов в базе пока нет.")}catch(k){if(g!==Y)return;V(`Не удалось загрузить слова: ${k instanceof Error?k.message:String(k)}`)}finally{g===Y&&(R=!1,b.disabled=!1,O())}}async function H(){if(!y||R||x.length>=m)return;R=!0;const g=++Y,C=E();b.disabled=!0,b.textContent="Загрузка…";try{const k=await jt({search:C.search,groupId:C.groupId??void 0,limit:At,offset:X});if(g!==Y)return;x=x.concat(k),X+=k.length,M.insertAdjacentHTML("beforeend",k.map(P=>er(P,Q,P.id===I)).join("")),w.textContent=`Показано ${x.length} из ${m}`,O()}catch(k){if(g!==Y)return;V(`Не удалось догрузить: ${k instanceof Error?k.message:String(k)}`)}finally{g===Y&&(R=!1,b.disabled=!1,O())}}u.addEventListener("input",()=>{window.clearTimeout(K),K=window.setTimeout(()=>{j()},300)}),u.addEventListener("keydown",g=>{g.key==="Enter"&&(window.clearTimeout(K),j())}),h.addEventListener("change",()=>{j()}),b.addEventListener("click",()=>{H()}),M.addEventListener("click",g=>{const C=g.target.closest(".word-row");if(!C||C.disabled)return;const k=Number(C.dataset.wordId);Number.isFinite(k)&&ee(I===k?null:k)}),(async()=>{if(!await Ge()){F("Локальной базы нет — скачай сохранение."),V("Нет локальной базы — скачай сохранение на странице «Синхронизация»."),w.textContent="";return}try{const C=await qn();if(!bt(C)){F("Файл в OPFS не похож на SQLite — перекачай сохранение.",!0),V("Файл в OPFS не похож на SQLite — перекачай сохранение.");return}N();try{const[P,_]=await Promise.all([Hn(),Pn()]),fe=P;Q=new Map(_.map(ue=>[ue.id,ue])),h.innerHTML='<option value="">Все группы</option>'+fe.map(ue=>`<option value="${ue.id}">${Ue(ue.name)}</option>`).join(""),y=!0,await j(),h.options[0].textContent=`Все группы (${m})`}catch(P){V(`БД есть, но слова не п
|
||
|
|
<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"),o=r.querySelector("#token-error"),s=r.querySelector("#token-counter"),i=r.querySelector("#btn-download"),u=r.querySelector("#btn-upload"),h=r.querySelector("#status");function w(y){h.textContent=y,console.log(`[JapLearn] ${y}`)}function U(){return rr(n.value)}function M(y){i.disabled=y||!be(U()),u.disabled=!0,n.disabled=y,y||Ge().then(Q=>{!n.disabled&&be(U())&&(u.disabled=!Q)})}function b(){const y=U(),Q=y.length;s.textContent=`${Q} / ${tr}`;const m=be(y),x=Q===0;n.classList.toggle("invalid",!x&&!m),n.setAttribute("aria-invalid",String(!x&&!m)),x?o.textContent="":Q!==tr?o.textContent=`Нужно 24 символа, сейчас ${Q}`:o.textContent=m?"":"Только латинские буквы и цифры",i.disabled=!m,m?Ge().then(I=>{n.disabled||(u.disabled=!I)}):u.disabled=!0}async function F(){const y=U();if(be(y)){wt(y),M(!0),w("Скачивание сохранения...");try{const Q=await ga(y);w(`Скачано ${(Q.length/1024).toFixed(1)} КБ, распаковка...`);const m=await ca(Q);w(`Распаковано ${(m.length/1024).toFixed(1)} КБ, сохранение в OPFS...`),await _n(m),w(`Готово: база ${(m.length/1024).toFixed(1)} КБ сохранена локально (OPFS).`)}catch(Q){w(`Ошибка скачивания: ${Q instanceof Error?Q.message:String(Q)}`)}finally{M(!1),b()}}}n.addEventListener("input",()=>{b()}),n.addEventListener("change",()=>{const y=U();n.value=y,be(y)&&(wt(y),w("Токен сохранён локально. Можно скачивать или выгружать.")),b()}),i.addEventListener("click",()=>{F()}),u.addEventListener("click",()=>{const y=U();be(y)&&(wt(y),w("Выгрузка — следующий шаг (POST /upload/stream/{id})."))});const N=fa();N&&(n.value=N),b(),be(rr(N))&&Ge().then(y=>{w(y?"Токен восстановлен, локальная база на месте.":"Токен восстановлен. Нажми «Скачать», чтобы получить базу."),b()})}const ma={home:"Главная",sync:"Синхронизация"};function Re(r){const n=document.getElementById(r);if(!n)throw new Error(`[JapLearn] Не найден элемент #${r}`);return n}const dr=Re("burger-btn"),We=Re("drawer"),pr=Re("drawer-overlay"),va=Re("page-title"),nr=Re("page");let Et=!1;function Ie(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 ya(r){We.querySelectorAll(".drawer-btn").forEach(n=>{n.classList.toggle("active",n.dataset.page===r)})}function fr(r){va.textContent=ma[r],ya(r),r==="home"?da(nr):wa(nr)}dr.addEventListener("click",()=>{Ie(!Et)});pr.addEventListener("click",()=>{Ie(!1)});document.addEventListener("keydown",r=>{r.key==="Escape"&&Et&&Ie(!1)});We.querySelectorAll(".drawer-btn").forEach(r=>{r.addEventListener("click",()=>{const n=r.dataset.page;(n==="home"||n==="sync")&&fr(n),Ie(!1)})});await kn();Ie(!1);fr("home");console.log("[JapLearn] crossOriginIsolated =",window.crossOriginIsolated);
|