@@ -20,9 +20,9 @@
- `;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"),b=e.querySelector("#words-more");function T(v,x){a.hidden=!1,o.textContent=v,s.hidden=!1}function k(){a.hidden=!0}s.addEventListener("click",()=>{document.querySelector('.drawer-btn[data-page="sync"]')?.click()});let U=!1,N=new Map,w=0,E=[],I=null,J=0,L=!1,z=0,P=0;function C(){const v=u.value;if(v==="")return{search:c.value.trim(),groupId:null};const x=Number(v);return{search:c.value.trim(),groupId:Number.isFinite(x)?x:null}}function _(){b.hidden=!(U&&E.lengthor(v,N,v.id===I)).join(""),g.textContent=w>0?`Показано ${E.length} из ${w}`:"",_()}function F(v){if(I===v){v!=null&&X(v),I=null;return}I!=null&&X(I),I=v,v!=null&&j(v)}function X(v){const x=m.querySelector(`.word-row-wrap[data-word-id="${v}"]`);if(!x)return;const M=x.querySelector(".word-row"),Q=x.querySelector(".word-extra");M&&(M.classList.remove("open"),M.setAttribute("aria-expanded","false")),Q&&(Q.hidden=!0,Q.innerHTML="")}function j(v){const x=E.find(he=>he.id===v),M=m.querySelector(`.word-row-wrap[data-word-id="${v}"]`);if(!x||!M)return;const Q=M.querySelector(".word-row"),O=M.querySelector(".word-extra");Q&&(Q.classList.add("open"),Q.setAttribute("aria-expanded","true")),O&&(O.hidden=!1,O.innerHTML=kr(x,N))}function V(v){y.textContent=v,y.hidden=v===""}async function B(){if(!U||L)return;L=!0;const v=++z,x=C();b.disabled=!0,E.length===0&&V("Загрузка слов…");try{const[M,Q]=await Promise.all([Xn({search:x.search,groupId:x.groupId??void 0}),tr({search:x.search,groupId:x.groupId??void 0,limit:ir,offset:0})]);if(v!==z)return;w=M,E=Q,J=Q.length,I!=null&&!E.some(O=>O.id===I)&&(I=null),R(),V(Q.length===0?"По этому запросу слов нет.":""),Q.length===0&&w===0&&(g.textContent="Слов в базе пока нет.")}catch(M){if(v!==z)return;V(`Не удалось загрузить слова: ${M instanceof Error?M.message:String(M)}`)}finally{v===z&&(L=!1,b.disabled=!1,_())}}async function H(){if(!U||L||E.length>=w)return;L=!0;const v=++z,x=C();b.disabled=!0,b.textContent="Загрузка…";try{const M=await tr({search:x.search,groupId:x.groupId??void 0,limit:ir,offset:J});if(v!==z)return;E=E.concat(M),J+=M.length,m.insertAdjacentHTML("beforeend",M.map(Q=>or(Q,N,Q.id===I)).join("")),g.textContent=`Показано ${E.length} из ${w}`,_()}catch(M){if(v!==z)return;V(`Не удалось догрузить: ${M instanceof Error?M.message:String(M)}`)}finally{v===z&&(L=!1,b.disabled=!1,_())}}c.addEventListener("input",()=>{window.clearTimeout(P),P=window.setTimeout(()=>{B()},300)}),c.addEventListener("keydown",v=>{v.key==="Enter"&&(window.clearTimeout(P),B())}),u.addEventListener("change",()=>{B()}),b.addEventListener("click",()=>{H()}),m.addEventListener("click",v=>{const x=v.target.closest(".word-row");if(!x||x.disabled)return;const M=Number(x.dataset.wordId);Number.isFinite(M)&&F(I===M?null:M)}),(async()=>{if(!await Je()){T("Локальной базы нет — скачай сохранение."),V("Нет локальной базы — скачай сохранение на странице «Синхронизация»."),g.textContent="";return}try{const x=await ra();if(!$t(x)){T("Файл в OPFS не похож на SQLite — перекачай сохранение.",!0),V("Файл в OPFS не похож на SQLite — перекачай сохранение.");return}k();try{const[Q,O]=await Promise.all([zn(),vr()]),he=Q;N=new Map(O.map(ue=>[ue.id,ue])),u.innerHTML=''+he.map(ue=>``).join(""),U=!0,await B(),u.options[0].textContent=`Все группы (${w})`}catch(Q){V(`БД есть, но слова не прочитались: ${Q instanceof Error?Q.message:String(Q)}`)}}catch(x){T(`Не удалось прочитать базу: ${x instanceof Error?x.message:String(x)}`)}})()}function cr(e){return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function La(e){const r=e%10,a=e%100;return r===1&&a!==11?"запись":r>=2&&r<=4&&(a<12||a>14)?"записи":"записей"}function Fa(e,r){const a=cr(e.name)||`Колода #${e.id}`;return``}function _a(e,r){e.innerHTML=`
+ `;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"),b=e.querySelector("#words-more");function T(v,x){a.hidden=!1,o.textContent=v,s.hidden=!1}function k(){a.hidden=!0}s.addEventListener("click",()=>{document.querySelector('.drawer-btn[data-page="sync"]')?.click()});let U=!1,N=new Map,w=0,E=[],I=null,J=0,L=!1,z=0,P=0;function C(){const v=u.value;if(v==="")return{search:c.value.trim(),groupId:null};const x=Number(v);return{search:c.value.trim(),groupId:Number.isFinite(x)?x:null}}function _(){b.hidden=!(U&&E.lengthor(v,N,v.id===I)).join(""),g.textContent=w>0?`Показано ${E.length} из ${w}`:"",_()}function F(v){if(I===v){v!=null&&X(v),I=null;return}I!=null&&X(I),I=v,v!=null&&j(v)}function X(v){const x=m.querySelector(`.word-row-wrap[data-word-id="${v}"]`);if(!x)return;const M=x.querySelector(".word-row"),Q=x.querySelector(".word-extra");M&&(M.classList.remove("open"),M.setAttribute("aria-expanded","false")),Q&&(Q.hidden=!0,Q.innerHTML="")}function j(v){const x=E.find(he=>he.id===v),M=m.querySelector(`.word-row-wrap[data-word-id="${v}"]`);if(!x||!M)return;const Q=M.querySelector(".word-row"),O=M.querySelector(".word-extra");Q&&(Q.classList.add("open"),Q.setAttribute("aria-expanded","true")),O&&(O.hidden=!1,O.innerHTML=kr(x,N))}function V(v){y.textContent=v,y.hidden=v===""}async function B(){if(!U||L)return;L=!0;const v=++z,x=C();b.disabled=!0,E.length===0&&V("Загрузка слов…");try{const[M,Q]=await Promise.all([Xn({search:x.search,groupId:x.groupId??void 0}),tr({search:x.search,groupId:x.groupId??void 0,limit:ir,offset:0})]);if(v!==z)return;w=M,E=Q,J=Q.length,I!=null&&!E.some(O=>O.id===I)&&(I=null),R(),V(Q.length===0?"По этому запросу слов нет.":""),Q.length===0&&w===0&&(g.textContent="Слов в базе пока нет.")}catch(M){if(v!==z)return;V(`Не удалось загрузить слова: ${M instanceof Error?M.message:String(M)}`)}finally{v===z&&(L=!1,b.disabled=!1,_())}}async function H(){if(!U||L||E.length>=w)return;L=!0;const v=++z,x=C();b.disabled=!0,b.textContent="Загрузка…";try{const M=await tr({search:x.search,groupId:x.groupId??void 0,limit:ir,offset:J});if(v!==z)return;E=E.concat(M),J+=M.length,m.insertAdjacentHTML("beforeend",M.map(Q=>or(Q,N,Q.id===I)).join("")),g.textContent=`Показано ${E.length} из ${w}`,_()}catch(M){if(v!==z)return;V(`Не удалось догрузить: ${M instanceof Error?M.message:String(M)}`)}finally{v===z&&(L=!1,b.disabled=!1,_())}}c.addEventListener("input",()=>{window.clearTimeout(P),P=window.setTimeout(()=>{B()},300)}),c.addEventListener("keydown",v=>{v.key==="Enter"&&(window.clearTimeout(P),B())}),u.addEventListener("change",()=>{B()}),b.addEventListener("click",()=>{H()}),m.addEventListener("click",v=>{const x=v.target.closest(".word-row");if(!x||x.disabled)return;const M=Number(x.dataset.wordId);Number.isFinite(M)&&F(I===M?null:M)}),(async()=>{if(!await Je()){T("Локальной базы нет — скачай сохранение."),V("Нет локальной базы — скачай сохранение на странице «Синхронизация»."),g.textContent="";return}try{const x=await ra();if(!$t(x)){T("Файл в OPFS не похож на SQLite — перекачай сохранение.",!0),V("Файл в OPFS не похож на SQLite — перекачай сохранение.");return}k();try{const[Q,O]=await Promise.all([zn(),vr()]),he=Q;N=new Map(O.map(ue=>[ue.id,ue])),u.innerHTML=''+he.map(ue=>``).join(""),U=!0,await B(),u.options[0].textContent=`Все группы (${w})`}catch(Q){V(`БД есть, но слова не прочитались: ${Q instanceof Error?Q.message:String(Q)}`)}}catch(x){T(`Не удалось прочитать базу: ${x instanceof Error?x.message:String(x)}`)}})()}function cr(e){return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function La(e){const r=e%10,a=e%100;return r===1&&a!==11?"запись":r>=2&&r<=4&&(a<12||a>14)?"записи":"записей"}function Fa(e,r){const a=cr(e.name)||`Колода #${e.id}`;return``}function _a(e,r){e.innerHTML=`
-
🗂️ Колоды
+
Колоды
Проверка локальной базы…
@@ -47,20 +47,20 @@
-
+
-
📚 ${b==null?"Количество слов неизвестно":`${b} ${Ra(b)}`}
+
${b==null?"Количество слов неизвестно":`${b} ${Ra(b)}`}
-
+
-
+
`;const T=u.querySelector("#deck-form"),k=u.querySelector("#deck-forward"),U=u.querySelector("#deck-backward"),N=u.querySelector("#deck-filter"),w=u.querySelector("#deck-save"),E=u.querySelector("#deck-form-status"),I=u.querySelector("#deck-review"),J=u.querySelector("#deck-history");I.addEventListener("click",()=>a.goReview(m.id)),J.addEventListener("click",()=>a.goHistory(m.id));const L=()=>`${k.value}
${U.value}
-${N.value}`;let z=L();function P(){w.disabled=L()===z||w.dataset.busy==="1"}for(const C of[k,U,N])C.addEventListener("input",P);P(),T.addEventListener("submit",C=>{C.preventDefault(),!w.disabled&&(w.dataset.busy="1",w.disabled=!0,w.textContent="Сохранение…",E.textContent="",(async()=>{try{const _=await Vn(m.id,{forward:k.value,backward:U.value,filter:N.value});if(!_){E.textContent="Колода пропала из базы — вернись к списку.";return}k.value=_.forward,U.value=_.backward,N.value=_.filter,z=L(),E.textContent="✅ Сохранено в локальную базу."}catch(_){E.textContent=`Не удалось сохранить: ${_ instanceof Error?_.message:String(_)}`}finally{delete w.dataset.busy,w.textContent="💾 Сохранить",P()}})())})})()}function _e(e){return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}const Ja=50,Oa=[{label:"Не получилось",cls:"grade-fail"},{label:"Тяжело",cls:"grade-hard"},{label:"Хорошо",cls:"grade-good"},{label:"Легко",cls:"grade-easy"}];function Ka(e){const r=Oa[e.answer]??{label:`Оценка ${e.answer}`,cls:""},a=e.wordKey?`${_e(e.wordKey)}`+(e.wordValue?` — ${_e(e.wordValue)}`:""):`Слово #${e.word_id} (удалено)`,o=e.before===e.after?`${e.before}`:`${e.before}→${e.after}`;return`
@@ -69,7 +69,7 @@ ${N.value}`;let z=L();function P(){w.disabled=L()===z||w.dataset.busy==="1"}for(
- `;const o=e.querySelector("#hist-back"),s=e.querySelector("#hist-title"),c=e.querySelector("#hist-count"),u=e.querySelector("#hist-status"),g=e.querySelector("#hist-list"),y=e.querySelector("#hist-more");o.addEventListener("click",()=>a.goDeck(r));function m(w){u.textContent=w,u.hidden=w===""}let b=0,T=0,k=!1;function U(){const w=b-T;y.hidden=w<=0,w>0&&(y.textContent=`Показать ещё (${w} осталось)`)}async function N(){if(!(k||T>=b)){k=!0,y.disabled=!0,y.textContent="Загрузка…";try{const w=await ea(r,{limit:Ja,offset:T});T+=w.length,g.insertAdjacentHTML("beforeend",w.map(Ka).join("")),c.textContent=`Показано ${T} из ${b}`}catch(w){m(`Не удалось загрузить историю: ${w instanceof Error?w.message:String(w)}`)}finally{k=!1,y.disabled=!1,U()}}}y.addEventListener("click",()=>{N()}),(async()=>{if(!await Je()){s.textContent="История недоступна",m("Нет локальной базы — скачай сохранение на странице «Синхронизация».");return}try{const E=await nt(r);if(!E){s.textContent="Колода не найдена",m(`Колоды с id=${r} нет в локальной базе.`);return}if(s.textContent=`🕘 ${E.name||`Колода #${E.id}`}`,b=await An(E.id),b===0){c.textContent="Ответов пока нет — начни повторение.",m("");return}m(""),await N()}catch(E){s.textContent="История недоступна",m(`Не удалось загрузить историю: ${E instanceof Error?E.message:String(E)}`)}})()}function Xa(e){return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}const za=[{value:0,label:"😞 Не получилось",cls:"grade-fail"},{value:1,label:"😟 Тяжело",cls:"grade-hard"},{value:2,label:"🙂 Хорошо",cls:"grade-good"},{value:3,label:"🤩 Легко",cls:"grade-easy"}];function Ba(e,r,a){e.innerHTML=`
+ `;const o=e.querySelector("#hist-back"),s=e.querySelector("#hist-title"),c=e.querySelector("#hist-count"),u=e.querySelector("#hist-status"),g=e.querySelector("#hist-list"),y=e.querySelector("#hist-more");o.addEventListener("click",()=>a.goDeck(r));function m(w){u.textContent=w,u.hidden=w===""}let b=0,T=0,k=!1;function U(){const w=b-T;y.hidden=w<=0,w>0&&(y.textContent=`Показать ещё (${w} осталось)`)}async function N(){if(!(k||T>=b)){k=!0,y.disabled=!0,y.textContent="Загрузка…";try{const w=await ea(r,{limit:Ja,offset:T});T+=w.length,g.insertAdjacentHTML("beforeend",w.map(Ka).join("")),c.textContent=`Показано ${T} из ${b}`}catch(w){m(`Не удалось загрузить историю: ${w instanceof Error?w.message:String(w)}`)}finally{k=!1,y.disabled=!1,U()}}}y.addEventListener("click",()=>{N()}),(async()=>{if(!await Je()){s.textContent="История недоступна",m("Нет локальной базы — скачай сохранение на странице «Синхронизация».");return}try{const E=await nt(r);if(!E){s.textContent="Колода не найдена",m(`Колоды с id=${r} нет в локальной базе.`);return}if(s.textContent=E.name||`Колода #${E.id}`,b=await An(E.id),b===0){c.textContent="Ответов пока нет — начни повторение.",m("");return}m(""),await N()}catch(E){s.textContent="История недоступна",m(`Не удалось загрузить историю: ${E instanceof Error?E.message:String(E)}`)}})()}function Xa(e){return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}const za=[{value:0,label:"Не получилось",cls:"grade-fail"},{value:1,label:"Тяжело",cls:"grade-hard"},{value:2,label:"Хорошо",cls:"grade-good"},{value:3,label:"Легко",cls:"grade-easy"}];function Ba(e,r,a){e.innerHTML=`
Повторение…
@@ -90,11 +90,11 @@ ${N.value}`;let z=L();function P(){w.disabled=L()===z||w.dataset.busy==="1"}for(
`,g.querySelector("#review-card").addEventListener("click",()=>{w||(w=!0,z())}),g.querySelectorAll(".review-grade").forEach(M=>{M.addEventListener("click",()=>{const Q=Number(M.dataset.grade);P(Q,j)})})}async function P(R,F){if(J)return;J=!0;const X=m[N],j=E??1,V=[...g.querySelectorAll(".review-grade")];for(const B of V)B.disabled=!0;try{await Wn({deckId:r,wordId:X,answer:R,before:j,after:j})}catch(B){y(`Ответ не записался: ${B instanceof Error?B.message:String(B)}${F}`),J=!1;for(const H of V)H.disabled=!1;return}N+=1,w=!1,E=null,N>=m.length?C():await z()}function C(){c.textContent=`Готово: ${m.length} из ${m.length}`,y(""),g.innerHTML=`
Все карточки сессии просмотрены, ответы записаны в history.
-
+
`,g.querySelector("#review-again").addEventListener("click",()=>{_()})}async function _(){if(y("Перемешиваю…"),g.innerHTML="",c.textContent="",m=await ar(r),N=0,w=!1,m.length===0){y("В колоде нет слов для повторения.");return}b=await rr(m),z()}(async()=>{if(!await Je()){s.textContent="Повторение недоступно",y("Нет локальной базы — скачай сохранение на странице «Синхронизация».");return}try{const[F,X]=await Promise.all([nt(r),vr()]);if(!F){s.textContent="Колода не найдена",y(`Колоды с id=${r} нет в локальной базе. Вернись к списку.`);return}if(T=X,k=F.forward,U=F.backward,s.textContent=F.name||`Колода #${F.id}`,m=await ar(F.id),m.length===0){c.textContent="",y("В колоде нет слов для повторения.");return}b=await rr(m),N=0,w=!1,await z()}catch(F){s.textContent="Повторение недоступно",y(`Не удалось загрузить карточки: ${F instanceof Error?F.message:String(F)}`)}})()}const lr=24,Va=/^[A-Za-z0-9]{24}$/,Er="japlearn_sync_token";function ur(e){return e.trim().replace(/\s+/g,"")}function Qe(e){return Va.test(e)}function Ya(){try{return localStorage.getItem(Er)??""}catch{return""}}function dr(e){try{localStorage.setItem(Er,e)}catch{}}const Cr="https://learning.micialware.ru";function Za(e,r=Cr){return`${r.replace(/\/+$/,"")}/download/${encodeURIComponent(e)}`}async function Ga(e,r,a=Cr){const o=Za(e,a);let s;try{s=await fetch(o,{method:"GET",...r})}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 ja(e){e.innerHTML=`
-
📡 Синхронизация
+
Синхронизация
Токен сохранения — 24 символа: латинские буквы и цифры.