mirror of
https://github.com/Mabbs/mabbs.github.io
synced 2026-10-03 08:07:21 +00:00
Update 12 files
- /vendor/libarchive/LICENSE - /vendor/libarchive/worker-bundle.js - /vendor/libarchive/wasm-gen/libarchive.js - /vendor/libarchive/wasm-gen/libarchive.wasm - /sw.js - /offline.md - /_layouts/default.html - /_tools/serv00_post-receive - /_tools/envs_post-receive - /_data/proxylist.yml - /assets/js/offline-page.js - /assets/js/offline-unpack.js
This commit is contained in:
1 parent
7659d3e9d5
commit
8cb782d1ff
12 files changed
+609
-4
No files matched your search
@@ -17,7 +17,6 @@ mirrors:
|
|||||||
- https://mayx.vercel.app/
|
- https://mayx.vercel.app/
|
||||||
- https://mayx.netlify.app/
|
- https://mayx.netlify.app/
|
||||||
- https://mayx.gitnet.page/
|
- https://mayx.gitnet.page/
|
||||||
- https://mayx.stormkit.dev/
|
|
||||||
- https://mayx.grebedoc.dev/
|
- https://mayx.grebedoc.dev/
|
||||||
- https://mayx.codeberg.page/
|
- https://mayx.codeberg.page/
|
||||||
- https://mayx.tildepages.org/
|
- https://mayx.tildepages.org/
|
||||||
|
|||||||
@@ -98,7 +98,8 @@ layout: xslt_container
|
|||||||
<li><a href="{{ site.github.tar_url }}">Download <strong>TAR Ball</strong></a></li>
|
<li><a href="{{ site.github.tar_url }}">Download <strong>TAR Ball</strong></a></li>
|
||||||
<li><a href="{{ site.github.repository_url }}">View On <strong>GitHub</strong></a></li>
|
<li><a href="{{ site.github.repository_url }}">View On <strong>GitHub</strong></a></li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li style="width: 270px; border-right: none;"><a href="/MayxBlog.7z">Download <strong>7-Zip File</strong></a></li>
|
<li style="width: 174px;"><a href="/MayxBlog.7z">Download <strong>7-Zip File</strong></a></li>
|
||||||
|
<li style="width: 174px;"><a href="/offline.html">Manage <strong>Offline Mode</strong></a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ mkdir Mabbs
|
|||||||
curl -L -o Mabbs/README.md https://github.com/Mabbs/Mabbs/raw/main/README.md
|
curl -L -o Mabbs/README.md https://github.com/Mabbs/Mabbs/raw/main/README.md
|
||||||
bundle exec jekyll build -d ../public_html
|
bundle exec jekyll build -d ../public_html
|
||||||
python3 _tools/blogquine.py --src-dir blog --quine-dir blog ../public_html/ MayxBlog.7z
|
python3 _tools/blogquine.py --src-dir blog --quine-dir blog ../public_html/ MayxBlog.7z
|
||||||
mv MayxBlog.7z ../public_html/
|
7z a -mx9 -bd -bb0 MayxBlog-new.7z MayxBlog.7z
|
||||||
|
mv MayxBlog-new.7z ../public_html/MayxBlog.7z
|
||||||
cd ../public_html/
|
cd ../public_html/
|
||||||
unset GIT_DIR
|
unset GIT_DIR
|
||||||
git init
|
git init
|
||||||
|
|||||||
@@ -7,13 +7,15 @@ mkdir Mabbs
|
|||||||
curl -L -o Mabbs/README.md https://github.com/Mabbs/Mabbs/raw/main/README.md
|
curl -L -o Mabbs/README.md https://github.com/Mabbs/Mabbs/raw/main/README.md
|
||||||
bundle exec jekyll build -d ../domains/mayx.serv00.net/public_html/
|
bundle exec jekyll build -d ../domains/mayx.serv00.net/public_html/
|
||||||
python3 _tools/blogquine.py --src-dir blog --quine-dir blog ../domains/mayx.serv00.net/public_html/ MayxBlog.7z
|
python3 _tools/blogquine.py --src-dir blog --quine-dir blog ../domains/mayx.serv00.net/public_html/ MayxBlog.7z
|
||||||
mv MayxBlog.7z ../domains/mayx.serv00.net/public_html/
|
7zz a -mx9 -bd -bb0 MayxBlog-new.7z MayxBlog.7z
|
||||||
|
mv MayxBlog-new.7z ../domains/mayx.serv00.net/public_html/MayxBlog.7z
|
||||||
rsync -avz --delete ../domains/mayx.serv00.net/public_html/ mayx@web.sourceforge.net:/home/project-web/mayx/htdocs/
|
rsync -avz --delete ../domains/mayx.serv00.net/public_html/ mayx@web.sourceforge.net:/home/project-web/mayx/htdocs/
|
||||||
cd ../domains/mayx.serv00.net/public_html/
|
cd ../domains/mayx.serv00.net/public_html/
|
||||||
unset GIT_DIR
|
unset GIT_DIR
|
||||||
git init
|
git init
|
||||||
git lfs install
|
git lfs install
|
||||||
git lfs track "*.png"
|
git lfs track "*.png"
|
||||||
|
git lfs track "*.webp"
|
||||||
git lfs track "*.moc"
|
git lfs track "*.moc"
|
||||||
git lfs track "*.tgz"
|
git lfs track "*.tgz"
|
||||||
git lfs track "*.jpg"
|
git lfs track "*.jpg"
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
/*
|
||||||
|
* Mayx的博客 · /offline.html 控制页控制器(classic script,PJAX 兼容)
|
||||||
|
*
|
||||||
|
* 本体是 IIFE,不含模块语法:PJAX 换页时 jQuery 会重新执行本文件,
|
||||||
|
* 每次执行都重新查询元素、重新绑定事件,天然幂等。真正的解包逻辑在
|
||||||
|
* ES module /assets/js/offline-unpack.js 中,通过动态 import() 加载
|
||||||
|
* (浏览器模块缓存保证 PJAX 反复进入本页也不会重复加载)。
|
||||||
|
* 注意:本文件经 Liquid 原样输出,不能使用模板语法字符。
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
var MODULE_URL = '/assets/js/offline-unpack.js';
|
||||||
|
// 压缩包大致量级(当前约 27MB),仅用于进度条比例,不影响正确性
|
||||||
|
var EXPECTED_BYTES = 27 * 1024 * 1024;
|
||||||
|
var busy = false;
|
||||||
|
|
||||||
|
function el(id) { return document.getElementById(id); }
|
||||||
|
function fmtMB(bytes) { return (bytes / 1048576).toFixed(1) + ' MB'; }
|
||||||
|
|
||||||
|
function setMessage(text) {
|
||||||
|
el('offline-message').textContent = text || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showProgress(show) {
|
||||||
|
el('offline-progress-box').hidden = !show;
|
||||||
|
}
|
||||||
|
|
||||||
|
function render(meta) {
|
||||||
|
var online = !meta;
|
||||||
|
el('offline-state').textContent = online
|
||||||
|
? '在线模式(离线未启用)'
|
||||||
|
: '离线模式(优先从本地缓存加载)';
|
||||||
|
el('offline-version').textContent = online ? '-' : fmtMB(meta.payloadSize) + '(压缩包大小即版本号)';
|
||||||
|
el('offline-files').textContent = online ? '-' : meta.files + ' 个文件 / ' + fmtMB(meta.bytes);
|
||||||
|
el('offline-time').textContent = online ? '-' : new Date(meta.unpackedAt).toLocaleString();
|
||||||
|
el('offline-persist').textContent = online ? '-' : (meta.persisted ? '已授权(缓存不会被自动回收)' : '未授权(浏览器空间紧张时可能回收缓存)');
|
||||||
|
el('offline-enable').disabled = busy || !online;
|
||||||
|
el('offline-disable').disabled = busy || online;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onProgress(info) {
|
||||||
|
if (info.done) return;
|
||||||
|
el('offline-progress').value = Math.min(100, (info.bytes / EXPECTED_BYTES) * 100);
|
||||||
|
el('offline-progress-text').textContent =
|
||||||
|
'已解包 ' + info.files + ' 个文件(' + fmtMB(info.bytes) + ')…';
|
||||||
|
}
|
||||||
|
|
||||||
|
function withModule(fn) {
|
||||||
|
return import(MODULE_URL).then(fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
if (!('serviceWorker' in navigator) || !('caches' in window)) {
|
||||||
|
el('offline-state').textContent = '当前浏览器不支持离线模式(需要 Service Worker)';
|
||||||
|
el('offline-enable').disabled = true;
|
||||||
|
el('offline-disable').disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
withModule(function (mod) { return mod.ensureFresh(); }).then(function (fresh) {
|
||||||
|
render(fresh.ready ? fresh.meta : null);
|
||||||
|
if (fresh.stale) {
|
||||||
|
setMessage('检测到站点压缩包已更新,离线缓存已自动失效,当前处于在线模式。可重新启用离线模式。');
|
||||||
|
}
|
||||||
|
}).catch(function (e) {
|
||||||
|
setMessage('状态检测失败:' + (e && e.message ? e.message : e));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEnable() {
|
||||||
|
busy = true;
|
||||||
|
render(null);
|
||||||
|
showProgress(true);
|
||||||
|
setMessage('正在下载并解包站点压缩包,请保持网络连接…');
|
||||||
|
withModule(function (mod) { return mod.activateOffline({ onProgress: onProgress }); })
|
||||||
|
.then(function (meta) {
|
||||||
|
el('offline-progress').value = 100;
|
||||||
|
el('offline-progress-text').textContent = '解包完成';
|
||||||
|
setMessage('离线模式已启用。之后访问本博客将优先使用本地缓存,断网也能阅读。');
|
||||||
|
busy = false;
|
||||||
|
showProgress(false);
|
||||||
|
render(meta); // 立即展示离线状态,不必等后台刷新
|
||||||
|
})
|
||||||
|
.catch(function (e) {
|
||||||
|
setMessage('启用失败:' + (e && e.message ? e.message : e));
|
||||||
|
busy = false;
|
||||||
|
showProgress(false);
|
||||||
|
refresh();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDisable() {
|
||||||
|
busy = true;
|
||||||
|
setMessage('');
|
||||||
|
withModule(function (mod) { return mod.deactivateOffline(); })
|
||||||
|
.then(function () {
|
||||||
|
setMessage('已切回在线模式,之后页面将通过网络加载。');
|
||||||
|
busy = false;
|
||||||
|
render(null);
|
||||||
|
if (navigator.onLine) {
|
||||||
|
setTimeout(function () { location.reload(); }, 800);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function (e) {
|
||||||
|
setMessage('操作失败:' + (e && e.message ? e.message : e));
|
||||||
|
busy = false;
|
||||||
|
refresh();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bind() {
|
||||||
|
var enable = el('offline-enable');
|
||||||
|
if (!enable) return; // 不在控制页(PJAX 容错)
|
||||||
|
enable.addEventListener('click', onEnable);
|
||||||
|
el('offline-disable').addEventListener('click', onDisable);
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
bind();
|
||||||
|
})();
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
/*
|
||||||
|
* Mayx的博客 · 离线模式核心逻辑(/offline.html 控制页使用,ES module)
|
||||||
|
*
|
||||||
|
* - activateOffline():注册 SW,下载站点压缩包 /MayxBlog.7z,经 libarchive(WASM)
|
||||||
|
* 流式解包,把每个文件按站点路径写入 Cache Storage,最后写入就绪标记
|
||||||
|
* (清单 + payloadSize 版本号),自此进入离线模式。
|
||||||
|
* - ensureFresh():HEAD 探测线上压缩包大小,与标记中的 payloadSize 不一致
|
||||||
|
* 说明站点已更新 → 清空缓存,回退在线模式。
|
||||||
|
* - deactivateOffline():清空缓存,手动切回在线模式。
|
||||||
|
* - readStatus():读取当前离线状态,供控制页展示。
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
const ARCHIVE_URL = '/MayxBlog.7z';
|
||||||
|
const ARCHIVE_ROOT = 'blog/';
|
||||||
|
const CACHE_NAME = 'mayxblog-offline-v1';
|
||||||
|
const READY_KEY = '/__offline_manifest__';
|
||||||
|
const WORKER_URL = '/vendor/libarchive/worker-bundle.js';
|
||||||
|
|
||||||
|
const MIME = {
|
||||||
|
html: 'text/html; charset=utf-8',
|
||||||
|
htm: 'text/html; charset=utf-8',
|
||||||
|
css: 'text/css; charset=utf-8',
|
||||||
|
js: 'application/javascript; charset=utf-8',
|
||||||
|
mjs: 'application/javascript; charset=utf-8',
|
||||||
|
json: 'application/json; charset=utf-8',
|
||||||
|
xml: 'application/xml; charset=utf-8',
|
||||||
|
xsl: 'text/xsl; charset=utf-8',
|
||||||
|
opml: 'text/x-opml; charset=utf-8',
|
||||||
|
txt: 'text/plain; charset=utf-8',
|
||||||
|
md: 'text/markdown; charset=utf-8',
|
||||||
|
csv: 'text/csv; charset=utf-8',
|
||||||
|
svg: 'image/svg+xml',
|
||||||
|
png: 'image/png',
|
||||||
|
jpg: 'image/jpeg',
|
||||||
|
jpeg: 'image/jpeg',
|
||||||
|
gif: 'image/gif',
|
||||||
|
webp: 'image/webp',
|
||||||
|
avif: 'image/avif',
|
||||||
|
ico: 'image/x-icon',
|
||||||
|
bmp: 'image/bmp',
|
||||||
|
woff: 'font/woff',
|
||||||
|
woff2: 'font/woff2',
|
||||||
|
ttf: 'font/ttf',
|
||||||
|
otf: 'font/otf',
|
||||||
|
eot: 'application/vnd.ms-fontobject',
|
||||||
|
mp3: 'audio/mpeg',
|
||||||
|
mp4: 'video/mp4',
|
||||||
|
webm: 'video/webm',
|
||||||
|
'7z': 'application/x-7z-compressed',
|
||||||
|
zip: 'application/zip',
|
||||||
|
wasm: 'application/wasm',
|
||||||
|
mtn: 'application/octet-stream',
|
||||||
|
moc: 'application/octet-stream',
|
||||||
|
pdf: 'application/pdf',
|
||||||
|
};
|
||||||
|
|
||||||
|
function mimeFor(pathname) {
|
||||||
|
const dot = pathname.lastIndexOf('.');
|
||||||
|
const ext = dot === -1 ? '' : pathname.slice(dot + 1).toLowerCase();
|
||||||
|
return MIME[ext] || 'application/octet-stream';
|
||||||
|
}
|
||||||
|
|
||||||
|
function sitePathFor(entryPath) {
|
||||||
|
const normalized = entryPath.replace(/\\/g, '/');
|
||||||
|
if (!normalized.startsWith(ARCHIVE_ROOT)) return null;
|
||||||
|
const relative = normalized.slice(ARCHIVE_ROOT.length);
|
||||||
|
if (!relative) return null;
|
||||||
|
|
||||||
|
const segments = [];
|
||||||
|
for (const segment of relative.split('/')) {
|
||||||
|
if (!segment || segment === '.') continue;
|
||||||
|
if (segment === '..') return null;
|
||||||
|
segments.push(segment);
|
||||||
|
}
|
||||||
|
if (!segments.length) return null;
|
||||||
|
return '/' + segments.join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 在独立 worker 中打开一层压缩包并流式吐出条目;
|
||||||
|
* 返回其中根级嵌套压缩包(下一层包装)的原始字节。
|
||||||
|
*/
|
||||||
|
function extractLayer(file, onEntry) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const worker = new Worker(WORKER_URL);
|
||||||
|
const nested = [];
|
||||||
|
let settled = false;
|
||||||
|
|
||||||
|
const finish = (error) => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
worker.terminate();
|
||||||
|
if (error) reject(error);
|
||||||
|
else resolve(nested);
|
||||||
|
};
|
||||||
|
|
||||||
|
worker.addEventListener('error', (event) => {
|
||||||
|
finish(new Error(event.message || 'libarchive worker failed'));
|
||||||
|
});
|
||||||
|
|
||||||
|
worker.addEventListener('message', ({ data }) => {
|
||||||
|
if (data.type === 'ERROR') {
|
||||||
|
finish(new Error(data.error && data.error.message ? data.error.message : 'libarchive failed'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.type === 'READY') {
|
||||||
|
worker.postMessage({ type: 'OPEN', file });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.type === 'OPENED') {
|
||||||
|
worker.postMessage({ type: 'EXTRACT_FILES' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.type === 'END') {
|
||||||
|
finish(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.type !== 'ENTRY') return;
|
||||||
|
|
||||||
|
const entry = data.entry;
|
||||||
|
if (entry.type !== 'FILE' || !entry.fileData) return;
|
||||||
|
|
||||||
|
// 根级压缩包是下一层包装,不是站点内容
|
||||||
|
if (entry.path.replace(/\\/g, '/') === 'MayxBlog.7z') {
|
||||||
|
nested.push(entry.fileData);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onEntry(entry);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 几十 MB 的缓存是浏览器常见的回收对象,而重新解包代价高,尽量申请持久存储。 */
|
||||||
|
async function requestPersistence() {
|
||||||
|
if (!navigator.storage || !navigator.storage.persist) return false;
|
||||||
|
try {
|
||||||
|
if (await navigator.storage.persisted()) return true;
|
||||||
|
return await navigator.storage.persist();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 读取就绪标记(= 离线模式状态);损坏或缺清单的标记视为不存在。 */
|
||||||
|
export async function readStatus() {
|
||||||
|
const cache = await caches.open(CACHE_NAME);
|
||||||
|
const hit = await cache.match(READY_KEY);
|
||||||
|
if (!hit) return null;
|
||||||
|
const meta = await hit.json().catch(() => null);
|
||||||
|
if (!meta || !Array.isArray(meta.keys) || !meta.keys.length) return null;
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 线上压缩包的大小即版本号。HEAD 不被 SW 拦截,拿到的一定是线上真实值;
|
||||||
|
* 网络失败返回 null,调用方选择信任现有缓存而不是误伤。
|
||||||
|
*/
|
||||||
|
async function payloadSize() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(ARCHIVE_URL, { method: 'HEAD', cache: 'no-store' });
|
||||||
|
if (!response.ok) return null;
|
||||||
|
const size = Number(response.headers.get('content-length'));
|
||||||
|
return Number.isFinite(size) && size > 0 ? size : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 控制页打开时调用:未启用 → {ready:false};已启用且版本一致 → {ready:true, meta};
|
||||||
|
* 版本不一致 → 清空缓存并返回 {ready:false, stale:true}(已回退在线模式)。
|
||||||
|
*/
|
||||||
|
export async function ensureFresh() {
|
||||||
|
const cache = await caches.open(CACHE_NAME);
|
||||||
|
const size = await payloadSize();
|
||||||
|
const meta = await readStatus();
|
||||||
|
|
||||||
|
if (!meta) return { ready: false, size, stale: false };
|
||||||
|
if (size && meta.payloadSize !== size) {
|
||||||
|
await caches.delete(CACHE_NAME);
|
||||||
|
return { ready: false, size, stale: true };
|
||||||
|
}
|
||||||
|
return { ready: true, size, stale: false, meta };
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 启用离线模式:注册 SW → 清空旧缓存(同时清掉上次中断的残留,保证随后
|
||||||
|
* 的压缩包下载不会被旧缓存喂旧的 quine 副本)→ 下载并逐层解包 → 写标记。
|
||||||
|
*/
|
||||||
|
export async function activateOffline({ onProgress } = {}) {
|
||||||
|
if (!('serviceWorker' in navigator)) {
|
||||||
|
throw new Error('当前浏览器不支持 Service Worker,无法启用离线模式。');
|
||||||
|
}
|
||||||
|
await navigator.serviceWorker.register('/sw.js', { scope: '/' });
|
||||||
|
await navigator.serviceWorker.ready;
|
||||||
|
await caches.delete(CACHE_NAME);
|
||||||
|
|
||||||
|
const persisted = await requestPersistence();
|
||||||
|
|
||||||
|
// 写操作串行链,保证按到达顺序落盘且不产生无界并发
|
||||||
|
const cache = await caches.open(CACHE_NAME);
|
||||||
|
let writes = Promise.resolve();
|
||||||
|
const keys = new Set();
|
||||||
|
let files = 0;
|
||||||
|
let bytes = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
|
||||||
|
const response = await fetch(ARCHIVE_URL, { cache: 'no-store' });
|
||||||
|
if (!response.ok) throw new Error(`无法下载站点压缩包:HTTP ${response.status}`);
|
||||||
|
const outer = await response.arrayBuffer();
|
||||||
|
const downloadedBytes = outer.byteLength;
|
||||||
|
|
||||||
|
let layer = [outer];
|
||||||
|
while (layer.length) {
|
||||||
|
const bytesThisLayer = layer.shift();
|
||||||
|
const file = new File([bytesThisLayer], 'MayxBlog.7z');
|
||||||
|
|
||||||
|
layer = await extractLayer(file, (entry) => {
|
||||||
|
const key = sitePathFor(entry.path);
|
||||||
|
if (!key) {
|
||||||
|
skipped += 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const body = entry.fileData;
|
||||||
|
files += 1;
|
||||||
|
bytes += body.byteLength;
|
||||||
|
|
||||||
|
writes = writes.then(async () => {
|
||||||
|
await cache.put(key, new Response(body, {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': mimeFor(key) },
|
||||||
|
}));
|
||||||
|
keys.add(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (onProgress) onProgress({ done: false, files, bytes, skipped });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await writes;
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
files,
|
||||||
|
bytes,
|
||||||
|
skipped,
|
||||||
|
persisted,
|
||||||
|
payloadSize: downloadedBytes,
|
||||||
|
failed: files - keys.size,
|
||||||
|
keys: [...keys],
|
||||||
|
unpackedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
await cache.put(
|
||||||
|
READY_KEY,
|
||||||
|
new Response(JSON.stringify(meta), {
|
||||||
|
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (onProgress) onProgress({ done: true, cached: false, ...meta });
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 切回在线模式:删掉缓存(含标记),SW 随即恢复纯转发。返回之前是否处于离线模式。 */
|
||||||
|
export async function deactivateOffline() {
|
||||||
|
const had = await readStatus();
|
||||||
|
await caches.delete(CACHE_NAME);
|
||||||
|
return Boolean(had);
|
||||||
|
}
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
layout: default
|
||||||
|
title: 离线模式
|
||||||
|
---
|
||||||
|
|
||||||
|
# 离线模式
|
||||||
|
|
||||||
|
本页用于管理本博客的离线阅读功能:
|
||||||
|
|
||||||
|
- **启用离线模式**:浏览器会下载整站压缩包(`MayxBlog.7z`)并解包到本地缓存,之后即使没有网络也能正常浏览博客;
|
||||||
|
- **自动回退**:当站点压缩包更新(博客重新部署)时,离线缓存会自动失效并回退到在线模式,下次重新启用即可获取最新内容;
|
||||||
|
- **手动切换**:离线模式下可以随时在本页切回在线模式,切回后页面将重新通过网络加载。
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr><th>当前状态</th><td id="offline-state">检测中…</td></tr>
|
||||||
|
<tr><th>缓存版本</th><td id="offline-version">-</td></tr>
|
||||||
|
<tr><th>文件统计</th><td id="offline-files">-</td></tr>
|
||||||
|
<tr><th>解包时间</th><td id="offline-time">-</td></tr>
|
||||||
|
<tr><th>持久存储</th><td id="offline-persist">-</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p id="offline-progress-box" hidden="hidden">
|
||||||
|
<progress id="offline-progress" max="100" value="0"></progress>
|
||||||
|
|
||||||
|
<small id="offline-progress-text">准备中…</small>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p><strong id="offline-message"></strong></p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<button type="button" id="offline-enable">启用离线模式</button>
|
||||||
|
<button type="button" id="offline-disable">切换回在线模式</button>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<script src="/assets/js/offline-page.js"></script>
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
/*
|
||||||
|
* Mayx的博客 · 离线模式 Service Worker
|
||||||
|
*
|
||||||
|
* 两种模式(由 Cache Storage 中是否存在就绪标记决定):
|
||||||
|
* - 在线模式(默认,无标记):所有请求原样转发网络,SW 形同虚设。
|
||||||
|
* - 离线模式(/offline.html 启用后写入标记):同源 GET 请求缓存优先,
|
||||||
|
* 缓存未命中且清单里没有的路径才回源网络,实现断网阅读。
|
||||||
|
*
|
||||||
|
* 版本控制:压缩包 /MayxBlog.7z 的 content-length 即版本号(每次重新部署
|
||||||
|
* 都会变化)。SW 激活时、以及导航请求按 RECHECK_INTERVAL 节流复核;
|
||||||
|
* 一旦发现线上压缩包与已缓存版本不一致,立即清空缓存 → 自动回退在线模式。
|
||||||
|
* 网络不可用时复核失败,保留缓存(离线阅读本来就是离线模式的目的)。
|
||||||
|
*/
|
||||||
|
|
||||||
|
const CACHE_NAME = 'mayxblog-offline-v1';
|
||||||
|
const READY_KEY = '/__offline_manifest__';
|
||||||
|
const ARCHIVE_URL = '/MayxBlog.7z';
|
||||||
|
const RECHECK_INTERVAL = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
let lastRecheck = 0;
|
||||||
|
|
||||||
|
self.addEventListener('install', () => {
|
||||||
|
self.skipWaiting();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('activate', (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.keys()
|
||||||
|
.then((keys) =>
|
||||||
|
Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
|
||||||
|
)
|
||||||
|
.then(verifyPayloadVersion)
|
||||||
|
.then(() => self.clients.claim())
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* HEAD 请求不被下面的 fetch 处理器拦截,这里读到的一定是线上真实压缩包,
|
||||||
|
* 而不是缓存里的 quine 副本。网络失败返回 null,调用方选择信任现有缓存。
|
||||||
|
*/
|
||||||
|
async function payloadSize() {
|
||||||
|
try {
|
||||||
|
const response = await fetch(ARCHIVE_URL, { method: 'HEAD', cache: 'no-store' });
|
||||||
|
if (!response.ok) return null;
|
||||||
|
const size = Number(response.headers.get('content-length'));
|
||||||
|
return Number.isFinite(size) && size > 0 ? size : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 就绪标记同时是清单:记录了解包写入的所有路径。没有它就无法区分
|
||||||
|
* “站点本来就没有这个页面”和“缓存条目被浏览器回收了”。
|
||||||
|
*/
|
||||||
|
async function readManifest(cache) {
|
||||||
|
const hit = await cache.match(READY_KEY);
|
||||||
|
if (!hit) return null;
|
||||||
|
const meta = await hit.json().catch(() => null);
|
||||||
|
if (!meta || !Array.isArray(meta.keys) || !meta.keys.length) return null;
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 压缩包更新 → 清空缓存(即回退在线模式);返回是否发生了失效。 */
|
||||||
|
async function verifyPayloadVersion() {
|
||||||
|
const cache = await caches.open(CACHE_NAME);
|
||||||
|
const meta = await readManifest(cache);
|
||||||
|
if (!meta) return false;
|
||||||
|
const size = await payloadSize();
|
||||||
|
if (size && meta.payloadSize !== size) {
|
||||||
|
await caches.delete(CACHE_NAME);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 服务器对目录索引的处理与 Jekyll 产物一致,这里补齐候选路径。 */
|
||||||
|
function candidatesFor(pathname) {
|
||||||
|
const keys = [pathname];
|
||||||
|
if (pathname.endsWith('/')) {
|
||||||
|
keys.push(pathname + 'index.html');
|
||||||
|
} else {
|
||||||
|
keys.push(pathname + '/index.html');
|
||||||
|
keys.push(pathname + '.html');
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.addEventListener('fetch', (event) => {
|
||||||
|
const request = event.request;
|
||||||
|
if (request.method !== 'GET') return; // HEAD 等直通网络(版本探测依赖这一点)
|
||||||
|
|
||||||
|
const url = new URL(request.url);
|
||||||
|
if (url.origin !== self.location.origin) return; // 跨域请求不干预
|
||||||
|
|
||||||
|
// 显式 no-store 的请求(重新下载压缩包等)绕过缓存,避免吃到旧的 quine 副本
|
||||||
|
if (request.cache === 'no-store') return;
|
||||||
|
|
||||||
|
event.respondWith(respond(request, url, event));
|
||||||
|
});
|
||||||
|
|
||||||
|
async function respond(request, url, event) {
|
||||||
|
const cache = await caches.open(CACHE_NAME);
|
||||||
|
|
||||||
|
// 导航请求顺带做节流版本复核;失效发生在后台,本次响应不受影响,
|
||||||
|
// 下一次请求自然落入在线模式。
|
||||||
|
if (request.mode === 'navigate' && Date.now() - lastRecheck > RECHECK_INTERVAL) {
|
||||||
|
lastRecheck = Date.now();
|
||||||
|
event.waitUntil(verifyPayloadVersion());
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidates = candidatesFor(url.pathname);
|
||||||
|
for (const key of candidates) {
|
||||||
|
const hit = await cache.match(key);
|
||||||
|
if (hit) return hit;
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = await readManifest(cache);
|
||||||
|
if (!meta) return fetch(request); // 在线模式:纯转发
|
||||||
|
|
||||||
|
// 清单承诺过这个路径但缓存里没有 → 条目被浏览器回收,缓存已不可信,
|
||||||
|
// 整体作废并回退在线模式。
|
||||||
|
if (candidates.some((key) => meta.keys.includes(key))) {
|
||||||
|
await caches.delete(CACHE_NAME);
|
||||||
|
return fetch(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清单之外的路径回源网络;断网时的导航请求用站点 404 页兜底。
|
||||||
|
try {
|
||||||
|
return await fetch(request);
|
||||||
|
} catch (error) {
|
||||||
|
if (request.mode === 'navigate') {
|
||||||
|
const notFound = await cache.match('/404.html');
|
||||||
|
if (notFound) {
|
||||||
|
return new Response(notFound.body, { status: 404, headers: notFound.headers });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+21
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018 ნიკა
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
var libarchive = (function() {
|
||||||
|
var _scriptDir = typeof document !== 'undefined' && document.currentScript ? document.currentScript.src : undefined;
|
||||||
|
return (
|
||||||
|
function(libarchive) {
|
||||||
|
libarchive = libarchive || {};
|
||||||
|
|
||||||
|
var Module=typeof libarchive!=="undefined"?libarchive:{};var moduleOverrides={};var key;for(key in Module){if(Module.hasOwnProperty(key)){moduleOverrides[key]=Module[key]}}Module["arguments"]=[];Module["thisProgram"]="./this.program";Module["quit"]=function(status,toThrow){throw toThrow};Module["preRun"]=[];Module["postRun"]=[];var ENVIRONMENT_IS_WEB=false;var ENVIRONMENT_IS_WORKER=false;var ENVIRONMENT_IS_NODE=false;var ENVIRONMENT_IS_SHELL=false;ENVIRONMENT_IS_WEB=typeof window==="object";ENVIRONMENT_IS_WORKER=typeof importScripts==="function";ENVIRONMENT_IS_NODE=typeof process==="object"&&typeof require==="function"&&!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_WORKER;ENVIRONMENT_IS_SHELL=!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_NODE&&!ENVIRONMENT_IS_WORKER;var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}else{return scriptDirectory+path}}if(ENVIRONMENT_IS_NODE){scriptDirectory=__dirname+"/";var nodeFS;var nodePath;Module["read"]=function shell_read(filename,binary){var ret;if(!nodeFS)nodeFS=require("fs");if(!nodePath)nodePath=require("path");filename=nodePath["normalize"](filename);ret=nodeFS["readFileSync"](filename);return binary?ret:ret.toString()};Module["readBinary"]=function readBinary(filename){var ret=Module["read"](filename,true);if(!ret.buffer){ret=new Uint8Array(ret)}assert(ret.buffer);return ret};if(process["argv"].length>1){Module["thisProgram"]=process["argv"][1].replace(/\\/g,"/")}Module["arguments"]=process["argv"].slice(2);process["on"]("uncaughtException",function(ex){if(!(ex instanceof ExitStatus)){throw ex}});process["on"]("unhandledRejection",abort);Module["quit"]=function(status){process["exit"](status)};Module["inspect"]=function(){return"[Emscripten Module object]"}}else if(ENVIRONMENT_IS_SHELL){if(typeof read!="undefined"){Module["read"]=function shell_read(f){return read(f)}}Module["readBinary"]=function readBinary(f){var data;if(typeof readbuffer==="function"){return new Uint8Array(readbuffer(f))}data=read(f,"binary");assert(typeof data==="object");return data};if(typeof scriptArgs!="undefined"){Module["arguments"]=scriptArgs}else if(typeof arguments!="undefined"){Module["arguments"]=arguments}if(typeof quit==="function"){Module["quit"]=function(status){quit(status)}}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptDir){scriptDirectory=_scriptDir}if(scriptDirectory.indexOf("blob:")!==0){scriptDirectory=scriptDirectory.substr(0,scriptDirectory.lastIndexOf("/")+1)}else{scriptDirectory=""}Module["read"]=function shell_read(url){var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.send(null);return xhr.responseText};if(ENVIRONMENT_IS_WORKER){Module["readBinary"]=function readBinary(url){var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}Module["readAsync"]=function readAsync(url,onload,onerror){var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=function xhr_onload(){if(xhr.status==200||xhr.status==0&&xhr.response){onload(xhr.response);return}onerror()};xhr.onerror=onerror;xhr.send(null)};Module["setWindowTitle"]=function(title){document.title=title}}else{}var out=Module["print"]||(typeof console!=="undefined"?console.log.bind(console):typeof print!=="undefined"?print:null);var err=Module["printErr"]||(typeof printErr!=="undefined"?printErr:typeof console!=="undefined"&&console.warn.bind(console)||out);for(key in moduleOverrides){if(moduleOverrides.hasOwnProperty(key)){Module[key]=moduleOverrides[key]}}moduleOverrides=undefined;function dynamicAlloc(size){var ret=HEAP32[DYNAMICTOP_PTR>>2];var end=ret+size+15&-16;if(end<=_emscripten_get_heap_size()){HEAP32[DYNAMICTOP_PTR>>2]=end}else{var success=_emscripten_resize_heap(end);if(!success)return 0}return ret}function getNativeTypeSize(type){switch(type){case"i1":case"i8":return 1;case"i16":return 2;case"i32":return 4;case"i64":return 8;case"float":return 4;case"double":return 8;default:{if(type[type.length-1]==="*"){return 4}else if(type[0]==="i"){var bits=parseInt(type.substr(1));assert(bits%8===0,"getNativeTypeSize invalid bits "+bits+", type "+type);return bits/8}else{return 0}}}}var asm2wasmImports={"f64-rem":function(x,y){return x%y},"debugger":function(){debugger}};var functionPointers=new Array(0);var tempRet0=0;var setTempRet0=function(value){tempRet0=value};if(typeof WebAssembly!=="object"){err("no native wasm support detected")}var wasmMemory;var wasmTable;var ABORT=false;var EXITSTATUS=0;function assert(condition,text){if(!condition){abort("Assertion failed: "+text)}}function getCFunc(ident){var func=Module["_"+ident];assert(func,"Cannot call unknown function "+ident+", make sure it is exported");return func}function ccall(ident,returnType,argTypes,args,opts){var toC={"string":function(Line truncated
|
||||||
|
|
||||||
|
|
||||||
|
return libarchive
|
||||||
|
}
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
export default libarchive;
|
||||||
BIN
Binary file not shown.
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
!function(){"use strict";const e={32768:"FILE",16384:"DIR",40960:"SYMBOLIC_LINK",49152:"SOCKET",8192:"CHARACTER_DEVICE",24576:"BLOCK_DEVICE",4096:"NAMED_PIPE"};class r{constructor(e){this._wasmModule=e,this._runCode=e.runCode,this._file=null,this._passphrase=null}open(e){null!==this._file&&(console.warn("Closing previous file"),this.close());const{promise:r,resolve:t,reject:n}=this._promiseHandles();this._file=e;const o=new FileReader;return o.onload=(()=>this._loadFile(o.result,t,n)),o.readAsArrayBuffer(e),r}close(){this._runCode.closeArchive(this._archive),this._wasmModule._free(this._filePtr),this._file=null,this._filePtr=null,this._archive=null}hasEncryptedData(){this._archive=this._runCode.openArchive(this._filePtr,this._fileLength,this._passphrase),this._runCode.getNextEntry(this._archive);const e=this._runCode.hasEncryptedEntries(this._archive);return 0!==e&&(e>0||null)}setPassphrase(e){this._passphrase=e}*entries(r=!1,t=null){let n;for(this._archive=this._runCode.openArchive(this._filePtr,this._fileLength,this._passphrase);0!==(n=this._runCode.getNextEntry(this._archive));){const o={size:this._runCode.getEntrySize(n),path:this._runCode.getEntryName(n),type:e[this._runCode.getEntryType(n)],ref:n};if("FILE"===o.type){let e=o.path.split("/");o.fileName=e[e.length-1]}if(r&&t!==o.path)this._runCode.skipEntry(this._archive);else{const e=this._runCode.getFileData(this._archive,o.size);if(e<0)throw new Error(this._runCode.getError(this._archive));o.fileData=this._wasmModule.HEAP8.slice(e,e+o.size),this._wasmModule._free(e)}yield o}}_loadFile(e,r,t){try{const n=new Uint8Array(e);this._fileLength=n.length,this._filePtr=this._runCode.malloc(this._fileLength),this._wasmModule.HEAP8.set(n,this._filePtr),r()}catch(e){t(e)}}_promiseHandles(){let e=null,r=null;return{promise:new Promise((t,n)=>{e=t,r=n}),resolve:e,reject:r}}}var t,n=(t="undefined"!=typeof document&&document.currentScript?document.currentScript.src:void 0,function(e){var r,n=void 0!==(e=e||{})?e:{},o={};for(r in n)n.hasOwnProperty(r)&&(o[r]=n[r]);n.arguments=[],n.thisProgram="./this.program",n.quit=function(e,r){throw r},n.preRun=[],n.postRun=[];var i,a,s=!1,u=!1;s="object"==typeof window,u="function"==typeof importScripts,i="object"==typeof process&&"function"==typeof require&&!s&&!u,a=!s&&!i&&!u;var c,f,l="";i?(l=__dirname+"/",n.read=function(e,r){var t;return c||(c=require("fs")),f||(f=require("path")),e=f.normalize(e),t=c.readFileSync(e),r?t:t.toString()},n.readBinary=function(e){var r=n.read(e,!0);return r.buffer||(r=new Uint8Array(r)),E(r.buffer),r},process.argv.length>1&&(n.thisProgram=process.argv[1].replace(/\\/g,"/")),n.arguments=process.argv.slice(2),process.on("uncaughtException",function(e){if(!(e instanceof qe))throw e}),process.on("unhandledRejection",Ze),n.quit=function(e){process.exit(e)},n.inspect=function(){return"[Emscripten Module object]"}):a?("undefined"!=typeof read&&(n.read=function(e){return read(e)}),n.readBinary=function(e){var r;return"function"==typeof readbuffer?new Uint8Array(readbuffer(e)):(E("object"==typeof(r=read(e,"binary"))),r)},"undefined"!=typeof scriptArgs?n.arguments=scriptArgs:void 0!==arguments&&(n.arguments=arguments),"function"==typeof quit&&(n.quit=function(e){quit(e)})):(s||u)&&(u?l=self.location.href:document.currentScript&&(l=document.currentScript.src),t&&(l=t),l=0!==l.indexOf("blob:")?l.substr(0,l.lastIndexOf("/")+1):"",n.read=function(e){var r=new XMLHttpRequest;return r.open("GET",e,!1),r.send(null),r.responseText},u&&(n.readBinary=function(e){var r=new XMLHttpRequest;return r.open("GET",e,!1),r.responseType="arraybuffer",r.send(null),new Uint8Array(r.response)}),n.readAsync=function(e,r,t){var n=new XMLHttpRequest;n.open("GET",e,!0),n.responseType="arraybuffer",n.onload=function(){200==n.status||0==n.status&&n.response?r(n.response):t()},n.onerror=t,n.send(null)},n.setWindowTitle=function(e){document.title=e});var d=n.print||("undefined"!=typeof console?console.log.bind(console):"undefined"!=typeof print?print:null),p=n.printErr||("undefined"!=typeof printErr?printErr:"undefined"!=typeof console&&console.warn.bind(console)||d);for(r in o)o.hasOwnProperty(r)&&(n[r]=o[r]);function m(e){var r=M[q>>2],t=r+e+15&-16;if(t<=Ae())M[q>>2]=t;else if(!Re(t))return 0;return r}function h(e){switch(e){case"i1":case"i8":return 1;case"i16":return 2;case"i32":return 4;case"i64":return 8;case"float":return 4;case"double":return 8;default:if("*"===e[e.length-1])return 4;if("i"===e[0]){var r=parseInt(e.substr(1));return E(r%8==0,"getNativeTypeSize invalid bits "+r+", type "+e),r/8}return 0}}o=void 0;var v,w={"f64-rem":function(e,r){return e%r},debugger:function(){}};"object"!=typeof WebAssembly&&p("no native wasm support detected");var y=!1;function E(e,r){e||Ze("Assertion failed: "+r)}function g(e){var r=n["_"+e];return E(r,"Cannot call unknown function "+e+", make sure it is exported"),r}function _(e,r,t,n,o){var i={string:function(e){var r=0;if(null!=e&&0!==e){var t=1+(e.length<<2);C(e,r=je(t),t)}return r},arrayLine truncated
|
||||||
Reference in new issue
Block a user