displaing words
This commit is contained in:
1 parent
8a0834abac
commit
ca18ee8cc7
14 files changed
+537
-87
No files matched your search
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
var ft=(()=>{var qe=import.meta.url;return function(oe={}){var fe,i=oe,ye,be,Oe=new Promise((e,t)=>{ye=e,be=t}),Xe=typeof window=="object",Ce=typeof importScripts=="function";typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string"&&process.type!="renderer";const Pe=globalThis.sqlite3InitModuleState||Object.assign(Object.create(null),{debugModule:()=>{}});delete globalThis.sqlite3InitModuleState,Pe.debugModule("globalThis.location =",globalThis.location);var He=Object.assign({},i),re="./this.program",me="";function Ie(e){return i.locateFile?i.locateFile(e,me):me+e}var De,je;(Xe||Ce)&&(Ce?me=self.location.href:typeof document<"u"&&document.currentScript&&(me=document.currentScript.src),qe&&(me=qe),me.startsWith("blob:")?me="":me=me.substr(0,me.replace(/[?#].*/,"").lastIndexOf("/")+1),Ce&&(je=e=>{var t=new XMLHttpRequest;return t.open("GET",e,!1),t.responseType="arraybuffer",t.send(null),new Uint8Array(t.response)}),De=e=>fetch(e,{credentials:"same-origin"}).then(t=>t.ok?t.arrayBuffer():Promise.reject(new Error(t.status+" : "+t.url))));var Ve=i.print||console.log.bind(console),Le=i.printErr||console.error.bind(console);Object.assign(i,He),He=null,i.arguments&&i.arguments,i.thisProgram&&(re=i.thisProgram);var Ye=i.wasmBinary,Re,dt=!1,xe,Be,Ge,ue,Ee,ke;function pt(){var e=Re.buffer;i.HEAP8=xe=new Int8Array(e),i.HEAP16=Ge=new Int16Array(e),i.HEAPU8=Be=new Uint8Array(e),i.HEAPU16=new Uint16Array(e),i.HEAP32=ue=new Int32Array(e),i.HEAPU32=Ee=new Uint32Array(e),i.HEAPF32=new Float32Array(e),i.HEAPF64=new Float64Array(e),i.HEAP64=ke=new BigInt64Array(e),i.HEAPU64=new BigUint64Array(e)}if(i.wasmMemory)Re=i.wasmMemory;else{var Pt=i.INITIAL_MEMORY||16777216;Re=new WebAssembly.Memory({initial:Pt/65536,maximum:32768})}pt();var ht=[],mt=[],gt=[];function Lt(){var e=i.preRun;e&&(typeof e=="function"&&(e=[e]),e.forEach(Rt)),ot(ht)}function Ct(){!i.noFSInit&&!a.initialized&&a.init(),a.ignorePermissions=!1,ot(mt)}function Dt(){var e=i.postRun;e&&(typeof e=="function"&&(e=[e]),e.forEach(Mt)),ot(gt)}function Rt(e){ht.unshift(e)}function Nt(e){mt.unshift(e)}function Mt(e){gt.unshift(e)}var Ne=0,$e=null;function tr(e){return e}function rt(e){Ne++,i.monitorRunDependencies?.(Ne)}function Ze(e){if(Ne--,i.monitorRunDependencies?.(Ne),Ne==0&&$e){var t=$e;$e=null,t()}}function st(e){i.onAbort?.(e),e="Aborted("+e+")",Le(e),dt=!0,e+=". Build with -sASSERTIONS for more info.";var t=new WebAssembly.RuntimeError(e);throw be(t),t}var jt="data:application/octet-stream;base64,",qt=e=>e.startsWith(jt);function Bt(){if(i.locateFile){var e="sqlite3.wasm";return qt(e)?e:Ie(e)}return new URL("/assets/sqlite3-DBpDb1lf.wasm",import.meta.url).href}var it;function bt(e){if(e==it&&Ye)return new Uint8Array(Ye);if(je)return je(e);throw"both async and sync fetching of the wasm failed"}function Ut(e){return Ye?Promise.resolve().then(()=>bt(e)):De(e).then(t=>new Uint8Array(t),()=>bt(e))}function yt(e,t,n){return Ut(e).then(r=>WebAssembly.instantiate(r,t)).then(n,r=>{Le(`failed to asynchronously prepare wasm: ${r}`),st(r)})}function zt(e,t,n,r){return!e&&typeof WebAssembly.instantiateStreaming=="function"&&!qt(t)&&typeof fetch=="function"?fetch(t,{credentials:"same-origin"}).then(s=>{var o=WebAssembly.instantiateStreaming(s,n);return o.then(r,function(p){return Le(`wasm streaming compile failed: ${p}`),Le("falling back to ArrayBuffer instantiation"),yt(t,n,r)})}):yt(t,n,r)}function Wt(){return{env:It,wasi_snapshot_preview1:It}}function Qt(){var e=Wt();function t(r,s){return h=r.exports,Nt(h.__wasm_call_ctors),Ze(),h}rt();function n(r){t(r.instance)}if(i.instantiateWasm)try{return i.instantiateWasm(e,t)}catch(r){Le(`Module.instantiateWasm callback failed with error: ${r}`),be(r)}return it??=Bt(),zt(Ye,it,e,n).catch(be),{}}var ot=e=>{e.forEach(t=>t(i))};i.noExitRuntime;var de={isAbs:e=>e.charAt(0)==="/",splitPath:e=>{var t=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return t.exec(e).slice(1)},normalizeArray:(e,t)=>{for(var n=0,r=e.length-1;r>=0;r--){var s=e[r];s==="."?e.splice(r,1):s===".."?(e.splice(r,1),n++):n&&(e.splice(r,1),n--)}if(t)for(;n;n--)e.unshift("..");return e},normalize:e=>{var t=de.isAbs(e),n=e.substr(-1)==="/";return e=de.normalizeArray(e.split("/").filter(r=>!!r),!t).join("/"),!e&&!t&&(e="."),e&&n&&(e+="/"),(t?"/":"")+e},dirname:e=>{var t=de.splitPath(e),n=t[0],r=t[1];return!n&&!r?".":(r&&(r=r.substr(0,r.length-1)),n+r)},basename:e=>{if(e==="/")return"/";e=de.normalize(e),e=e.replace(/\/$/,"");var t=e.lastIndexOf("/");return t===-1?e:e.substr(t+1)},join:(...e)=>de.normalize(e.join("/")),join2:(e,t)=>de.normalize(e+"/"+t)},Ht=()=>{if(typeof crypto=="object"&&typeof crypto.getRandomValues=="function")return e=>crypto.getRandomValues(e);st("initRandomDevice")},wt=e=>(wt=Ht())(e),Te={resolve:(...e)=>{for(var t="",n=!1,r=e.length-1;r>=-1&&!n;r--){var s=r>=0?e[r]:a.cwd();if(typeof s!="string")throw new TypeError("Arguments to path.resolve must be strings");if(!s)return"";t=s+"/"+t,n=de.iLine truncated
|
||||
`)),!e)return null;lt=vt(e)}return lt.shift()},Me={ttys:[],init(){},shutdown(){},register(e,t){Me.ttys[e]={input:[],output:[],ops:t},a.registerDevice(e,Me.stream_ops)},stream_ops:{open(e){var t=Me.ttys[e.node.rdev];if(!t)throw new a.ErrnoError(43);e.tty=t,e.seekable=!1},close(e){e.tty.ops.fsync(e.tty)},fsync(e){e.tty.ops.fsync(e.tty)},read(e,t,n,r,s){if(!e.tty||!e.tty.ops.get_char)throw new a.ErrnoError(60);for(var o=0,p=0;p<r;p++){var g;try{g=e.tty.ops.get_char(e.tty)}catch{throw new a.ErrnoError(29)}if(g===void 0&&o===0)throw new a.ErrnoError(6);if(g==null)break;o++,t[n+p]=g}return o&&(e.node.timestamp=Date.now()),o},write(e,t,n,r,s){if(!e.tty||!e.tty.ops.put_char)throw new a.ErrnoError(60);try{for(var o=0;o<r;o++)e.tty.ops.put_char(e.tty,t[n+o])}catch{throw new a.ErrnoError(29)}return r&&(e.node.timestamp=Date.now()),o}},default_tty_ops:{get_char(e){return Vt()},put_char(e,t){t===null||t===10?(Ve(Ue(e.output)),e.output=[]):t!=0&&e.output.push(t)},fsync(e){e.output&&e.output.length>0&&(Ve(Ue(e.output)),e.output=[])},ioctl_tcgets(e){return{c_iflag:25856,c_oflag:5,c_cflag:191,c_lflag:35387,c_cc:[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},ioctl_tcsets(e,t,n){return 0},ioctl_tiocgwinsz(e){return[24,80]}},default_tty1_ops:{put_char(e,t){t===null||t===10?(Le(Ue(e.output)),e.output=[]):t!=0&&e.output.push(t)},fsync(e){e.output&&e.output.length>0&&(Le(Ue(e.output)),e.output=[])}}},Gt=(e,t)=>{Be.fill(0,e,e+t)},xt=(e,t)=>Math.ceil(e/t)*t,St=e=>{e=xt(e,65536);var t=kt(65536,e);return t&&Gt(t,e),t},le={ops_table:null,mount(e){return le.createNode(null,"/",16895,0)},createNode(e,t,n,r){if(a.isBlkdev(n)||a.isFIFO(n))throw new a.ErrnoError(63);le.ops_table||={dir:{node:{getattr:le.node_ops.getattr,setattr:le.node_ops.setattr,lookup:le.node_ops.lookup,mknod:le.node_ops.mknod,rename:le.node_ops.rename,unlink:le.node_ops.unlink,rmdir:le.node_ops.rmdir,readdir:le.node_ops.readdir,symlink:le.node_ops.symlink},stream:{llseek:le.stream_ops.llseek}},file:{node:{getattr:le.node_ops.getattr,setattr:le.node_ops.setattr},stream:{llseek:le.stream_ops.llseek,read:le.stream_ops.read,write:le.stream_ops.write,allocate:le.stream_ops.allocate,mmap:le.stream_ops.mmap,msync:le.stream_ops.msync}},link:{node:{getattr:le.node_ops.getattr,setattr:le.node_ops.setattr,readlink:le.node_ops.readlink},stream:{}},chrdev:{node:{getattr:le.node_ops.getattr,setattr:le.node_ops.setattr},stream:a.chrdev_stream_ops}};var s=a.createNode(e,t,n,r);return a.isDir(s.mode)?(s.node_ops=le.ops_table.dir.node,s.stream_ops=le.ops_table.dir.stream,s.contents={}):a.isFile(s.mode)?(s.node_ops=le.ops_table.file.node,s.stream_ops=le.ops_table.file.stream,s.usedBytes=0,s.contents=null):a.isLink(s.mode)?(s.node_ops=le.ops_table.link.node,s.stream_ops=le.ops_table.link.stream):a.isChrdev(s.mode)&&(s.node_ops=le.ops_table.chrdev.node,s.stream_ops=le.ops_table.chrdev.stream),s.timestamp=Date.now(),e&&(e.contents[t]=s,e.timestamp=s.timestamp),s},getFileDataAsTypedArray(e){return e.contents?e.contents.subarray?e.contents.subarray(0,e.usedBytes):new Uint8Array(e.contents):new Uint8Array(0)},expandFileStorage(e,t){var n=e.contents?e.contents.length:0;if(!(n>=t)){var r=1024*1024;t=Math.max(t,n*(n<r?2:1.125)>>>0),n!=0&&(t=Math.max(t,256));var s=e.contents;e.contents=new Uint8Array(t),e.usedBytes>0&&e.contents.set(s.subarray(0,e.usedBytes),0)}},resizeFileStorage(e,t){if(e.usedBytes!=t)if(t==0)e.contents=null,e.usedBytes=0;else{var n=e.contents;e.contents=new Uint8Array(t),n&&e.contents.set(n.subarray(0,Math.min(t,e.usedBytes))),e.usedBytes=t}},node_ops:{getattr(e){var t={};return t.dev=a.isChrdev(e.mode)?e.id:1,t.ino=e.id,t.mode=e.mode,t.nlink=1,t.uid=0,t.gid=0,t.rdev=e.rdev,a.isDir(e.mode)?t.size=4096:a.isFile(e.mode)?t.size=e.usedBytes:a.isLink(e.mode)?t.size=e.link.length:t.size=0,t.atime=new Date(e.timestamp),t.mtime=new Date(e.timestamp),t.ctime=new Date(e.timestamp),t.blksize=4096,t.blocks=Math.ceil(t.size/t.blksize),t},setattr(e,t){t.mode!==void 0&&(e.mode=t.mode),t.timestamp!==void 0&&(e.timestamp=t.timestamp),t.size!==void 0&&le.resizeFileStorage(e,t.size)},lookup(e,t){throw a.genericErrors[44]},mknod(e,t,n,r){return le.createNode(e,t,n,r)},rename(e,t,n){if(a.isDir(e.mode)){var r;try{r=a.lookupNode(t,n)}catch{}if(r)for(var s in r.contents)throw new a.ErrnoError(55)}delete e.parent.contents[e.name],e.parent.timestamp=Date.now(),e.name=n,t.contents[n]=e,t.timestamp=e.parent.timestamp},unlink(e,t){delete e.contents[t],e.timestamp=Date.now()},rmdir(e,t){var n=a.lookupNode(e,t);for(var r in n.contents)throw new a.ErrnoError(55);delete e.contents[t],e.timestamp=Date.now()},readdir(e){var t=[".",".."];for(var n of Object.keys(e.contents))t.push(n);return t},symlink(e,t,n){var r=le.createNode(e,t,41471,0);return r.link=n,r},readlink(e){if(!a.isLink(e.mode))throw new a.ErrnoError(28);return e.link}},stream_ops:{read(e,t,n,r,s){var o=e.node.contents;if(s>=e.node.usedBytes)return 0;var p=Math.min(e.node.usedBytes-s,r);if(p>8&&o.subarray)t.set(o.sub
Line truncated
|
||||
Total of`,C,"op(s) for",M,"ms (incl. "+J+" ms of waiting on the async side)"),t.config.log("Serialization metrics:",I.s11n),m.postMessage({type:"opfs-async-metrics"})},reset:function(){let E;const C=J=>J.count=J.time=J.wait=0;for(E in u.opIds)C(I[E]=Object.create(null));let M=I.s11n=Object.create(null);M=M.serialize=Object.create(null),M.count=M.time=0,M=I.s11n.deserialize=Object.create(null),M.count=M.time=0}};const c=new S,d=new N().addOnDispose(()=>c.dispose());let y;const B=E=>(y=!0,d.dispose(),O(E)),f=()=>(y=!1,g(t)),m=new Worker(new URL("/assets/sqlite3-opfs-async-proxy-B_ImRJXp.js",import.meta.url));setTimeout(()=>{y===void 0&&B(new Error("Timeout while waiting for OPFS async proxy worker."))},4e3),m._originalOnError=m.onerror,m.onerror=function(E){A("Error initializing OPFS asyncer:",E),B(new Error("Loading OPFS async Worker failed for unknown reasons."))};const l=q.sqlite3_vfs_find(null),_=l?new N(l):null;c.$iVersion=1,d.$iVersion=2,d.$szOsFile=q.sqlite3_file.structInfo.sizeof,d.$mxPathname=1024,d.$zName=x.allocCString("opfs"),d.$xDlOpen=d.$xDlError=d.$xDlSym=d.$xDlClose=null,d.addOnDispose("$zName",d.$zName,"cleanup default VFS wrapper",()=>_?_.dispose():null);const u=Object.create(null);u.verbose=s.verbose,u.littleEndian=(()=>{const E=new ArrayBuffer(2);return new DataView(E).setInt16(0,256,!0),new Int16Array(E)[0]===256})(),u.asyncIdleWaitTime=150,u.asyncS11nExceptions=1,u.fileBufferSize=1024*64,u.sabS11nOffset=u.fileBufferSize,u.sabS11nSize=d.$mxPathname*2,u.sabIO=new SharedArrayBuffer(u.fileBufferSize+u.sabS11nSize),u.opIds=Object.create(null);const I=Object.create(null);{let E=0;u.opIds.whichOp=E++,u.opIds.rc=E++,u.opIds.xAccess=E++,u.opIds.xClose=E++,u.opIds.xDelete=E++,u.opIds.xDeleteNoWait=E++,u.opIds.xFileSize=E++,u.opIds.xLock=E++,u.opIds.xOpen=E++,u.opIds.xRead=E++,u.opIds.xSleep=E++,u.opIds.xSync=E++,u.opIds.xTruncate=E++,u.opIds.xUnlock=E++,u.opIds.xWrite=E++,u.opIds.mkdir=E++,u.opIds["opfs-async-metrics"]=E++,u.opIds["opfs-async-shutdown"]=E++,u.opIds.retry=E++,u.sabOP=new SharedArrayBuffer(E*4),L.metrics.reset()}u.sq3Codes=Object.create(null),["SQLITE_ACCESS_EXISTS","SQLITE_ACCESS_READWRITE","SQLITE_BUSY","SQLITE_CANTOPEN","SQLITE_ERROR","SQLITE_IOERR","SQLITE_IOERR_ACCESS","SQLITE_IOERR_CLOSE","SQLITE_IOERR_DELETE","SQLITE_IOERR_FSYNC","SQLITE_IOERR_LOCK","SQLITE_IOERR_READ","SQLITE_IOERR_SHORT_READ","SQLITE_IOERR_TRUNCATE","SQLITE_IOERR_UNLOCK","SQLITE_IOERR_WRITE","SQLITE_LOCK_EXCLUSIVE","SQLITE_LOCK_NONE","SQLITE_LOCK_PENDING","SQLITE_LOCK_RESERVED","SQLITE_LOCK_SHARED","SQLITE_LOCKED","SQLITE_MISUSE","SQLITE_NOTFOUND","SQLITE_OPEN_CREATE","SQLITE_OPEN_DELETEONCLOSE","SQLITE_OPEN_MAIN_DB","SQLITE_OPEN_READONLY"].forEach(E=>{(u.sq3Codes[E]=q[E])===void 0&&D("Maintenance required: not found:",E)}),u.opfsFlags=Object.assign(Object.create(null),{OPFS_UNLOCK_ASAP:1,OPFS_UNLINK_BEFORE_OPEN:2,defaultUnlockAsap:!1});const U=(E,...C)=>{const M=u.opIds[E]||D("Invalid op ID:",E);u.s11n.serialize(...C),Atomics.store(u.sabOPView,u.opIds.rc,-1),Atomics.store(u.sabOPView,u.opIds.whichOp,M),Atomics.notify(u.sabOPView,u.opIds.whichOp);const J=performance.now();for(;Atomics.wait(u.sabOPView,u.opIds.rc,-1)!=="not-equal";);const G=Atomics.load(u.sabOPView,u.opIds.rc);if(I[E].wait+=performance.now()-J,G&&u.asyncS11nExceptions){const te=u.s11n.deserialize();te&&A(E+"() async error:",...te)}return G};L.debug={asyncShutdown:()=>{T("Shutting down OPFS async listener. The OPFS VFS will no longer work."),U("opfs-async-shutdown")},asyncRestart:()=>{T("Attempting to restart OPFS VFS async listener. Might work, might not."),m.postMessage({type:"opfs-async-restart"})}};const ee=()=>{if(u.s11n)return u.s11n;const E=new TextDecoder,C=new TextEncoder("utf-8"),M=new Uint8Array(u.sabIO,u.sabS11nOffset,u.sabS11nSize),J=new DataView(u.sabIO,u.sabS11nOffset,u.sabS11nSize);u.s11n=Object.create(null);const G=Object.create(null);G.number={id:1,size:8,getter:"getFloat64",setter:"setFloat64"},G.bigint={id:2,size:8,getter:"getBigInt64",setter:"setBigInt64"},G.boolean={id:3,size:4,getter:"getInt32",setter:"setInt32"},G.string={id:4};const te=Y=>G[typeof Y]||D("Maintenance required: this value type cannot be serialized.",Y),ae=Y=>{switch(Y){case G.number.id:return G.number;case G.bigint.id:return G.bigint;case G.boolean.id:return G.boolean;case G.string.id:return G.string;default:D("Invalid type ID:",Y)}};return u.s11n.deserialize=function(Y=!1){++I.s11n.deserialize.count;const we=performance.now(),F=M[0],V=F?[]:null;if(F){const K=[];let X=1,_e,ge,pe;for(_e=0;_e<F;++_e,++X)K.push(ae(M[X]));for(_e=0;_e<F;++_e){const he=K[_e];he.getter?(pe=J[he.getter](X,u.littleEndian),X+=he.size):(ge=J.getInt32(X,u.littleEndian),X+=4,pe=E.decode(M.slice(X,X+ge)),X+=ge),V.push(pe)}}return Y&&(M[0]=0),I.s11n.deserialize.time+=performance.now()-we,V},u.s11n.serialize=function(...Y){const we=performance.now();if(++I.s11n.serialize.count,Y.length){const F=[];let V=0,K=1;for(M[0]=Y.length&255;V<Y.length;++V,++K)F.push(te(Y[V])),M[K]=F[V].id;for(V
Line truncated
|
||||
Vendored
-45
@@ -1,45 +0,0 @@
|
||||
(function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const s of document.querySelectorAll('link[rel="modulepreload"]'))u(s);new MutationObserver(s=>{for(const l of s)if(l.type==="childList")for(const h of l.addedNodes)h.tagName==="LINK"&&h.rel==="modulepreload"&&u(h)}).observe(document,{childList:!0,subtree:!0});function i(s){const l={};return s.integrity&&(l.integrity=s.integrity),s.referrerPolicy&&(l.referrerPolicy=s.referrerPolicy),s.crossOrigin==="use-credentials"?l.credentials="include":s.crossOrigin==="anonymous"?l.credentials="omit":l.credentials="same-origin",l}function u(s){if(s.ep)return;s.ep=!0;const l=i(s);fetch(s.href,l)}})();const vt="japanese_app.db";async function wt(){return navigator.storage.getDirectory()}async function gn(r){const i=await(await wt()).getFileHandle(vt,{create:!0}),u=i.createSyncAccessHandle;if(typeof u=="function")try{const l=await u.call(i);try{l.truncate(0),l.write(r,{at:0}),l.flush()}finally{l.close()}return}catch{}const s=await i.createWritable();try{await s.truncate(0);const l=new Uint8Array(r.length);l.set(r),await s.write(l)}finally{await s.close()}}async function vn(){const r=await wt();let n;try{n=await r.getFileHandle(vt)}catch{throw new Error("Локальная база не найдена — сначала скачай сохранение")}const i=await n.getFile();if(i.size===0)throw new Error("Локальная база пуста");return new Uint8Array(await i.arrayBuffer())}async function Se(){try{return(await(await(await wt()).getFileHandle(vt)).getFile()).size>0}catch{return!1}}var wn=ArrayBuffer,L=Uint8Array,Ye=Uint16Array,yn=Int16Array,De=Int32Array,ut=function(r,n,i){if(L.prototype.slice)return L.prototype.slice.call(r,n,i);(n==null||n<0)&&(n=0),(i==null||i>r.length)&&(i=r.length);var u=new L(i-n);return u.set(r.subarray(n,i)),u},$e=function(r,n,i,u){if(L.prototype.fill)return L.prototype.fill.call(r,n,i,u);for((i==null||i<0)&&(i=0),(u==null||u>r.length)&&(u=r.length);i<u;++i)r[i]=n;return r},mn=function(r,n,i,u){if(L.prototype.copyWithin)return L.prototype.copyWithin.call(r,n,i,u);for((i==null||i<0)&&(i=0),(u==null||u>r.length)&&(u=r.length);i<u;)r[n++]=r[i++]},bn=["invalid zstd data","window size too large (>2046MB)","invalid block type","FSE accuracy too high","match distance too far back","unexpected EOF"],I=function(r,n,i){var u=new Error(n||bn[r]);if(u.code=r,Error.captureStackTrace&&Error.captureStackTrace(u,I),!i)throw u;return u},Yt=function(r,n,i){for(var u=0,s=0;u<i;++u)s|=r[n++]<<(u<<3);return s},Un=function(r,n){return(r[n]|r[n+1]<<8|r[n+2]<<16|r[n+3]<<24)>>>0},Cn=function(r,n){var i=r[0]|r[1]<<8|r[2]<<16;if(i==3126568&&r[3]==253){var u=r[4],s=u>>5&1,l=u>>2&1,h=u&3,g=u>>6;u&8&&I(0);var y=6-s,E=h==3?4:h,U=Yt(r,y,E);y+=E;var k=g?1<<g:s,F=Yt(r,y,k)+(g==1&&256),m=F;if(!s){var x=1<<10+(r[5]>>3);m=x+(x>>3)*(r[5]&7)}m>2145386496&&I(1);var w=new L((n==1?F||m:n?0:m)+12);return w[0]=1,w[4]=4,w[8]=8,{b:y+k,y:0,l:0,d:U,w:n&&n!=1?n:w.subarray(12),e:m,o:new De(w.buffer,0,3),u:F,c:l,m:Math.min(131072,m)}}else if((i>>4|r[3]<<20)==25481893)return Un(r,4)+8;I(0)},me=function(r){for(var n=0;1<<n<=r;++n);return n-1},Te=function(r,n,i){var u=(n<<3)+4,s=(r[n]&15)+5;s>i&&I(3);for(var l=1<<s,h=l,g=-1,y=-1,E=-1,U=l,k=new wn(512+(l<<2)),F=new yn(k,0,256),m=new Ye(k,0,256),x=new Ye(k,512,l),w=512+(l<<1),$=new L(k,w,l),D=new L(k,w+l);g<255&&h>0;){var X=me(h+1),J=u>>3,se=(1<<X+1)-1,R=(r[J]|r[J+1]<<8|r[J+2]<<16)>>(u&7)&se,b=(1<<X)-1,V=se-h-1,Z=R&b;if(Z<V?(u+=X,R=Z):(u+=X+1,R>b&&(R-=V)),F[++g]=--R,R==-1?(h+=R,$[--U]=g):h-=R,!R)do{var te=u>>3;y=(r[te]|r[te+1]<<8)>>(u&7)&3,u+=2,g+=y}while(y==3)}(g>255||h)&&I(0);for(var Y=0,re=(l>>1)+(l>>3)+3,ne=l-1,G=0;G<=g;++G){var N=F[G];if(N<1){m[G]=-N;continue}for(E=0;E<N;++E){$[Y]=G;do Y=Y+re≠while(Y>=U)}}for(Y&&I(0),E=0;E<l;++E){var C=m[$[E]]++,S=D[E]=s-me(C);x[E]=(C<<S)-l}return[u+7>>3,{b:s,s:$,n:D,t:x}]},Mn=function(r,n){var i=0,u=-1,s=new L(292),l=r[n],h=s.subarray(0,256),g=s.subarray(256,268),y=new Ye(s.buffer,268);if(l<128){var E=Te(r,n+1,6),U=E[0],k=E[1];n+=l;var F=U<<3,m=r[n];m||I(0);for(var x=0,w=0,$=k.b,D=$,X=(++n<<3)-8+me(m);X-=$,!(X<F);){var J=X>>3;if(x+=(r[J]|r[J+1]<<8)>>(X&7)&(1<<$)-1,h[++u]=k.s[x],X-=D,X<F)break;J=X>>3,w+=(r[J]|r[J+1]<<8)>>(X&7)&(1<<D)-1,h[++u]=k.s[w],$=k.n[x],x=k.t[x],D=k.n[w],w=k.t[w]}++u>255&&I(0)}else{for(u=l-127;i<u;i+=2){var se=r[++n];h[i]=se>>4,h[i+1]=se&15}++n}var R=0;for(i=0;i<u;++i){var b=h[i];b>11&&I(0),R+=b&&1<<b-1}var V=me(R)+1,Z=1<<V,te=Z-R;for(te&te-1&&I(0),h[u++]=me(te)+1,i=0;i<u;++i){var b=h[i];++g[h[i]=b&&V+1-b]}var Y=new L(Z<<1),re=Y.subarray(0,Z),ne=Y.subarray(Z);for(y[V]=0,i=V;i>0;--i){var G=y[i];$e(ne,i,G,y[i-1]=G+g[i]*(1<<V-i))}for(y[0]!=Z&&I(0),i=0;i<u;++i){var N=h[i];if(N){var C=y[N];$e(re,i,C,y[N]=C+(1<<V-N))}}return[n,{n:ne,b:V,s:re}]},Qn=Te(new L([81,16,99,140,49,198,24,99,12,33,196,24,99,102,102,134,70,146,4]),0,6)[1],En=Te(new L([33,20,196,24,99,140,33,132,16,66,8,33,132,16,66,8,33,68,68,68,68,68,68,Line truncated
|
||||
<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>
|
||||
`;const i=r.querySelector("#home-db-status"),u=r.querySelector("#btn-go-sync"),s=r.querySelector("#home-actions"),l=r.querySelector("#btn-start");u.addEventListener("click",()=>n.goSync()),l.addEventListener("click",()=>{i.textContent="Экран повторений — следующий шаг."}),(async()=>{if(!await Se()){i.textContent="Локальной базы нет. Скачай сохранение на странице «Синхронизация».",s.hidden=!0;return}try{const g=await vn(),y=gt(g);i.textContent=y?`Локальная база на месте: ${(g.length/1024).toFixed(1)} КБ, сигнатура SQLite OK.`:"Файл в OPFS есть, но это не похоже на SQLite — перекачай сохранение.",s.hidden=!y}catch(g){i.textContent=`Не удалось прочитать базу: ${g instanceof Error?g.message:String(g)}`,s.hidden=!0}})()}const Bt=24,Yn=/^[A-Za-z0-9]{24}$/,At="japlearn_sync_token";function Dt(r){return r.trim().replace(/\s+/g,"")}function ye(r){return Yn.test(r)}function Bn(){try{return localStorage.getItem(At)??""}catch{return""}}function dt(r){try{localStorage.setItem(At,r)}catch{}}const er="https://learning.micialware.ru";function Dn(r,n=er){return`${n.replace(/\/+$/,"")}/download/${encodeURIComponent(r)}`}async function Gn(r,n,i=er){const u=Dn(r,i);let s;try{s=await fetch(u,{method:"GET",...n})}catch(h){throw new Error(`Нет связи с сервером: ${h instanceof Error?h.message:String(h)}`)}if(!s.ok)throw s.status===404?new Error("Сохранение не найдено (404). Проверь токен."):new Error(`Сервер ответил ${s.status} ${s.statusText}`);const l=await s.arrayBuffer();if(l.byteLength===0)throw new Error("Сервер вернул пустой файл");return new Uint8Array(l)}function jn(r){r.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" 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"),u=r.querySelector("#token-counter"),s=r.querySelector("#btn-download"),l=r.querySelector("#btn-upload"),h=r.querySelector("#status");function g(m){h.textContent=m,console.log(`[JapLearn] ${m}`)}function y(){return Dt(n.value)}function E(m){s.disabled=m||!ye(y()),l.disabled=!0,n.disabled=m,m||Se().then(x=>{!n.disabled&&ye(y())&&(l.disabled=!x)})}function U(){const m=y(),x=m.length;u.textContent=`${x} / ${Bt}`;const w=ye(m),$=x===0;n.classList.toggle("invalid",!$&&!w),n.setAttribute("aria-invalid",String(!$&&!w)),$?i.textContent="":x!==Bt?i.textContent=`Нужно 24 символа, сейчас ${x}`:i.textContent=w?"":"Только латинские буквы и цифры",s.disabled=!w,w?Se().then(D=>{n.disabled||(l.disabled=!D)}):l.disabled=!0}async function k(){const m=y();if(ye(m)){dt(m),E(!0),g("Скачивание сохранения...");try{const x=await Gn(m);g(`Скачано ${(x.length/1024).toFixed(1)} КБ, распаковка...`);const w=await Zn(x);g(`Распаковано ${(w.length/1024).toFixed(1)} КБ, сохранение в OPFS...`),await gn(w),g(`Готово: база ${(w.length/1024).toFixed(1)} КБ сохранена локально (OPFS).`)}catch(x){g(`Ошибка скачивания: ${x instanceof Error?x.message:String(x)}`)}finally{E(!1),U()}}}n.addEventListener("input",()=>{U()}),n.addEventListener("change",()=>{const m=y();n.value=m,ye(m)&&(dt(m),g("Токен сохранён локально. Можно скачивать или выгружать.")),U()}),s.addEventListener("click",()=>{k()}),l.addEventListener("click",()=>{const m=y();ye(m)&&(dt(m),g("Выгрузка — следующий шаг (POST /upload/stream/{id})."))});const F=Bn();F&&(n.value=F),U(),ye(Dt(F))&&Se().then(m=>{g(m?"Токен восстановлен, локальная база на месте.":"Токен восстановлен. Нажми «Скачать», чтобы получить базу."),U()})}const Wn={home:"Главная",sync:"Синхронизация"};function _e(r){const n=document.getElementById(r);if(!n)throw new Error(`[JapLearn] Не найден элемент #${r}`);return n}const tr=_e("burger-btn"),Be=_e("drawer"),rr=_e("drawer-overlay"),An=_e("page-title"),Gt=_e("page");let yt=!1;function qe(r){yt=r,Be.classList.toggle("open",r),rr.hidden=!r,tr.setAttribute("aria-expanded",String(r)),Be.setAttribute("aria-hidden",String(!r)),document.body.classList.toggle("drawer-open",r)}function ea(r){Be.querySelectorAll(".drawer-btn").forEach(n=>{n.classList.toggle("active",n.dataset.page===r)})}function mt(r){An.textContent=Wn[r],ea(r),r==="home"?Sn(Gt,{goSync:()=>mt("sync")}):jn(Gt)}tr.addEventListener("click",()=>{qe(!yt)});rr.addEventListener("click",()=>{qe(!1)});document.addEventListener("keydown",r=>{r.key==="Escape"&&yt&&qe(!1)});Be.querySelectorAll(".drawer-btn").forEach(r=>{r.addEventListener("click",()=>{const n=r.dataset.page;(n==="home"||n==="sync")&&mt(n),qe(!1)})});qe(!1);mt("home");console.log("[JapLearn] crossOriginIsolated =",window.crossOriginIsolated);
|
||||
Vendored
+60
@@ -0,0 +1,60 @@
|
||||
(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(){try{return(await(await(await Ct()).getFileHandle(Ut)).getFile()).size>0}catch{return!1}}var qn=ArrayBuffer,z=Uint8Array,je=Uint16Array,Ln=Int16Array,et=Int32Array,ft=function(r,n,i){if(z.prototype.slice)return z.prototype.slice.call(r,n,i);(n==null||n<0)&&(n=0),(i==null||i>r.length)&&(i=r.length);var c=new z(i-n);return c.set(r.subarray(n,i)),c},He=function(r,n,i,c){if(z.prototype.fill)return z.prototype.fill.call(r,n,i,c);for((i==null||i<0)&&(i=0),(c==null||c>r.length)&&(c=r.length);i<c;++i)r[i]=n;return r},Rn=function(r,n,i,c){if(z.prototype.copyWithin)return z.prototype.copyWithin.call(r,n,i,c);for((i==null||i<0)&&(i=0),(c==null||c>r.length)&&(c=r.length);i<c;)r[n++]=r[i++]},In=["invalid zstd data","window size too large (>2046MB)","invalid block type","FSE accuracy too high","match distance too far back","unexpected EOF"],Y=function(r,n,i){var c=new Error(n||In[r]);if(c.code=r,Error.caLine truncated
|
||||
<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(/\/+$/,"")}/download/${encodeURIComponent(r)}`}async function ha(r,n,i=ur){const c=fa(r,i);let o;try{o=await fetch(c,{method:"GET",...n})}catch(h){throw new Error(`Нет связи с сервером: ${h instanceof Error?h.message:String(h)}`)}if(!o.ok)throw o.status===404?new Error("Сохранение не найдено (404). Проверь токен."):new Error(`Сервер ответил ${o.status} ${o.statusText}`);const l=await o.arrayBuffer();if(l.byteLength===0)throw new Error("Сервер вернул пустой файл");return new Uint8Array(l)}function ga(r){r.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" 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);
|
||||
@@ -1 +1 @@
|
||||
:root{font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;line-height:1.5;color:#1e1b2e;background:#f4f1fa}*{box-sizing:border-box}body{margin:0;min-height:100dvh;display:flex;justify-content:center;padding:env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left)}#app{width:100%;max-width:480px;padding:24px 16px 32px;display:flex;flex-direction:column;gap:16px}.app-header h1{margin:0;font-size:1.75rem;letter-spacing:.02em}.app-subtitle{margin:4px 0 0;color:#6d6390}.top-bar{display:flex;align-items:center;gap:12px}.burger-btn{width:44px;height:44px;border:2px solid #d9d2f0;border-radius:12px;background:#fff;cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:5px;touch-action:manipulation;flex:none}.burger-btn span{display:block;width:20px;height:2px;border-radius:2px;background:#4c446b}.burger-btn[aria-expanded=true]{border-color:#7c3aed}.top-bar-title h1{margin:0;font-size:1.4rem;line-height:1.2}.top-bar-title p{margin:0;font-size:.9rem;color:#6d6390}.top-bar-spacer{flex:1}.app-content{display:flex;flex-direction:column;gap:16px}.drawer-overlay{position:fixed;inset:0;background:#1e1b2e73;z-index:40}.drawer-overlay[hidden]{display:none}.drawer{position:fixed;top:0;left:0;bottom:0;width:min(300px,85vw);background:#fff;z-index:50;transform:translate(-105%);transition:transform .22s ease;display:flex;flex-direction:column;gap:8px;padding:20px 14px;box-shadow:8px 0 32px #1e1b2e2e}.drawer.open{transform:translate(0)}.drawer-header{padding:4px 8px 14px;border-bottom:1px solid #eee9fb;margin-bottom:8px}.drawer-logo{font-size:1.3rem;font-weight:800;color:#7c3aed}.drawer-subtitle{font-size:.9rem;color:#6d6390}.drawer-btn{border:none;background:transparent;text-align:left;font-size:1.05rem;font-weight:600;color:#1e1b2e;padding:13px 12px;border-radius:12px;cursor:pointer;touch-action:manipulation}.drawer-btn.active{background:#f1eafe;color:#6d28d9}.drawer-btn:active{background:#e9defc}body.drawer-open{overflow:hidden}.card{background:#fff;border-radius:16px;padding:20px 16px;box-shadow:0 8px 24px #7c3aed1f;display:flex;flex-direction:column;gap:12px}.card-title{margin:0;font-size:1.25rem}.card-hint{margin:0;color:#6d6390;font-size:.95rem}.field-label{font-weight:600;font-size:.95rem}.token-input{width:100%;padding:14px 12px;font-size:1.05rem;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:.04em;border:2px solid #d9d2f0;border-radius:12px;outline:none}.token-input:focus{border-color:#7c3aed}.token-input.invalid{border-color:#dc2626;background:#fef2f2}.token-meta{display:flex;justify-content:space-between;align-items:baseline;gap:8px;min-height:1.4em}.token-error{color:#dc2626;font-size:.9rem}.token-counter{color:#6d6390;font-variant-numeric:tabular-nums;white-space:nowrap}.btn-row{display:flex;gap:12px}.btn{flex:1;padding:14px 12px;font-size:1rem;font-weight:600;border-radius:12px;border:2px solid #7c3aed;background:#fff;color:#7c3aed;cursor:pointer;touch-action:manipulation}.btn-primary{background:#7c3aed;color:#fff}.btn:disabled{opacity:.4;cursor:not-allowed}.btn:not(:disabled):active{transform:scale(.98)}.status-bar{margin:0;padding:10px 12px;border-radius:10px;background:#f4f1fa;color:#4c446b;font-size:.95rem;word-break:break-word}
|
||||
:root{font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;line-height:1.5;color:#1e1b2e;background:#f4f1fa}*{box-sizing:border-box}body{margin:0;min-height:100dvh;display:flex;justify-content:center;padding:env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left)}#app{width:100%;max-width:480px;padding:24px 16px 32px;display:flex;flex-direction:column;gap:16px}.app-header h1{margin:0;font-size:1.75rem;letter-spacing:.02em}.app-subtitle{margin:4px 0 0;color:#6d6390}.top-bar{display:flex;align-items:center;gap:12px}.burger-btn{width:44px;height:44px;border:2px solid #d9d2f0;border-radius:12px;background:#fff;cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:5px;touch-action:manipulation;flex:none}.burger-btn span{display:block;width:20px;height:2px;border-radius:2px;background:#4c446b}.burger-btn[aria-expanded=true]{border-color:#7c3aed}.top-bar-title h1{margin:0;font-size:1.4rem;line-height:1.2}.top-bar-title p{margin:0;font-size:.9rem;color:#6d6390}.top-bar-spacer{flex:1}.app-content{display:flex;flex-direction:column;gap:16px}.drawer-overlay{position:fixed;inset:0;background:#1e1b2e73;z-index:40}.drawer-overlay[hidden]{display:none}.drawer{position:fixed;top:0;left:0;bottom:0;width:min(300px,85vw);background:#fff;z-index:50;transform:translate(-105%);transition:transform .22s ease;display:flex;flex-direction:column;gap:8px;padding:20px 14px;box-shadow:8px 0 32px #1e1b2e2e}.drawer.open{transform:translate(0)}.drawer-header{padding:4px 8px 14px;border-bottom:1px solid #eee9fb;margin-bottom:8px}.drawer-logo{font-size:1.3rem;font-weight:800;color:#7c3aed}.drawer-subtitle{font-size:.9rem;color:#6d6390}.drawer-btn{border:none;background:transparent;text-align:left;font-size:1.05rem;font-weight:600;color:#1e1b2e;padding:13px 12px;border-radius:12px;cursor:pointer;touch-action:manipulation}.drawer-btn.active{background:#f1eafe;color:#6d28d9}.drawer-btn:active{background:#e9defc}body.drawer-open{overflow:hidden}.card{background:#fff;border-radius:16px;padding:20px 16px;box-shadow:0 8px 24px #7c3aed1f;display:flex;flex-direction:column;gap:12px}.card-title{margin:0;font-size:1.25rem}.card-hint{margin:0;color:#6d6390;font-size:.95rem}.field-label{font-weight:600;font-size:.95rem}.token-input{width:100%;padding:14px 12px;font-size:1.05rem;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:.04em;border:2px solid #d9d2f0;border-radius:12px;outline:none}.token-input:focus{border-color:#7c3aed}.token-input.invalid{border-color:#dc2626;background:#fef2f2}.token-meta{display:flex;justify-content:space-between;align-items:baseline;gap:8px;min-height:1.4em}.token-error{color:#dc2626;font-size:.9rem}.token-counter{color:#6d6390;font-variant-numeric:tabular-nums;white-space:nowrap}.btn-row{display:flex;gap:12px}.btn{flex:1;padding:14px 12px;font-size:1rem;font-weight:600;border-radius:12px;border:2px solid #7c3aed;background:#fff;color:#7c3aed;cursor:pointer;touch-action:manipulation}.btn-primary{background:#7c3aed;color:#fff}.btn:disabled{opacity:.4;cursor:not-allowed}.btn:not(:disabled):active{transform:scale(.98)}.status-bar{margin:0;padding:10px 12px;border-radius:10px;background:#f4f1fa;color:#4c446b;font-size:.95rem;word-break:break-word}.status-bar[hidden]{display:none}.words-toolbar{display:flex;gap:8px}.words-search{flex:1;min-width:0;padding:12px;font-size:1rem;border:2px solid #d9d2f0;border-radius:12px;outline:none;background:#fff;color:inherit}.words-search:focus{border-color:#7c3aed}.words-group{flex:none;max-width:42%;padding:12px 8px;font-size:.95rem;font-weight:600;color:#4c446b;border:2px solid #d9d2f0;border-radius:12px;background:#fff;outline:none}.words-group:focus{border-color:#7c3aed}.words-list{display:flex;flex-direction:column;gap:10px}.word-card{border:1px solid #eee9fb;border-radius:12px;padding:12px;background:#fbfaff;display:flex;flex-direction:column;gap:8px}.word-card-top{display:flex;align-items:flex-start;justify-content:space-between;gap:8px}.word-key{font-size:1.2rem;font-weight:700;line-height:1.3;word-break:break-word}.word-value{font-size:1rem;color:#4c446b;word-break:break-word}.word-group{flex:none;font-size:.75rem;font-weight:700;color:#6d28d9;background:#f1eafe;border-radius:999px;padding:4px 10px;white-space:nowrap}.tag-row{display:flex;flex-wrap:wrap;gap:6px}.tag-chip{font-size:.8rem;font-weight:600;color:#5b21b6;background:#ede9fe;border-radius:999px;padding:3px 10px;word-break:break-word}.word-more{font-size:.9rem;color:#4c446b}.word-more summary{cursor:pointer;font-weight:600;color:#6d28d9;touch-action:manipulation}.word-more-list{margin:8px 0 0;display:flex;flex-direction:column;gap:6px}.word-more-row{background:#fff;border:1px solid #eee9fb;border-radius:8px;padding:8px 10px}.word-more-row dt{font-size:.8rem;font-weight:700;color:#6d6390}.word-more-row dd{margin:2px 0 0;word-break:break-word}.word-more-id{font-weight:400;opacity:.7}
|
||||
Vendored
BIN
Binary file not shown.
@@ -0,0 +1 @@
|
||||
const m=(w,...t)=>postMessage({type:w,payload:t}),Q=function(){const w=function(...e){throw new Error(e.join(" "))};globalThis.window===globalThis?w("This code cannot run from the main thread.","Load it as a Worker from a separate Worker."):navigator?.storage?.getDirectory||w("This API requires navigator.storage.getDirectory.");const t=Object.create(null);t.verbose=1;const v={0:console.error.bind(console),1:console.warn.bind(console),2:console.log.bind(console)},A=(e,...s)=>{t.verbose>e&&v[e]("OPFS asyncer:",...s)},g=(...e)=>A(2,...e),S=(...e)=>A(1,...e),h=(...e)=>A(0,...e),E=Object.create(null),I=new Set,k=function(e,s){const n=new URL(e,"file://irrelevant").pathname;return s?n.split("/").filter(o=>!!o):n},x=async function(s,n=!1){const o=k(s,!0),a=o.pop();let i=t.rootDir;for(const r of o)r&&(i=await i.getDirectoryHandle(r,{create:!!n}));return[i,a]},R=async e=>{if(e.syncHandle){g("Closing sync handle for",e.filenameAbs);const s=e.syncHandle;return delete e.syncHandle,delete e.xLock,I.delete(e.fid),s.close()}},F=async e=>{try{await R(e)}catch(s){S("closeSyncHandleNoThrow() ignoring:",s,e)}},D=async()=>{if(I.size)for(const e of I){const s=E[e];await F(s),g("Auto-unlocked",e,s.filenameAbs)}},T=async e=>{if(e.releaseImplicitLocks&&I.has(e.fid))return F(e)};class b extends Error{constructor(s,...n){super([...n,": "+s.name+":",s.message].join(" "),{cause:s}),this.name="GetSyncHandleError"}}b.convertRc=(e,s)=>{if(e instanceof b){if(e.cause.name==="NoModificationAllowedError"||e.cause.name==="DOMException"&&e.cause.message.indexOf("Access Handles cannot")===0)return t.sq3Codes.SQLITE_BUSY;if(e.cause.name==="NotFoundError")return t.sq3Codes.SQLITE_CANTOPEN}else if(e?.name==="NotFoundError")return t.sq3Codes.SQLITE_CANTOPEN;return s};const O=async(e,s)=>{if(!e.syncHandle){const n=performance.now();g("Acquiring sync handle for",e.filenameAbs);const o=6,a=t.asyncIdleWaitTime*2;let i=1,r=a;for(;;r=a*++i)try{e.syncHandle=await e.fileHandle.createSyncAccessHandle();break}catch(c){if(i===o)throw new b(c,"Error getting sync handle for",s+"().",o,"attempts failed.",e.filenameAbs);S("Error getting sync handle for",s+"(). Waiting",r,"ms and trying again.",e.filenameAbs,c),Atomics.wait(t.sabOPView,t.opIds.retry,0,r)}g("Got",s+"() sync handle for",e.filenameAbs,"in",performance.now()-n,"ms"),e.xLock||(I.add(e.fid),g("Acquired implicit lock for",s+"()",e.fid,e.filenameAbs))}return e.syncHandle},d=(e,s)=>{g(e+"() => notify(",s,")"),Atomics.store(t.sabOPView,t.opIds.rc,s),Atomics.notify(t.sabOPView,t.opIds.rc)},H=function(e,s){s.readOnly&&w(e+"(): File is read-only: "+s.filenameAbs)};let L=!1;const _={"opfs-async-shutdown":async()=>{L=!0,d("opfs-async-shutdown",0)},mkdir:async e=>{let s=0;try{await x(e+"/filepart",!0)}catch(n){t.s11n.storeException(2,n),s=t.sq3Codes.SQLITE_IOERR}d("mkdir",s)},xAccess:async e=>{let s=0;try{const[n,o]=await x(e);await n.getFileHandle(o)}catch(n){t.s11n.storeException(2,n),s=t.sq3Codes.SQLITE_IOERR}d("xAccess",s)},xClose:async function(e){const s="xClose";I.delete(e);const n=E[e];let o=0;if(n){if(delete E[e],await R(n),n.deleteOnClose)try{await n.dirHandle.removeEntry(n.filenamePart)}catch(a){S("Ignoring dirHandle.removeEntry() failure of",n,a)}}else t.s11n.serialize(),o=t.sq3Codes.SQLITE_NOTFOUND;d(s,o)},xDelete:async function(...e){const s=await _.xDeleteNoWait(...e);d("xDelete",s)},xDeleteNoWait:async function(e,s=0,n=!1){let o=0;try{for(;e;){const[a,i]=await x(e,!1);if(!i||(await a.removeEntry(i,{recursive:n}),s!==4660))break;n=!1,e=k(e,!0),e.pop(),e=e.join("/")}}catch(a){t.s11n.storeException(2,a),o=t.sq3Codes.SQLITE_IOERR_DELETE}return o},xFileSize:async function(e){const s=E[e];let n=0;try{const o=await(await O(s,"xFileSize")).getSize();t.s11n.serialize(Number(o))}catch(o){t.s11n.storeException(1,o),n=b.convertRc(o,t.sq3Codes.SQLITE_IOERR)}await T(s),d("xFileSize",n)},xLock:async function(e,s){const n=E[e];let o=0;const a=n.xLock;if(n.xLock=s,!n.syncHandle)try{await O(n,"xLock"),I.delete(e)}catch(i){t.s11n.storeException(1,i),o=b.convertRc(i,t.sq3Codes.SQLITE_IOERR_LOCK),n.xLock=a}d("xLock",o)},xOpen:async function(e,s,n,o){const a="xOpen",i=t.sq3Codes.SQLITE_OPEN_CREATE&n;try{let r,c;try{[r,c]=await x(s,!!i)}catch(f){t.s11n.storeException(1,f),d(a,t.sq3Codes.SQLITE_NOTFOUND);return}if(t.opfsFlags.OPFS_UNLINK_BEFORE_OPEN&o)try{await r.removeEntry(c)}catch{}const y=await r.getFileHandle(c,{create:i}),l=Object.assign(Object.create(null),{fid:e,filenameAbs:s,filenamePart:c,dirHandle:r,fileHandle:y,sabView:t.sabFileBufView,readOnly:!i&&!!(t.sq3Codes.SQLITE_OPEN_READONLY&n),deleteOnClose:!!(t.sq3Codes.SQLITE_OPEN_DELETEONCLOSE&n)});l.releaseImplicitLocks=o&t.opfsFlags.OPFS_UNLOCK_ASAP||t.opfsFlags.defaultUnlockAsap,E[e]=l,d(a,0)}catch(r){h(a,r),t.s11n.storeException(1,r),d(a,t.sq3Codes.SQLITE_IOERR)}},xRead:async function(e,s,n){let o=0,a;const i=E[e];try{a=(await O(i,"xRead")).read(i.sabView.subarray(0,s),{at:Number(n)}),a<s&&(i.sabView.fill(0,a,s),o=t.sq3Codes.SQLITE_IOERR_SHORT_READ)}catch(r){Line truncated
|
||||
@@ -0,0 +1,3 @@
|
||||
var ot=(()=>{var Ie=import.meta.url;return function(qe={}){var be,i=qe,ve,Ee,$e=new Promise((e,t)=>{ve=e,Ee=t}),lt=typeof window=="object",Ue=typeof importScripts=="function";typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string"&&process.type!="renderer";const Tt=globalThis.sqlite3InitModuleState||Object.assign(Object.create(null),{debugModule:()=>{}});delete globalThis.sqlite3InitModuleState,Tt.debugModule("globalThis.location =",globalThis.location);var at=Object.assign({},i),ct="./this.program",ye="";function Ft(e){return i.locateFile?i.locateFile(e,ye):ye+e}var Ke,ze;(lt||Ue)&&(Ue?ye=self.location.href:typeof document<"u"&&document.currentScript&&(ye=document.currentScript.src),Ie&&(ye=Ie),ye.startsWith("blob:")?ye="":ye=ye.substr(0,ye.replace(/[?#].*/,"").lastIndexOf("/")+1),Ue&&(ze=e=>{var t=new XMLHttpRequest;return t.open("GET",e,!1),t.responseType="arraybuffer",t.send(null),new Uint8Array(t.response)}),Ke=e=>fetch(e,{credentials:"same-origin"}).then(t=>t.ok?t.arrayBuffer():Promise.reject(new Error(t.status+" : "+t.url))));var Je=i.print||console.log.bind(console),ke=i.printErr||console.error.bind(console);Object.assign(i,at),at=null,i.arguments&&i.arguments,i.thisProgram&&(ct=i.thisProgram);var Qe=i.wasmBinary,Te,_t=!1,ge,Pe,Ne,ce,he,xe;function ut(){var e=Te.buffer;i.HEAP8=ge=new Int8Array(e),i.HEAP16=Ne=new Int16Array(e),i.HEAPU8=Pe=new Uint8Array(e),i.HEAPU16=new Uint16Array(e),i.HEAP32=ce=new Int32Array(e),i.HEAPU32=he=new Uint32Array(e),i.HEAPF32=new Float32Array(e),i.HEAPF64=new Float64Array(e),i.HEAP64=xe=new BigInt64Array(e),i.HEAPU64=new BigUint64Array(e)}if(i.wasmMemory)Te=i.wasmMemory;else{var Ot=i.INITIAL_MEMORY||16777216;Te=new WebAssembly.Memory({initial:Ot/65536,maximum:32768})}ut();var ft=[],dt=[],pt=[];function Pt(){var e=i.preRun;e&&(typeof e=="function"&&(e=[e]),e.forEach(Dt)),et(ft)}function Lt(){!i.noFSInit&&!a.initialized&&a.init(),a.ignorePermissions=!1,et(dt)}function Ct(){var e=i.postRun;e&&(typeof e=="function"&&(e=[e]),e.forEach(Nt)),et(pt)}function Dt(e){ft.unshift(e)}function Rt(e){dt.unshift(e)}function Nt(e){pt.unshift(e)}var Fe=0,Me=null;function Zn(e){return e}function Xe(e){Fe++,i.monitorRunDependencies?.(Fe)}function We(e){if(Fe--,i.monitorRunDependencies?.(Fe),Fe==0&&Me){var t=Me;Me=null,t()}}function Ye(e){i.onAbort?.(e),e="Aborted("+e+")",ke(e),_t=!0,e+=". Build with -sASSERTIONS for more info.";var t=new WebAssembly.RuntimeError(e);throw Ee(t),t}var Mt="data:application/octet-stream;base64,",ht=e=>e.startsWith(Mt);function jt(){if(i.locateFile){var e="sqlite3.wasm";return ht(e)?e:Ft(e)}return new URL("/assets/sqlite3-DBpDb1lf.wasm",import.meta.url).href}var Ze;function mt(e){if(e==Ze&&Qe)return new Uint8Array(Qe);if(ze)return ze(e);throw"both async and sync fetching of the wasm failed"}function Bt(e){return Qe?Promise.resolve().then(()=>mt(e)):Ke(e).then(t=>new Uint8Array(t),()=>mt(e))}function gt(e,t,n){return Bt(e).then(r=>WebAssembly.instantiate(r,t)).then(n,r=>{ke(`failed to asynchronously prepare wasm: ${r}`),Ye(r)})}function Ut(e,t,n,r){return!e&&typeof WebAssembly.instantiateStreaming=="function"&&!ht(t)&&typeof fetch=="function"?fetch(t,{credentials:"same-origin"}).then(s=>{var o=WebAssembly.instantiateStreaming(s,n);return o.then(r,function(p){return ke(`wasm streaming compile failed: ${p}`),ke("falling back to ArrayBuffer instantiation"),gt(t,n,r)})}):gt(t,n,r)}function zt(){return{env:xt,wasi_snapshot_preview1:xt}}function Qt(){var e=zt();function t(r,s){return h=r.exports,Rt(h.__wasm_call_ctors),We(),h}Xe();function n(r){t(r.instance)}if(i.instantiateWasm)try{return i.instantiateWasm(e,t)}catch(r){ke(`Module.instantiateWasm callback failed with error: ${r}`),Ee(r)}return Ze??=jt(),Ut(Qe,Ze,e,n).catch(Ee),{}}var et=e=>{e.forEach(t=>t(i))};i.noExitRuntime;var _e={isAbs:e=>e.charAt(0)==="/",splitPath:e=>{var t=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return t.exec(e).slice(1)},normalizeArray:(e,t)=>{for(var n=0,r=e.length-1;r>=0;r--){var s=e[r];s==="."?e.splice(r,1):s===".."?(e.splice(r,1),n++):n&&(e.splice(r,1),n--)}if(t)for(;n;n--)e.unshift("..");return e},normalize:e=>{var t=_e.isAbs(e),n=e.substr(-1)==="/";return e=_e.normalizeArray(e.split("/").filter(r=>!!r),!t).join("/"),!e&&!t&&(e="."),e&&n&&(e+="/"),(t?"/":"")+e},dirname:e=>{var t=_e.splitPath(e),n=t[0],r=t[1];return!n&&!r?".":(r&&(r=r.substr(0,r.length-1)),n+r)},basename:e=>{if(e==="/")return"/";e=_e.normalize(e),e=e.replace(/\/$/,"");var t=e.lastIndexOf("/");return t===-1?e:e.substr(t+1)},join:(...e)=>_e.normalize(e.join("/")),join2:(e,t)=>_e.normalize(e+"/"+t)},Wt=()=>{if(typeof crypto=="object"&&typeof crypto.getRandomValues=="function")return e=>crypto.getRandomValues(e);Ye("initRandomDevice")},qt=e=>(qt=Wt())(e),Se={resolve:(...e)=>{for(var t="",n=!1,r=e.length-1;r>=-1&&!n;r--){var s=r>=0?e[r]:a.cwd();if(typeof s!="string")throw new TypeError("Arguments to path.resolve must be strings");if(!s)return"";t=s+"/"+t,n=_e.iLine truncated
|
||||
`)),!e)return null;tt=yt(e)}return tt.shift()},Oe={ttys:[],init(){},shutdown(){},register(e,t){Oe.ttys[e]={input:[],output:[],ops:t},a.registerDevice(e,Oe.stream_ops)},stream_ops:{open(e){var t=Oe.ttys[e.node.rdev];if(!t)throw new a.ErrnoError(43);e.tty=t,e.seekable=!1},close(e){e.tty.ops.fsync(e.tty)},fsync(e){e.tty.ops.fsync(e.tty)},read(e,t,n,r,s){if(!e.tty||!e.tty.ops.get_char)throw new a.ErrnoError(60);for(var o=0,p=0;p<r;p++){var g;try{g=e.tty.ops.get_char(e.tty)}catch{throw new a.ErrnoError(29)}if(g===void 0&&o===0)throw new a.ErrnoError(6);if(g==null)break;o++,t[n+p]=g}return o&&(e.node.timestamp=Date.now()),o},write(e,t,n,r,s){if(!e.tty||!e.tty.ops.put_char)throw new a.ErrnoError(60);try{for(var o=0;o<r;o++)e.tty.ops.put_char(e.tty,t[n+o])}catch{throw new a.ErrnoError(29)}return r&&(e.node.timestamp=Date.now()),o}},default_tty_ops:{get_char(e){return Ht()},put_char(e,t){t===null||t===10?(Je(Le(e.output)),e.output=[]):t!=0&&e.output.push(t)},fsync(e){e.output&&e.output.length>0&&(Je(Le(e.output)),e.output=[])},ioctl_tcgets(e){return{c_iflag:25856,c_oflag:5,c_cflag:191,c_lflag:35387,c_cc:[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},ioctl_tcsets(e,t,n){return 0},ioctl_tiocgwinsz(e){return[24,80]}},default_tty1_ops:{put_char(e,t){t===null||t===10?(ke(Le(e.output)),e.output=[]):t!=0&&e.output.push(t)},fsync(e){e.output&&e.output.length>0&&(ke(Le(e.output)),e.output=[])}}},Vt=(e,t)=>{Pe.fill(0,e,e+t)},wt=(e,t)=>Math.ceil(e/t)*t,vt=e=>{e=wt(e,65536);var t=St(65536,e);return t&&Vt(t,e),t},ie={ops_table:null,mount(e){return ie.createNode(null,"/",16895,0)},createNode(e,t,n,r){if(a.isBlkdev(n)||a.isFIFO(n))throw new a.ErrnoError(63);ie.ops_table||={dir:{node:{getattr:ie.node_ops.getattr,setattr:ie.node_ops.setattr,lookup:ie.node_ops.lookup,mknod:ie.node_ops.mknod,rename:ie.node_ops.rename,unlink:ie.node_ops.unlink,rmdir:ie.node_ops.rmdir,readdir:ie.node_ops.readdir,symlink:ie.node_ops.symlink},stream:{llseek:ie.stream_ops.llseek}},file:{node:{getattr:ie.node_ops.getattr,setattr:ie.node_ops.setattr},stream:{llseek:ie.stream_ops.llseek,read:ie.stream_ops.read,write:ie.stream_ops.write,allocate:ie.stream_ops.allocate,mmap:ie.stream_ops.mmap,msync:ie.stream_ops.msync}},link:{node:{getattr:ie.node_ops.getattr,setattr:ie.node_ops.setattr,readlink:ie.node_ops.readlink},stream:{}},chrdev:{node:{getattr:ie.node_ops.getattr,setattr:ie.node_ops.setattr},stream:a.chrdev_stream_ops}};var s=a.createNode(e,t,n,r);return a.isDir(s.mode)?(s.node_ops=ie.ops_table.dir.node,s.stream_ops=ie.ops_table.dir.stream,s.contents={}):a.isFile(s.mode)?(s.node_ops=ie.ops_table.file.node,s.stream_ops=ie.ops_table.file.stream,s.usedBytes=0,s.contents=null):a.isLink(s.mode)?(s.node_ops=ie.ops_table.link.node,s.stream_ops=ie.ops_table.link.stream):a.isChrdev(s.mode)&&(s.node_ops=ie.ops_table.chrdev.node,s.stream_ops=ie.ops_table.chrdev.stream),s.timestamp=Date.now(),e&&(e.contents[t]=s,e.timestamp=s.timestamp),s},getFileDataAsTypedArray(e){return e.contents?e.contents.subarray?e.contents.subarray(0,e.usedBytes):new Uint8Array(e.contents):new Uint8Array(0)},expandFileStorage(e,t){var n=e.contents?e.contents.length:0;if(!(n>=t)){var r=1024*1024;t=Math.max(t,n*(n<r?2:1.125)>>>0),n!=0&&(t=Math.max(t,256));var s=e.contents;e.contents=new Uint8Array(t),e.usedBytes>0&&e.contents.set(s.subarray(0,e.usedBytes),0)}},resizeFileStorage(e,t){if(e.usedBytes!=t)if(t==0)e.contents=null,e.usedBytes=0;else{var n=e.contents;e.contents=new Uint8Array(t),n&&e.contents.set(n.subarray(0,Math.min(t,e.usedBytes))),e.usedBytes=t}},node_ops:{getattr(e){var t={};return t.dev=a.isChrdev(e.mode)?e.id:1,t.ino=e.id,t.mode=e.mode,t.nlink=1,t.uid=0,t.gid=0,t.rdev=e.rdev,a.isDir(e.mode)?t.size=4096:a.isFile(e.mode)?t.size=e.usedBytes:a.isLink(e.mode)?t.size=e.link.length:t.size=0,t.atime=new Date(e.timestamp),t.mtime=new Date(e.timestamp),t.ctime=new Date(e.timestamp),t.blksize=4096,t.blocks=Math.ceil(t.size/t.blksize),t},setattr(e,t){t.mode!==void 0&&(e.mode=t.mode),t.timestamp!==void 0&&(e.timestamp=t.timestamp),t.size!==void 0&&ie.resizeFileStorage(e,t.size)},lookup(e,t){throw a.genericErrors[44]},mknod(e,t,n,r){return ie.createNode(e,t,n,r)},rename(e,t,n){if(a.isDir(e.mode)){var r;try{r=a.lookupNode(t,n)}catch{}if(r)for(var s in r.contents)throw new a.ErrnoError(55)}delete e.parent.contents[e.name],e.parent.timestamp=Date.now(),e.name=n,t.contents[n]=e,t.timestamp=e.parent.timestamp},unlink(e,t){delete e.contents[t],e.timestamp=Date.now()},rmdir(e,t){var n=a.lookupNode(e,t);for(var r in n.contents)throw new a.ErrnoError(55);delete e.contents[t],e.timestamp=Date.now()},readdir(e){var t=[".",".."];for(var n of Object.keys(e.contents))t.push(n);return t},symlink(e,t,n){var r=ie.createNode(e,t,41471,0);return r.link=n,r},readlink(e){if(!a.isLink(e.mode))throw new a.ErrnoError(28);return e.link}},stream_ops:{read(e,t,n,r,s){var o=e.node.contents;if(s>=e.node.usedBytes)return 0;var p=Math.min(e.node.usedBytes-s,r);if(p>8&&o.subarray)t.set(o.sub
Line truncated
|
||||
Total of`,C,"op(s) for",M,"ms (incl. "+J+" ms of waiting on the async side)"),t.config.log("Serialization metrics:",I.s11n),m.postMessage({type:"opfs-async-metrics"})},reset:function(){let E;const C=J=>J.count=J.time=J.wait=0;for(E in u.opIds)C(I[E]=Object.create(null));let M=I.s11n=Object.create(null);M=M.serialize=Object.create(null),M.count=M.time=0,M=I.s11n.deserialize=Object.create(null),M.count=M.time=0}};const c=new S,d=new N().addOnDispose(()=>c.dispose());let y;const B=E=>(y=!0,d.dispose(),O(E)),f=()=>(y=!1,g(t)),m=new Worker(new URL("/assets/sqlite3-opfs-async-proxy-B_ImRJXp.js",import.meta.url));setTimeout(()=>{y===void 0&&B(new Error("Timeout while waiting for OPFS async proxy worker."))},4e3),m._originalOnError=m.onerror,m.onerror=function(E){A("Error initializing OPFS asyncer:",E),B(new Error("Loading OPFS async Worker failed for unknown reasons."))};const l=q.sqlite3_vfs_find(null),_=l?new N(l):null;c.$iVersion=1,d.$iVersion=2,d.$szOsFile=q.sqlite3_file.structInfo.sizeof,d.$mxPathname=1024,d.$zName=x.allocCString("opfs"),d.$xDlOpen=d.$xDlError=d.$xDlSym=d.$xDlClose=null,d.addOnDispose("$zName",d.$zName,"cleanup default VFS wrapper",()=>_?_.dispose():null);const u=Object.create(null);u.verbose=s.verbose,u.littleEndian=(()=>{const E=new ArrayBuffer(2);return new DataView(E).setInt16(0,256,!0),new Int16Array(E)[0]===256})(),u.asyncIdleWaitTime=150,u.asyncS11nExceptions=1,u.fileBufferSize=1024*64,u.sabS11nOffset=u.fileBufferSize,u.sabS11nSize=d.$mxPathname*2,u.sabIO=new SharedArrayBuffer(u.fileBufferSize+u.sabS11nSize),u.opIds=Object.create(null);const I=Object.create(null);{let E=0;u.opIds.whichOp=E++,u.opIds.rc=E++,u.opIds.xAccess=E++,u.opIds.xClose=E++,u.opIds.xDelete=E++,u.opIds.xDeleteNoWait=E++,u.opIds.xFileSize=E++,u.opIds.xLock=E++,u.opIds.xOpen=E++,u.opIds.xRead=E++,u.opIds.xSleep=E++,u.opIds.xSync=E++,u.opIds.xTruncate=E++,u.opIds.xUnlock=E++,u.opIds.xWrite=E++,u.opIds.mkdir=E++,u.opIds["opfs-async-metrics"]=E++,u.opIds["opfs-async-shutdown"]=E++,u.opIds.retry=E++,u.sabOP=new SharedArrayBuffer(E*4),L.metrics.reset()}u.sq3Codes=Object.create(null),["SQLITE_ACCESS_EXISTS","SQLITE_ACCESS_READWRITE","SQLITE_BUSY","SQLITE_CANTOPEN","SQLITE_ERROR","SQLITE_IOERR","SQLITE_IOERR_ACCESS","SQLITE_IOERR_CLOSE","SQLITE_IOERR_DELETE","SQLITE_IOERR_FSYNC","SQLITE_IOERR_LOCK","SQLITE_IOERR_READ","SQLITE_IOERR_SHORT_READ","SQLITE_IOERR_TRUNCATE","SQLITE_IOERR_UNLOCK","SQLITE_IOERR_WRITE","SQLITE_LOCK_EXCLUSIVE","SQLITE_LOCK_NONE","SQLITE_LOCK_PENDING","SQLITE_LOCK_RESERVED","SQLITE_LOCK_SHARED","SQLITE_LOCKED","SQLITE_MISUSE","SQLITE_NOTFOUND","SQLITE_OPEN_CREATE","SQLITE_OPEN_DELETEONCLOSE","SQLITE_OPEN_MAIN_DB","SQLITE_OPEN_READONLY"].forEach(E=>{(u.sq3Codes[E]=q[E])===void 0&&D("Maintenance required: not found:",E)}),u.opfsFlags=Object.assign(Object.create(null),{OPFS_UNLOCK_ASAP:1,OPFS_UNLINK_BEFORE_OPEN:2,defaultUnlockAsap:!1});const U=(E,...C)=>{const M=u.opIds[E]||D("Invalid op ID:",E);u.s11n.serialize(...C),Atomics.store(u.sabOPView,u.opIds.rc,-1),Atomics.store(u.sabOPView,u.opIds.whichOp,M),Atomics.notify(u.sabOPView,u.opIds.whichOp);const J=performance.now();for(;Atomics.wait(u.sabOPView,u.opIds.rc,-1)!=="not-equal";);const G=Atomics.load(u.sabOPView,u.opIds.rc);if(I[E].wait+=performance.now()-J,G&&u.asyncS11nExceptions){const te=u.s11n.deserialize();te&&A(E+"() async error:",...te)}return G};L.debug={asyncShutdown:()=>{T("Shutting down OPFS async listener. The OPFS VFS will no longer work."),U("opfs-async-shutdown")},asyncRestart:()=>{T("Attempting to restart OPFS VFS async listener. Might work, might not."),m.postMessage({type:"opfs-async-restart"})}};const ee=()=>{if(u.s11n)return u.s11n;const E=new TextDecoder,C=new TextEncoder("utf-8"),M=new Uint8Array(u.sabIO,u.sabS11nOffset,u.sabS11nSize),J=new DataView(u.sabIO,u.sabS11nOffset,u.sabS11nSize);u.s11n=Object.create(null);const G=Object.create(null);G.number={id:1,size:8,getter:"getFloat64",setter:"setFloat64"},G.bigint={id:2,size:8,getter:"getBigInt64",setter:"setBigInt64"},G.boolean={id:3,size:4,getter:"getInt32",setter:"setInt32"},G.string={id:4};const te=Y=>G[typeof Y]||D("Maintenance required: this value type cannot be serialized.",Y),oe=Y=>{switch(Y){case G.number.id:return G.number;case G.bigint.id:return G.bigint;case G.boolean.id:return G.boolean;case G.string.id:return G.string;default:D("Invalid type ID:",Y)}};return u.s11n.deserialize=function(Y=!1){++I.s11n.deserialize.count;const pe=performance.now(),F=M[0],V=F?[]:null;if(F){const K=[];let X=1,ae,de,ue;for(ae=0;ae<F;++ae,++X)K.push(oe(M[X]));for(ae=0;ae<F;++ae){const fe=K[ae];fe.getter?(ue=J[fe.getter](X,u.littleEndian),X+=fe.size):(de=J.getInt32(X,u.littleEndian),X+=4,ue=E.decode(M.slice(X,X+de)),X+=de),V.push(ue)}}return Y&&(M[0]=0),I.s11n.deserialize.time+=performance.now()-pe,V},u.s11n.serialize=function(...Y){const pe=performance.now();if(++I.s11n.serialize.count,Y.length){const F=[];let V=0,K=1;for(M[0]=Y.length&255;V<Y.length;++V,++K)F.push(te(Y[V])),M[K]=F[V].id;for(V
Line truncated
|
||||
Vendored
+2
-2
@@ -5,8 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#7c3aed" />
|
||||
<title>JapLearn — интервальные повторения</title>
|
||||
<script type="module" crossorigin src="/assets/index-17GvxYKC.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DhXfM6jc.css">
|
||||
<script type="module" crossorigin src="/assets/index-BdLQfp0Z.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CbQBYl48.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
|
||||
+130
-17
@@ -7,7 +7,7 @@
|
||||
// т.е. количество дней с 1970-01-01, без времени;
|
||||
// - states.fsrs_memory: сырое INTEGER-значение FSRS (не дата, не трогаем).
|
||||
|
||||
import { query } from "./sqlite-api";
|
||||
import { query } from "./sqlite-api.ts";
|
||||
|
||||
export interface CustomField {
|
||||
id: number;
|
||||
@@ -37,32 +37,145 @@ export interface Word {
|
||||
id: number;
|
||||
key: string;
|
||||
value: string;
|
||||
/** Сырое значение колонки tags (как лежит в БД). */
|
||||
tags: string;
|
||||
/** Распарсенные теги для UI (split по запятой/пробелам/точке с запятой). */
|
||||
tagsList: string[];
|
||||
/**
|
||||
* Доп. поля: more хранится как чередование пар через \u001f:
|
||||
* "fieldId\u001fvalue\u001ffieldId\u001fvalue...".
|
||||
* Ключ — id из custom_fields, значение — текст.
|
||||
*/
|
||||
more: Map<number, string>;
|
||||
group_id: number;
|
||||
}
|
||||
|
||||
export async function wordList() {
|
||||
let rows = await query("SELECT id, key, value, tags, more, group_id FROM words");
|
||||
export interface WordListFilter {
|
||||
/** Подстрока для поиска по key/value/tags (LIKE %q%, без учёта регистра). */
|
||||
search?: string;
|
||||
/** Фильтр по группе. undefined/null = все группы. */
|
||||
groupId?: number | null;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
return rows.map((row: any) => {
|
||||
let more : string = row.more.split('');
|
||||
let dict = new Map();
|
||||
for (let i = 0; i < more.length / 2; i++) {
|
||||
dict.set(more[i * 2], more[i * 2 + 1]);
|
||||
/** Разбирает сырое поле tags в список чипсов. */
|
||||
export function parseTags(raw: unknown): string[] {
|
||||
if (raw == null) return [];
|
||||
const s = String(raw).trim();
|
||||
if (!s) return [];
|
||||
return s
|
||||
.split(/[,\s;]+/)
|
||||
.map((t) => t.trim())
|
||||
.filter((t) => t.length > 0);
|
||||
}
|
||||
let w = {
|
||||
id: row.id,
|
||||
key: row.key,
|
||||
value: row.value,
|
||||
tags: row.tags,
|
||||
more: dict,
|
||||
group_id: row.group_id,
|
||||
|
||||
/**
|
||||
* Разбирает сырое поле more в Map<fieldId, value>.
|
||||
* Формат десктопа: пары через \u001f (Unit Separator):
|
||||
* "3\u001fчтение\u001f5\u001fпример". Нечисловые ключи игнорируем,
|
||||
* висячий хвост без пары отбрасываем, пустые значения пропускаем.
|
||||
*/
|
||||
export function parseMore(raw: unknown): Map<number, string> {
|
||||
const dict = new Map<number, string>();
|
||||
if (raw == null) return dict;
|
||||
const s = String(raw);
|
||||
if (!s) return dict;
|
||||
const parts = s.split("");
|
||||
for (let i = 0; i + 1 < parts.length; i += 2) {
|
||||
const id = Number(parts[i]);
|
||||
const val = parts[i + 1] ?? "";
|
||||
if (!Number.isFinite(id) || !Number.isInteger(id)) continue;
|
||||
if (!val) continue;
|
||||
dict.set(id, val);
|
||||
}
|
||||
return w;
|
||||
}) as Word[];
|
||||
return dict;
|
||||
}
|
||||
|
||||
function mapRowToWord(row: Record<string, unknown>): Word {
|
||||
const id = Number(row["id"] ?? 0);
|
||||
const key = String(row["key"] ?? "");
|
||||
const value = String(row["value"] ?? "");
|
||||
const tagsRaw = row["tags"] == null ? "" : String(row["tags"]);
|
||||
const group_id = Number(row["group_id"] ?? 1);
|
||||
return {
|
||||
id,
|
||||
key,
|
||||
value,
|
||||
tags: tagsRaw,
|
||||
tagsList: parseTags(tagsRaw),
|
||||
more: parseMore(row["more"]),
|
||||
group_id: Number.isFinite(group_id) ? group_id : 1,
|
||||
};
|
||||
}
|
||||
|
||||
function buildWhere(filter: WordListFilter): { clause: string; params: unknown[] } {
|
||||
const conds: string[] = [];
|
||||
const params: unknown[] = [];
|
||||
const search = (filter.search ?? "").trim();
|
||||
if (search) {
|
||||
// Простой LIKE-поиск по трём колонкам. more не трогаем (там id+значения вперемешку).
|
||||
conds.push("(key LIKE ? ESCAPE '\\' OR value LIKE ? ESCAPE '\\' OR tags LIKE ? ESCAPE '\\')");
|
||||
const like = `%${search.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_")}%`;
|
||||
params.push(like, like, like);
|
||||
}
|
||||
if (filter.groupId != null) {
|
||||
conds.push("group_id = ?");
|
||||
params.push(filter.groupId);
|
||||
}
|
||||
return { clause: conds.length > 0 ? `WHERE ${conds.join(" AND ")}` : "", params };
|
||||
}
|
||||
|
||||
/** Сколько всего слов подходит под фильтр (для пагинации/счётчика). */
|
||||
export async function countWords(filter: WordListFilter = {}): Promise<number> {
|
||||
const { clause, params } = buildWhere(filter);
|
||||
const rows = await query(`SELECT COUNT(*) as cnt FROM words ${clause}`, params as never[]);
|
||||
const cnt = (rows[0] as Record<string, unknown> | undefined)?.["cnt"];
|
||||
return typeof cnt === "number" ? cnt : Number(cnt ?? 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Визуализация слов для главной страницы: поиск + фильтр по группе + пагинация.
|
||||
* Сортировка — по id (порядок добавления), чтобы список был стабильным.
|
||||
*/
|
||||
export async function getWords(filter: WordListFilter = {}): Promise<Word[]> {
|
||||
const { clause, params } = buildWhere(filter);
|
||||
const limit = Math.max(1, Math.min(filter.limit ?? 50, 500));
|
||||
const offset = Math.max(0, filter.offset ?? 0);
|
||||
const rows = await query(
|
||||
`SELECT id, key, value, tags, more, group_id FROM words ${clause} ORDER BY id LIMIT ? OFFSET ?`,
|
||||
[...(params as unknown[]), limit, offset] as never[],
|
||||
);
|
||||
return (rows as Record<string, unknown>[]).map(mapRowToWord);
|
||||
}
|
||||
|
||||
/**
|
||||
* Легаси-обёртка: все слова без пагинации (для десктоп-совместимости / отладки).
|
||||
* Для UI используй getWords() + countWords().
|
||||
*/
|
||||
export async function wordList(): Promise<Word[]> {
|
||||
const rows = await query("SELECT id, key, value, tags, more, group_id FROM words ORDER BY id");
|
||||
return (rows as Record<string, unknown>[]).map(mapRowToWord);
|
||||
}
|
||||
|
||||
export async function getWordGroups(): Promise<WordGroup[]> {
|
||||
const rows = await query("SELECT id, name FROM word_groups ORDER BY id");
|
||||
return (rows as Record<string, unknown>[]).map((r) => ({
|
||||
id: Number(r["id"]),
|
||||
name: String(r["name"] ?? ""),
|
||||
}));
|
||||
}
|
||||
|
||||
export async function getCustomFields(): Promise<CustomField[]> {
|
||||
const rows = await query("SELECT id, name, identifier FROM custom_fields ORDER BY id");
|
||||
return (rows as Record<string, unknown>[]).map((r) => ({
|
||||
id: Number(r["id"]),
|
||||
name: String(r["name"] ?? ""),
|
||||
identifier: String(r["identifier"] ?? ""),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
export interface HistoryEntry {
|
||||
id: number;
|
||||
deck_id: number;
|
||||
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
import sqlite3InitModule from '@sqlite.org/sqlite-wasm';
|
||||
import { DB_FILENAME } from './opfs';
|
||||
import { DB_FILENAME } from './opfs.ts';
|
||||
|
||||
let db: any | null = null;
|
||||
|
||||
@@ -7,7 +7,6 @@ self.onmessage = async (e) => {
|
||||
const { type, id, sql, params } = e.data;
|
||||
|
||||
try {
|
||||
console.log(e.data.type)
|
||||
switch (type) {
|
||||
case 'start': {
|
||||
if (db != null) {
|
||||
@@ -30,14 +29,14 @@ self.onmessage = async (e) => {
|
||||
}
|
||||
case 'query': {
|
||||
if (db === null) throw new Error('БД не инициализирована');
|
||||
const result = db.selectObjects(sql, params);
|
||||
const result = db.selectObjects(sql, params ?? []);
|
||||
self.postMessage({ type: 'success', id, data: result });
|
||||
|
||||
break;
|
||||
}
|
||||
case 'command': {
|
||||
if (db === null) throw new Error('БД не инициализирована');
|
||||
db.exec(sql, params);
|
||||
db.exec(sql, params ?? []);
|
||||
self.postMessage({ type: 'success', id, data: null });
|
||||
break;
|
||||
}
|
||||
@@ -45,11 +44,12 @@ self.onmessage = async (e) => {
|
||||
if (db === null) throw new Error('БД не инициализирована');
|
||||
db.close();
|
||||
db = null;
|
||||
self.postMessage({ type: 'success', id, data: null });
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(err);
|
||||
self.postMessage({ type: 'error', message: err.message , id});
|
||||
self.postMessage({ type: 'error', error: err?.message ?? String(err), id });
|
||||
}
|
||||
};
|
||||
+177
-7
@@ -1,8 +1,8 @@
|
||||
// Главная страница: статус локальной базы + быстрый переход к синхронизации.
|
||||
// Никакой БД-логики кроме проверки наличия файла в OPFS.
|
||||
// Главная страница: статус локальной базы + визуализация слов (getWords).
|
||||
// Поиск по key/value/tags, фильтр по группе, пагинация «Показать ещё».
|
||||
|
||||
import { wordList } from './db-types.ts';
|
||||
import { worker } from './main.ts';
|
||||
import { countWords, getCustomFields, getWordGroups, getWords } from './db-types.ts';
|
||||
import type { CustomField, Word, WordGroup } from './db-types.ts';
|
||||
import { hasDatabaseFile, readDatabaseFile } from './opfs.ts';
|
||||
import { looksLikeSQLite } from './zstd.ts';
|
||||
|
||||
@@ -10,6 +10,36 @@ export type HomeNav = {
|
||||
goSync: () => void;
|
||||
};
|
||||
|
||||
const PAGE_SIZE = 30;
|
||||
|
||||
function escapeHtml(s: string): string {
|
||||
return s
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function wordCardHtml(w: Word, gname: string | undefined, fmap: Map<number, CustomField>): string {
|
||||
const tags = w.tagsList.map((t) => `<span class="tag-chip">${escapeHtml(t)}</span>`).join('');
|
||||
const mrows = [...w.more.entries()]
|
||||
.map(([fid, val]) => {
|
||||
const f = fmap.get(fid);
|
||||
const label = f ? `${escapeHtml(f.name)} <span class="word-more-id">#${fid}</span>` : `Поле #${fid}`;
|
||||
return `<div class="word-more-row"><dt>${label}</dt><dd>${escapeHtml(val)}</dd></div>`;
|
||||
})
|
||||
.join('');
|
||||
const more = mrows
|
||||
? `<details class="word-more"><summary>Дополнительно (${w.more.size})</summary><dl class="word-more-list">${mrows}</dl></details>`
|
||||
: '';
|
||||
return `<article class="word-card" data-word-id="${w.id}">
|
||||
<div class="word-card-top"><div class="word-key">${escapeHtml(w.key)}</div>`
|
||||
+ (gname ? `<span class="word-group">${escapeHtml(gname)}</span>` : '')
|
||||
+ `</div><div class="word-value">${escapeHtml(w.value)}</div>`
|
||||
+ (tags ? `<div class="tag-row">${tags}</div>` : '') + more + `</article>`;
|
||||
}
|
||||
|
||||
export function renderHomePage(root: HTMLElement, nav: HomeNav): void {
|
||||
root.innerHTML = `
|
||||
<main class="card" aria-labelledby="home-title">
|
||||
@@ -23,23 +53,150 @@ export function renderHomePage(root: HTMLElement, nav: HomeNav): void {
|
||||
<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 statusEl = root.querySelector<HTMLElement>('#home-db-status')!;
|
||||
const goSync = root.querySelector<HTMLButtonElement>('#btn-go-sync')!;
|
||||
const actions = root.querySelector<HTMLElement>('#home-actions')!;
|
||||
const btnStart = root.querySelector<HTMLButtonElement>('#btn-start')!;
|
||||
const searchInput = root.querySelector<HTMLInputElement>('#words-search')!;
|
||||
const groupSelect = root.querySelector<HTMLSelectElement>('#words-group')!;
|
||||
const countEl = root.querySelector<HTMLElement>('#words-count')!;
|
||||
const wordsStatus = root.querySelector<HTMLElement>('#words-status')!;
|
||||
const listEl = root.querySelector<HTMLElement>('#words-list')!;
|
||||
const moreBtn = root.querySelector<HTMLButtonElement>('#words-more')!;
|
||||
|
||||
goSync.addEventListener('click', () => nav.goSync());
|
||||
btnStart.addEventListener('click', () => {
|
||||
statusEl.textContent = 'Экран повторений — следующий шаг.';
|
||||
});
|
||||
|
||||
// --- Состояние списка слов ---
|
||||
let dbReady = false;
|
||||
let gnames = new Map<number, string>();
|
||||
let fmap = new Map<number, CustomField>();
|
||||
let total = 0;
|
||||
let loaded: Word[] = [];
|
||||
let offset = 0;
|
||||
let loading = false;
|
||||
let reqSeq = 0;
|
||||
let debounce = 0;
|
||||
|
||||
function wordsFilter(): { search: string; groupId: number | null } {
|
||||
const v = groupSelect.value;
|
||||
if (v === '') return { search: searchInput.value.trim(), groupId: null };
|
||||
const n = Number(v);
|
||||
return { search: searchInput.value.trim(), groupId: Number.isFinite(n) ? n : null };
|
||||
}
|
||||
|
||||
function refreshMoreBtn(): void {
|
||||
moreBtn.hidden = !(dbReady && loaded.length < total);
|
||||
if (!moreBtn.hidden) moreBtn.textContent = `Показать ещё (${total - loaded.length} осталось)`;
|
||||
}
|
||||
|
||||
function fullRender(): void {
|
||||
listEl.innerHTML = loaded.map((w) => wordCardHtml(w, gnames.get(w.group_id), fmap)).join('');
|
||||
countEl.textContent = total > 0 ? `Показано ${loaded.length} из ${total}` : '';
|
||||
refreshMoreBtn();
|
||||
}
|
||||
|
||||
function setWStatus(msg: string): void {
|
||||
wordsStatus.textContent = msg;
|
||||
wordsStatus.hidden = msg === '';
|
||||
}
|
||||
|
||||
async function reloadWords(): Promise<void> {
|
||||
if (!dbReady || loading) return;
|
||||
loading = true;
|
||||
const my = ++reqSeq;
|
||||
const f = wordsFilter();
|
||||
moreBtn.disabled = true;
|
||||
if (loaded.length === 0) setWStatus('Загрузка слов…');
|
||||
try {
|
||||
const [cnt, words] = await Promise.all([
|
||||
countWords({ search: f.search, groupId: f.groupId ?? undefined }),
|
||||
getWords({ search: f.search, groupId: f.groupId ?? undefined, limit: PAGE_SIZE, offset: 0 }),
|
||||
]);
|
||||
if (my !== reqSeq) return;
|
||||
total = cnt;
|
||||
loaded = words;
|
||||
offset = words.length;
|
||||
fullRender();
|
||||
setWStatus(words.length === 0 ? 'По этому запросу слов нет.' : '');
|
||||
if (words.length === 0 && total === 0) countEl.textContent = 'Слов в базе пока нет.';
|
||||
} catch (err) {
|
||||
if (my !== reqSeq) return;
|
||||
setWStatus(`Не удалось загрузить слова: ${err instanceof Error ? err.message : String(err)}`);
|
||||
} finally {
|
||||
if (my === reqSeq) {
|
||||
loading = false;
|
||||
moreBtn.disabled = false;
|
||||
refreshMoreBtn();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMore(): Promise<void> {
|
||||
if (!dbReady || loading || loaded.length >= total) return;
|
||||
loading = true;
|
||||
const my = ++reqSeq;
|
||||
const f = wordsFilter();
|
||||
moreBtn.disabled = true;
|
||||
moreBtn.textContent = 'Загрузка…';
|
||||
try {
|
||||
const words = await getWords({ search: f.search, groupId: f.groupId ?? undefined, limit: PAGE_SIZE, offset });
|
||||
if (my !== reqSeq) return;
|
||||
loaded = loaded.concat(words);
|
||||
offset += words.length;
|
||||
listEl.insertAdjacentHTML('beforeend', words.map((w) => wordCardHtml(w, gnames.get(w.group_id), fmap)).join(''));
|
||||
countEl.textContent = `Показано ${loaded.length} из ${total}`;
|
||||
refreshMoreBtn();
|
||||
} catch (err) {
|
||||
if (my !== reqSeq) return;
|
||||
setWStatus(`Не удалось догрузить: ${err instanceof Error ? err.message : String(err)}`);
|
||||
} finally {
|
||||
if (my === reqSeq) {
|
||||
loading = false;
|
||||
moreBtn.disabled = false;
|
||||
refreshMoreBtn();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
searchInput.addEventListener('input', () => {
|
||||
window.clearTimeout(debounce);
|
||||
debounce = window.setTimeout(() => void reloadWords(), 300);
|
||||
});
|
||||
searchInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
window.clearTimeout(debounce);
|
||||
void reloadWords();
|
||||
}
|
||||
});
|
||||
groupSelect.addEventListener('change', () => void reloadWords());
|
||||
moreBtn.addEventListener('click', () => void loadMore());
|
||||
|
||||
void (async () => {
|
||||
const has = await hasDatabaseFile();
|
||||
if (!has) {
|
||||
statusEl.textContent = 'Локальной базы нет. Скачай сохранение на странице «Синхронизация».';
|
||||
actions.hidden = true;
|
||||
setWStatus('Нет локальной базы — скачай сохранение на странице «Синхронизация».');
|
||||
countEl.textContent = '';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -49,9 +206,22 @@ export function renderHomePage(root: HTMLElement, nav: HomeNav): void {
|
||||
? `Локальная база на месте: ${(raw.length / 1024).toFixed(1)} КБ, сигнатура SQLite OK.`
|
||||
: 'Файл в OPFS есть, но это не похоже на SQLite — перекачай сохранение.';
|
||||
actions.hidden = !ok;
|
||||
if (ok) {
|
||||
let list = await wordList();
|
||||
console.log(list);
|
||||
if (!ok) {
|
||||
setWStatus('Файл в OPFS не похож на SQLite — перекачай сохранение.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const [gs, fields] = await Promise.all([getWordGroups(), getCustomFields()]);
|
||||
const groups: WordGroup[] = gs;
|
||||
gnames = new Map(groups.map((g) => [g.id, g.name]));
|
||||
fmap = new Map(fields.map((f) => [f.id, f]));
|
||||
groupSelect.innerHTML = '<option value="">Все группы</option>'
|
||||
+ groups.map((g) => `<option value="${g.id}">${escapeHtml(g.name)}</option>`).join('');
|
||||
dbReady = true;
|
||||
await reloadWords();
|
||||
groupSelect.options[0].textContent = `Все группы (${total})`;
|
||||
} catch (err) {
|
||||
setWStatus(`БД есть, но слова не прочитались: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
} catch (err) {
|
||||
statusEl.textContent = `Не удалось прочитать базу: ${err instanceof Error ? err.message : String(err)}`;
|
||||
|
||||
@@ -3,10 +3,6 @@ import { renderHomePage } from './home-page.ts';
|
||||
import { renderSyncPage } from './sync-page.ts';
|
||||
import { initWorker } from './sqlite-api.ts';
|
||||
|
||||
export const worker = new Worker(new URL('./db-worker.ts', import.meta.url), {
|
||||
type: 'module',
|
||||
});
|
||||
|
||||
// Каркас приложения: топ-бар + бургер-меню + две страницы (Главная / Синхронизация).
|
||||
|
||||
type Page = 'home' | 'sync';
|
||||
|
||||
+6
-6
@@ -1,4 +1,6 @@
|
||||
import { worker } from "./main";
|
||||
const worker = new Worker(new URL('./db-worker.ts', import.meta.url), {
|
||||
type: 'module',
|
||||
});
|
||||
|
||||
let requestId = 0;
|
||||
|
||||
@@ -14,7 +16,6 @@ export function initWorker(): Promise<void> {
|
||||
|
||||
initPromise = new Promise((resolve, reject) => {
|
||||
worker.onmessage = (e: MessageEvent) => {
|
||||
console.log("Handling worker output");
|
||||
const msg = e.data;
|
||||
|
||||
if (msg.type === 'ready') {
|
||||
@@ -29,8 +30,7 @@ export function initWorker(): Promise<void> {
|
||||
if (msg.type == "success") {
|
||||
pendingReq.resolve(msg.data);
|
||||
} else {
|
||||
console.log(msg)
|
||||
pendingReq.reject(new Error(msg.error || 'Неизвестная ошибка Worker'));
|
||||
pendingReq.reject(new Error(msg.error ?? msg.message ?? 'Неизвестная ошибка Worker'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -63,12 +63,12 @@ function sendToWorker(type: string, payload?: any): Promise<any> {
|
||||
}
|
||||
|
||||
export async function query(sql: string, params?: any[]): Promise<any[]> {
|
||||
const result = await sendToWorker('query', { sql, params });
|
||||
const result = await sendToWorker('query', { sql, params: params ?? [] });
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function command(sql: string, params?: any[]): Promise<void> {
|
||||
await sendToWorker('command', { sql, params });
|
||||
await sendToWorker('command', { sql, params: params ?? [] });
|
||||
}
|
||||
|
||||
export async function closeDb(): Promise<void> {
|
||||
|
||||
+149
@@ -281,3 +281,152 @@ body.drawer-open {
|
||||
font-size: 0.95rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.status-bar[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* --- Список слов (главная) --- */
|
||||
|
||||
.words-toolbar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.words-search {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 12px;
|
||||
font-size: 1rem;
|
||||
border: 2px solid #d9d2f0;
|
||||
border-radius: 12px;
|
||||
outline: none;
|
||||
background: #fff;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.words-search:focus {
|
||||
border-color: #7c3aed;
|
||||
}
|
||||
|
||||
.words-group {
|
||||
flex: none;
|
||||
max-width: 42%;
|
||||
padding: 12px 8px;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: #4c446b;
|
||||
border: 2px solid #d9d2f0;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.words-group:focus {
|
||||
border-color: #7c3aed;
|
||||
}
|
||||
|
||||
.words-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.word-card {
|
||||
border: 1px solid #eee9fb;
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
background: #fbfaff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.word-card-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.word-key {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.word-value {
|
||||
font-size: 1rem;
|
||||
color: #4c446b;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.word-group {
|
||||
flex: none;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: #6d28d9;
|
||||
background: #f1eafe;
|
||||
border-radius: 999px;
|
||||
padding: 4px 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tag-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tag-chip {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: #5b21b6;
|
||||
background: #ede9fe;
|
||||
border-radius: 999px;
|
||||
padding: 3px 10px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.word-more {
|
||||
font-size: 0.9rem;
|
||||
color: #4c446b;
|
||||
}
|
||||
|
||||
.word-more summary {
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
color: #6d28d9;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.word-more-list {
|
||||
margin: 8px 0 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.word-more-row {
|
||||
background: #fff;
|
||||
border: 1px solid #eee9fb;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.word-more-row dt {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
color: #6d6390;
|
||||
}
|
||||
|
||||
.word-more-row dd {
|
||||
margin: 2px 0 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.word-more-id {
|
||||
font-weight: 400;
|
||||
opacity: 0.7;
|
||||
}
|
||||
Reference in new issue
Block a user