Web 端本地缓存、首页与会员弹窗体验优化
新增 IndexedDB 与最近文件 L1 缓存、云保存/打开链路;首页支持下载与清空本地缓存;修复清空 storage 后语言初始化报错;优化 design2 背景加载与侧栏标签样式;完善会员激活/支付弹层样式;后端补充文件缩略图字段与 thumb 接口。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -38,4 +38,122 @@
|
||||
var n = String(rel || '').replace(/^\/+/, '');
|
||||
return window.SOON_JS_BASE + n;
|
||||
};
|
||||
|
||||
/** 等 HTMLImage 解码完成后再 toDataURL / setSrc,避免间歇性空白背景 */
|
||||
window.soonFabricImageWhenReady = function (image, cb, skipEmbed) {
|
||||
if (!image) {
|
||||
if (typeof cb === 'function') cb(null);
|
||||
return;
|
||||
}
|
||||
function finalize() {
|
||||
if (skipEmbed) {
|
||||
if (typeof cb === 'function') cb(image);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
var dataUrl = image.toDataURL();
|
||||
if (dataUrl && dataUrl.length > 500) {
|
||||
image.setSrc(dataUrl, function (img) {
|
||||
if (typeof cb === 'function') cb(img || image);
|
||||
});
|
||||
return;
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
if (typeof cb === 'function') cb(image);
|
||||
}
|
||||
var el = image.getElement ? image.getElement() : null;
|
||||
if (!el) {
|
||||
finalize();
|
||||
return;
|
||||
}
|
||||
if (el.complete && el.naturalWidth > 0) {
|
||||
finalize();
|
||||
return;
|
||||
}
|
||||
el.onload = function () { finalize(); };
|
||||
el.onerror = function () {
|
||||
if (typeof cb === 'function') cb(null);
|
||||
};
|
||||
};
|
||||
|
||||
window.soonFabricImageFromAsset = function (assetName, onReady, onFail, retryLeft, skipEmbed) {
|
||||
if (typeof fabric === 'undefined' || !fabric.Image) {
|
||||
if (typeof onFail === 'function') onFail();
|
||||
return;
|
||||
}
|
||||
var retries = retryLeft == null ? 2 : retryLeft;
|
||||
var url = window.soonAsset(assetName);
|
||||
fabric.Image.fromURL(url, function (image) {
|
||||
if (!image || !image.width) {
|
||||
if (retries > 0) {
|
||||
setTimeout(function () {
|
||||
window.soonFabricImageFromAsset(assetName, onReady, onFail, retries - 1, skipEmbed);
|
||||
}, 150);
|
||||
return;
|
||||
}
|
||||
if (typeof onFail === 'function') onFail();
|
||||
return;
|
||||
}
|
||||
function deliver(img) {
|
||||
if (img && typeof onReady === 'function') onReady(img);
|
||||
else if (retries > 0) {
|
||||
setTimeout(function () {
|
||||
window.soonFabricImageFromAsset(assetName, onReady, onFail, retries - 1, skipEmbed);
|
||||
}, 150);
|
||||
} else if (typeof onFail === 'function') onFail();
|
||||
}
|
||||
if (skipEmbed) {
|
||||
window.soonFabricImageWhenReady(image, deliver, true);
|
||||
return;
|
||||
}
|
||||
window.soonFabricImageWhenReady(image, deliver);
|
||||
}, null, { crossOrigin: 'anonymous' });
|
||||
};
|
||||
|
||||
window.soonRunWhenCanvasReady = function (selector, fn) {
|
||||
function tryRun() {
|
||||
var el = typeof selector === 'string' ? document.querySelector(selector) : selector;
|
||||
var w = el ? (el.clientWidth || el.width || 0) : 0;
|
||||
if ((!w || w < 50) && window.SOON_DEPLOY_CONFIG) {
|
||||
requestAnimationFrame(function () {
|
||||
requestAnimationFrame(tryRun);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (typeof fn === 'function') fn(w || 800);
|
||||
}
|
||||
tryRun();
|
||||
};
|
||||
|
||||
var SOON_LOCALE_CODES = { zh: 1, ozh: 1, en: 1 };
|
||||
|
||||
window.soonNormalizeLocaleCode = function (loc) {
|
||||
if (!loc) return 'zh';
|
||||
if (SOON_LOCALE_CODES[loc]) return loc;
|
||||
var s = String(loc);
|
||||
if (s.indexOf('zh') === 0) return s.indexOf('TW') >= 0 ? 'ozh' : 'zh';
|
||||
return 'en';
|
||||
};
|
||||
|
||||
window.soonResolveLocale = function (cb) {
|
||||
if (typeof cb !== 'function') return;
|
||||
var stored;
|
||||
try { stored = localStorage.getItem('lang'); } catch (e) { stored = null; }
|
||||
if (stored && SOON_LOCALE_CODES[stored]) {
|
||||
cb(stored);
|
||||
return;
|
||||
}
|
||||
var bridge = window.platformBridge;
|
||||
if (!bridge || typeof bridge.getLocale !== 'function') {
|
||||
cb('zh');
|
||||
return;
|
||||
}
|
||||
var ret;
|
||||
try { ret = bridge.getLocale(); } catch (e) { cb('zh'); return; }
|
||||
if (ret && typeof ret.then === 'function') {
|
||||
ret.then(function (loc) { cb(window.soonNormalizeLocaleCode(loc)); }).catch(function () { cb('zh'); });
|
||||
return;
|
||||
}
|
||||
cb(window.soonNormalizeLocaleCode(ret));
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -39,18 +39,243 @@
|
||||
return FILE_PREFIX + id + ':v' + version;
|
||||
}
|
||||
|
||||
function soonIsTemplateKey(key) {
|
||||
return !!(key && typeof key === 'string' && key.indexOf(TEMPLATE_PREFIX) === 0);
|
||||
}
|
||||
|
||||
function soonNeedsSaveDialog(key) {
|
||||
return !key || soonIsTemplateKey(key);
|
||||
}
|
||||
|
||||
function soonDefaultNewSoonName() {
|
||||
var d = new Date();
|
||||
function pad(n) { return n < 10 ? '0' + n : String(n); }
|
||||
var yy = String(d.getFullYear()).slice(-2);
|
||||
var stamp = yy + pad(d.getMonth() + 1) + pad(d.getDate()) +
|
||||
pad(d.getHours()) + pad(d.getMinutes()) + pad(d.getSeconds());
|
||||
return 'design' + stamp + '.soon';
|
||||
}
|
||||
|
||||
function soonEnsureSoonExt(name) {
|
||||
var n = String(name || 'design.soon').trim();
|
||||
if (!/\.soon$/i.test(n)) n = (n.replace(/\.soon$/i, '') || 'design') + '.soon';
|
||||
return n;
|
||||
}
|
||||
|
||||
function soonSessionSlug(nameOrKey) {
|
||||
var s = String(nameOrKey || 'design').replace(/^soondesign_session:/, '').split(/[/\\]/).pop();
|
||||
s = s.replace(/\.soon$/i, '');
|
||||
s = s.replace(/-\d{10,}$/, '');
|
||||
return s || 'design';
|
||||
}
|
||||
|
||||
function soonMakeSessionKey(nameOrKey) {
|
||||
return 'soondesign_session:' + soonSessionSlug(nameOrKey);
|
||||
}
|
||||
|
||||
function soonSessionDisplayName(keyOrName) {
|
||||
return soonEnsureSoonExt(soonSessionSlug(keyOrName));
|
||||
}
|
||||
|
||||
function soonBuildSavePreviewPic(canvas) {
|
||||
var THUMB_MAX = 102400;
|
||||
if (!canvas || typeof canvas.getObjects !== 'function') return '';
|
||||
var hidden = [];
|
||||
canvas.getObjects().forEach(function (obj) {
|
||||
if (obj.isGuideLine) {
|
||||
hidden.push(obj);
|
||||
obj.visible = false;
|
||||
}
|
||||
});
|
||||
if (hidden.length) canvas.renderAll();
|
||||
var mult = 0.18;
|
||||
var quality = 0.72;
|
||||
var url = '';
|
||||
for (var i = 0; i < 3; i++) {
|
||||
try {
|
||||
var candidate = canvas.toDataURL({ format: 'jpeg', quality: quality, multiplier: mult });
|
||||
if (candidate && candidate.indexOf('data:image/') === 0 && candidate.length <= THUMB_MAX) {
|
||||
url = candidate;
|
||||
break;
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
quality -= 0.18;
|
||||
mult -= 0.04;
|
||||
}
|
||||
hidden.forEach(function (obj) { obj.visible = true; });
|
||||
if (hidden.length) canvas.renderAll();
|
||||
return url;
|
||||
}
|
||||
|
||||
function soonApplySavePreview(con_o, canvas) {
|
||||
if (!con_o) return con_o;
|
||||
var pic = soonBuildSavePreviewPic(canvas);
|
||||
if (pic) con_o.frontDisplayPic = pic;
|
||||
return con_o;
|
||||
}
|
||||
|
||||
function soonSaveDialogDefaultPath(openKey) {
|
||||
if (soonNeedsSaveDialog(openKey)) return soonDefaultNewSoonName();
|
||||
return openKey || undefined;
|
||||
}
|
||||
|
||||
function soonEnsureSaveFileName(fp) {
|
||||
var name = String(fp || 'design.soon');
|
||||
if (typeof window !== 'undefined' && window.platformBridge && window.fs == null) {
|
||||
return soonEnsureSoonExt(name);
|
||||
}
|
||||
if (typeof window !== 'undefined' && window.path && window.path.extname) {
|
||||
if (window.path.extname(name) !== '.soon') {
|
||||
name = (name.replace(/\.soon$/i, '') || 'design') + '.soon';
|
||||
}
|
||||
} else if (!/\.soon$/i.test(name)) {
|
||||
name = (name.replace(/\.soon$/i, '') || 'design') + '.soon';
|
||||
}
|
||||
return soonEnsureSoonExt(name);
|
||||
}
|
||||
|
||||
function soonReportSaveError(err) {
|
||||
if (typeof window.soonShowApiError === 'function') {
|
||||
window.soonShowApiError({
|
||||
status: err && err.status,
|
||||
message: (err && err.message) || '保存失败',
|
||||
code: err && err.code
|
||||
});
|
||||
} else {
|
||||
soonToast((err && err.message) || '保存失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function soonWriteSoonContentWeb(fp, content, prevOpenKey, onDone, onError) {
|
||||
if (!window.platformBridge || !window.platformBridge.writeFile) return false;
|
||||
window.platformBridge.writeFile(fp, content).then(function (res) {
|
||||
if (typeof onDone === 'function') onDone(res, prevOpenKey);
|
||||
}).catch(function (err) {
|
||||
if (typeof onError === 'function') onError(err);
|
||||
else soonReportSaveError(err);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function soonDownloadBlob(filename, content, mime) {
|
||||
var name = soonEnsureSoonExt(filename || 'design.soon');
|
||||
var blob = content instanceof Blob
|
||||
? content
|
||||
: new Blob([content], { type: mime || 'application/json' });
|
||||
var a = document.createElement('a');
|
||||
a.download = name;
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.click();
|
||||
URL.revokeObjectURL(a.href);
|
||||
}
|
||||
|
||||
function soonDownloadRecentItem(item) {
|
||||
if (!item) return Promise.resolve();
|
||||
var key = item.filePath || item.key || '';
|
||||
var name = soonEnsureSoonExt(item.name || 'design.soon');
|
||||
var kind = item.kind || (typeof window.soonRecentKindFromKey === 'function'
|
||||
? window.soonRecentKindFromKey(key) : 'local');
|
||||
|
||||
if (kind === 'template') {
|
||||
var tpl = soonParseTemplateKey(key);
|
||||
if (!tpl) {
|
||||
soonToast('无法下载模板', 'error');
|
||||
return Promise.resolve();
|
||||
}
|
||||
var base = (window.SOON_DEPLOY_CONFIG && window.SOON_DEPLOY_CONFIG.api_v1_base) || '';
|
||||
if (!base) {
|
||||
soonToast('下载地址未配置', 'error');
|
||||
return Promise.resolve();
|
||||
}
|
||||
return fetch(base + '/templates/' + tpl.id + '/file', { headers: { Accept: 'application/json' } })
|
||||
.then(function (r) {
|
||||
if (!r.ok) throw new Error('download_failed');
|
||||
return r.text();
|
||||
})
|
||||
.then(function (text) {
|
||||
soonDownloadBlob(name, text, 'application/json');
|
||||
})
|
||||
.catch(function () {
|
||||
soonToast('模板下载失败', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
if (kind === 'cloud') {
|
||||
function tryCloudApiDownload() {
|
||||
if (!item.fileId || !soonGetAccessToken()) return Promise.resolve(false);
|
||||
var bridge = window.platformBridge;
|
||||
if (!bridge || typeof bridge.downloadCloudFile !== 'function') return Promise.resolve(false);
|
||||
return bridge.downloadCloudFile(item.fileId, name).then(function () { return true; }).catch(function () {
|
||||
soonToast('下载失败', 'error');
|
||||
return true;
|
||||
});
|
||||
}
|
||||
if (key && typeof window.soonLocalGet === 'function') {
|
||||
return window.soonLocalGet(key).then(function (hit) {
|
||||
if (hit && hit.json) {
|
||||
soonDownloadBlob(name, JSON.stringify(hit.json), 'application/json');
|
||||
return;
|
||||
}
|
||||
return tryCloudApiDownload().then(function (done) {
|
||||
if (!done) soonToast('请先打开或保存该文件', 'warn');
|
||||
});
|
||||
});
|
||||
}
|
||||
return tryCloudApiDownload().then(function (done) {
|
||||
if (!done) soonToast('请先打开或保存该文件', 'warn');
|
||||
});
|
||||
}
|
||||
|
||||
if (key.indexOf('soondesign_session:') === 0 || kind === 'local') {
|
||||
if (typeof window.soonLocalGet !== 'function') {
|
||||
soonToast('本地缓存不可用', 'error');
|
||||
return Promise.resolve();
|
||||
}
|
||||
return window.soonLocalGet(key).then(function (hit) {
|
||||
if (!hit || !hit.json) {
|
||||
soonToast('请先打开或保存该文件', 'warn');
|
||||
return;
|
||||
}
|
||||
soonDownloadBlob(name, JSON.stringify(hit.json), 'application/json');
|
||||
});
|
||||
}
|
||||
|
||||
soonToast('无法下载该文件', 'warn');
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function soonLookupRecentCloudName(fileId) {
|
||||
if (!fileId || typeof window.soonRecentList !== 'function') return '';
|
||||
var items = window.soonRecentList();
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
if (items[i].fileId === fileId && items[i].name) return items[i].name;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function soonResolveCloudFileName(fileKey) {
|
||||
if (!fileKey || fileKey.indexOf(FILE_PREFIX) !== 0) return '';
|
||||
var parsed = soonParseFileKey(fileKey);
|
||||
if (!parsed) return '';
|
||||
var meta = window._soonFileMeta;
|
||||
if (meta && meta.id === parsed.id && meta.name) return meta.name;
|
||||
var recentName = soonLookupRecentCloudName(parsed.id);
|
||||
if (recentName) return recentName;
|
||||
return '';
|
||||
}
|
||||
|
||||
function soonNormalizeSoonName(pathOrName) {
|
||||
var n = String(pathOrName || 'design.soon');
|
||||
if (n.indexOf(FILE_PREFIX) === 0) {
|
||||
var meta = window._soonFileMeta;
|
||||
return (meta && meta.name) ? meta.name : 'design.soon';
|
||||
var resolved = soonResolveCloudFileName(n);
|
||||
if (resolved) return resolved;
|
||||
return 'design.soon';
|
||||
}
|
||||
if (n.indexOf(TEMPLATE_PREFIX) === 0) {
|
||||
var tpl = soonParseTemplateKey(n);
|
||||
return tpl ? ('template-' + tpl.id + '.soon') : 'template.soon';
|
||||
return soonDefaultNewSoonName();
|
||||
}
|
||||
if (n.indexOf('soondesign_session:') === 0) {
|
||||
n = n.replace(/^soondesign_session:/, '');
|
||||
return soonSessionDisplayName(n);
|
||||
}
|
||||
n = n.split(/[/\\]/).pop();
|
||||
if (!/\.soon$/i.test(n)) n = (n.replace(/\.soon$/i, '') || 'design') + '.soon';
|
||||
@@ -128,10 +353,12 @@
|
||||
var parsed = soonParseFileKey(fileKey);
|
||||
if (!parsed) return;
|
||||
var prev = window._soonFileMeta || {};
|
||||
var name = (opts && opts.name) || (prev.id === parsed.id ? prev.name : '') || '';
|
||||
if (!name) name = soonLookupRecentCloudName(parsed.id);
|
||||
window._soonFileMeta = {
|
||||
id: parsed.id,
|
||||
version: parsed.version != null ? parsed.version : prev.version,
|
||||
name: (opts && opts.name) || (prev.id === parsed.id ? prev.name : '') || ''
|
||||
name: name
|
||||
};
|
||||
}
|
||||
|
||||
@@ -301,11 +528,15 @@
|
||||
|
||||
function soonPutSoonSession(j, fileName) {
|
||||
if (!j) return '';
|
||||
var hint = (fileName || 'design').replace(/\.soon$/i, '');
|
||||
var key = 'soondesign_session:' + hint + '-' + Date.now();
|
||||
var displayName = soonEnsureSoonExt(fileName || 'design.soon');
|
||||
var key = soonMakeSessionKey(displayName);
|
||||
if (typeof window.soonLocalCacheAndThumb === 'function') {
|
||||
window.soonLocalCacheAndThumb(key, j, { source: 'session', name: displayName });
|
||||
try { sessionStorage.setItem(key, 'idb'); } catch (e) { /* ignore */ }
|
||||
return key;
|
||||
}
|
||||
try {
|
||||
sessionStorage.setItem(key, JSON.stringify(j));
|
||||
try { localStorage.setItem(key, JSON.stringify(j)); } catch (e2) { /* ignore quota */ }
|
||||
return key;
|
||||
} catch (e) {
|
||||
return '';
|
||||
@@ -318,6 +549,9 @@
|
||||
soonToast('无法打开文件(存储空间不足)', 'error');
|
||||
return '';
|
||||
}
|
||||
if (typeof window.soonRecentUpsertFromOpen === 'function') {
|
||||
window.soonRecentUpsertFromOpen(key, j);
|
||||
}
|
||||
soonScheduleCloudImport(key, fileName);
|
||||
var type = soonSoonTypeFromJson(j);
|
||||
if (window.platformBridge && window.platformBridge.openDesignPage) {
|
||||
@@ -369,25 +603,55 @@
|
||||
if (!pending || pending.sessionKey !== currentFileKey) return;
|
||||
try { sessionStorage.removeItem(PENDING_IMPORT_KEY); } catch (e) { /* ignore */ }
|
||||
|
||||
var jsonRaw;
|
||||
try {
|
||||
jsonRaw = sessionStorage.getItem(currentFileKey);
|
||||
if (!jsonRaw && typeof localStorage !== 'undefined') jsonRaw = localStorage.getItem(currentFileKey);
|
||||
} catch (e) {
|
||||
return;
|
||||
function loadJsonForImport() {
|
||||
if (typeof window.soonLocalGet === 'function') {
|
||||
return window.soonLocalGet(currentFileKey).then(function (hit) {
|
||||
if (hit && hit.json) return hit.json;
|
||||
try {
|
||||
var raw = sessionStorage.getItem(currentFileKey);
|
||||
if (raw && raw !== 'idb') return JSON.parse(raw);
|
||||
} catch (e) { /* ignore */ }
|
||||
return null;
|
||||
});
|
||||
}
|
||||
try {
|
||||
var raw = sessionStorage.getItem(currentFileKey);
|
||||
if (raw && raw !== 'idb') return JSON.parse(raw);
|
||||
} catch (e) { /* ignore */ }
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
if (!jsonRaw) return;
|
||||
|
||||
var bridge = window.platformBridge;
|
||||
if (!bridge || typeof bridge.importSoonFile !== 'function') return;
|
||||
|
||||
bridge.importSoonFile(pending.name || 'design.soon', jsonRaw).then(function (res) {
|
||||
if (res && res.fileKey && typeof window.openAs !== 'undefined' && window.openAs) {
|
||||
window.openAs.name = res.fileKey;
|
||||
}
|
||||
loadJsonForImport().then(function (jsonObj) {
|
||||
if (!jsonObj) return;
|
||||
var oldKey = currentFileKey;
|
||||
return bridge.importSoonFile(pending.name || 'design.soon', jsonObj).then(function (res) {
|
||||
if (res && res.fileKey && typeof window.openAs !== 'undefined' && window.openAs) {
|
||||
window.openAs.name = res.fileKey;
|
||||
}
|
||||
if (res && res.fileKey && typeof window.soonLocalRenameKey === 'function') {
|
||||
window.soonLocalRenameKey(oldKey, res.fileKey, { source: 'cloud', name: res.name || pending.name });
|
||||
}
|
||||
try {
|
||||
sessionStorage.removeItem(oldKey);
|
||||
localStorage.removeItem(oldKey);
|
||||
} catch (e) { /* ignore */ }
|
||||
if (typeof window.soonRecentOnCloudSave === 'function') {
|
||||
window.soonRecentOnCloudSave(res, oldKey, soonSoonTypeFromJson(jsonObj));
|
||||
}
|
||||
});
|
||||
}).catch(function (err) {
|
||||
if (err && err.status) return;
|
||||
soonToast((err && err.message) ? err.message : '云端登记失败,可稍后保存重试', 'warn');
|
||||
if (typeof window.soonShowApiError === 'function') {
|
||||
window.soonShowApiError({
|
||||
status: err && err.status,
|
||||
message: (err && err.message) || '云端登记失败,可稍后保存重试',
|
||||
code: err && err.code
|
||||
});
|
||||
} else {
|
||||
soonToast((err && err.message) || '云端登记失败,可稍后保存重试', 'warn');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -462,13 +726,13 @@
|
||||
return tplParsed ? ('模板 #' + tplParsed.id) : pathOrKey;
|
||||
}
|
||||
if (pathOrKey.indexOf(FILE_PREFIX) === 0) {
|
||||
var meta = window._soonFileMeta;
|
||||
if (meta && meta.name) return meta.name;
|
||||
var resolved = soonResolveCloudFileName(pathOrKey);
|
||||
if (resolved) return resolved;
|
||||
var parsed = soonParseFileKey(pathOrKey);
|
||||
return parsed ? ('文件 #' + parsed.id) : pathOrKey;
|
||||
}
|
||||
if (pathOrKey.indexOf('soondesign_session:') === 0) {
|
||||
return pathOrKey.replace(/^soondesign_session:/, '');
|
||||
return soonSessionDisplayName(pathOrKey);
|
||||
}
|
||||
return String(pathOrKey).split(/[/\\]/).pop();
|
||||
}
|
||||
@@ -503,6 +767,7 @@
|
||||
window.soonParseFileKey = soonParseFileKey;
|
||||
window.soonMakeFileKey = soonMakeFileKey;
|
||||
window.soonNormalizeSoonName = soonNormalizeSoonName;
|
||||
window.soonResolveCloudFileName = soonResolveCloudFileName;
|
||||
window.soonApplyCloudMeta = soonApplyCloudMeta;
|
||||
window.soonBindFileMeta = soonBindFileMeta;
|
||||
window.soonSyncOpenNavigation = soonSyncOpenNavigation;
|
||||
@@ -529,6 +794,21 @@
|
||||
window.soonParseApiError = soonParseApiError;
|
||||
window.soonShowApiError = soonShowApiError;
|
||||
window.soonDisplayFileName = soonDisplayFileName;
|
||||
window.soonIsTemplateKey = soonIsTemplateKey;
|
||||
window.soonNeedsSaveDialog = soonNeedsSaveDialog;
|
||||
window.soonDefaultNewSoonName = soonDefaultNewSoonName;
|
||||
window.soonEnsureSoonExt = soonEnsureSoonExt;
|
||||
window.soonSessionSlug = soonSessionSlug;
|
||||
window.soonMakeSessionKey = soonMakeSessionKey;
|
||||
window.soonSessionDisplayName = soonSessionDisplayName;
|
||||
window.soonBuildSavePreviewPic = soonBuildSavePreviewPic;
|
||||
window.soonApplySavePreview = soonApplySavePreview;
|
||||
window.soonSaveDialogDefaultPath = soonSaveDialogDefaultPath;
|
||||
window.soonEnsureSaveFileName = soonEnsureSaveFileName;
|
||||
window.soonReportSaveError = soonReportSaveError;
|
||||
window.soonWriteSoonContentWeb = soonWriteSoonContentWeb;
|
||||
window.soonDownloadBlob = soonDownloadBlob;
|
||||
window.soonDownloadRecentItem = soonDownloadRecentItem;
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', soonHandlePayReturn);
|
||||
|
||||
@@ -141,6 +141,8 @@
|
||||
}
|
||||
_gate.plan = plan;
|
||||
if (statusEl) statusEl.textContent = '';
|
||||
var priceEl = modal.querySelector('.soon-activate-price__value');
|
||||
if (priceEl) priceEl.textContent = '¥' + planPriceDisplay(plan);
|
||||
updatePayButton(modal);
|
||||
}).catch(function () {
|
||||
if (statusEl) statusEl.textContent = '网络错误,请稍后重试';
|
||||
@@ -205,24 +207,36 @@
|
||||
try { layer.close(_gate.payIndex); } catch (e) { /* ignore */ }
|
||||
}
|
||||
_gate = { index: null, payIndex: null, plan: null, gateOpts: opts };
|
||||
var width = Math.min(520, window.innerWidth - 24);
|
||||
var width = Math.min(480, window.innerWidth - 32);
|
||||
layer.open({
|
||||
type: 1,
|
||||
skin: 'soon-layer',
|
||||
title: false,
|
||||
closeBtn: 1,
|
||||
fixed: true,
|
||||
offset: 'auto',
|
||||
shade: [0.68, '#000'],
|
||||
shadeClose: true,
|
||||
maxWidth: width,
|
||||
area: [width + 'px', 'auto'],
|
||||
content: shellHtml(opts.reason, null),
|
||||
success: function (layero, index) {
|
||||
var layerEl = layero && layero[0] ? layero[0] : layero;
|
||||
if (layerEl && layerEl.classList) layerEl.classList.add('soon-layer--subscribe');
|
||||
if (layerEl && layerEl.classList) {
|
||||
layerEl.classList.add('soon-layer--activate');
|
||||
}
|
||||
var content = layerEl && layerEl.querySelector ? layerEl.querySelector('.layui-layer-content') : null;
|
||||
if (content) content.style.padding = '0';
|
||||
if (content) {
|
||||
content.style.padding = '0';
|
||||
content.style.overflow = 'visible';
|
||||
}
|
||||
var modal = layerEl.querySelector('.soon-activate-modal');
|
||||
_gate.index = index;
|
||||
_gate.gateOpts = opts;
|
||||
if (modal) bindModal(modal);
|
||||
requestAnimationFrame(function () {
|
||||
if (typeof layer.style === 'function') layer.style(index);
|
||||
});
|
||||
},
|
||||
end: function () {
|
||||
var payIdx = _gate.payIndex;
|
||||
|
||||
@@ -37,21 +37,29 @@
|
||||
}
|
||||
|
||||
function shellHtml(reason) {
|
||||
var mailIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 6h16v12H4z"/><path d="M4 8l8 5 8-5"/></svg>';
|
||||
var lockIcon = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="5" y="11" width="14" height="10" rx="1"/><path d="M8 11V8a4 4 0 118 0v3"/></svg>';
|
||||
return '<div class="soon-login-gate">' +
|
||||
'<header class="soon-subscribe-modal__head">' +
|
||||
'<span class="soon-subscribe-modal__eyebrow">登录</span>' +
|
||||
'<h3 class="soon-subscribe-modal__title">登录后继续</h3>' +
|
||||
'<p class="soon-subscribe-modal__desc">预览内' + esc(reason || '导出或打印') + '需先登录账号。</p>' +
|
||||
'<header class="soon-login-gate__head">' +
|
||||
'<span class="soon-login-gate__eyebrow">登录</span>' +
|
||||
'<h3 class="soon-login-gate__title">登录后继续</h3>' +
|
||||
'<p class="soon-login-gate__desc">预览内' + esc(reason || '导出或打印') + '成品需先登录账号。</p>' +
|
||||
'</header>' +
|
||||
'<form class="soon-login-gate__form" data-role="login-form">' +
|
||||
'<div class="soon-input-wrap"><input type="email" name="email" required class="soon-input" placeholder="邮箱" autocomplete="email"></div>' +
|
||||
'<div class="soon-input-wrap"><input type="password" name="password" required class="soon-input" placeholder="密码" autocomplete="current-password"></div>' +
|
||||
'<p class="soon-login-gate__error" data-role="login-error" style="display:none"></p>' +
|
||||
'<button type="submit" class="soon-btn soon-btn--block">登录</button>' +
|
||||
'</form>' +
|
||||
'<div class="soon-input-wrap soon-login-gate__field">' +
|
||||
'<span class="soon-input-icon" aria-hidden="true">' + mailIcon + '</span>' +
|
||||
'<input type="email" name="email" required class="soon-input soon-login-gate__input" placeholder="邮箱" autocomplete="email">' +
|
||||
'</div>' +
|
||||
'<div class="soon-input-wrap soon-login-gate__field">' +
|
||||
'<span class="soon-input-icon" aria-hidden="true">' + lockIcon + '</span>' +
|
||||
'<input type="password" name="password" required class="soon-input soon-login-gate__input" placeholder="密码" autocomplete="current-password">' +
|
||||
'</div>' +
|
||||
'<p class="soon-login-gate__error" data-role="login-error" hidden></p>' +
|
||||
'<button type="submit" class="soon-btn soon-btn--block soon-login-gate__submit">登录</button>' +
|
||||
'<p class="soon-login-gate__links">还没有账号?<a href="register.web.html" target="_blank" rel="noopener">立即注册</a></p>' +
|
||||
'<footer class="soon-subscribe-modal__foot">' +
|
||||
'<button type="button" class="soon-subscribe-modal__stay" data-action="stay">继续设计</button>' +
|
||||
'</form>' +
|
||||
'<footer class="soon-login-gate__foot">' +
|
||||
'<button type="button" class="soon-login-gate__stay" data-action="stay">继续设计</button>' +
|
||||
'</footer></div>';
|
||||
}
|
||||
|
||||
@@ -77,7 +85,7 @@
|
||||
form.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
if (errEl) {
|
||||
errEl.style.display = 'none';
|
||||
errEl.hidden = true;
|
||||
errEl.textContent = '';
|
||||
}
|
||||
var email = (form.email && form.email.value || '').trim();
|
||||
@@ -90,7 +98,7 @@
|
||||
if (!j.ok || !j.data || !j.data.access_token) {
|
||||
if (errEl) {
|
||||
errEl.textContent = (j && j.message) || '登录失败';
|
||||
errEl.style.display = 'block';
|
||||
errEl.hidden = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -109,7 +117,7 @@
|
||||
}).catch(function () {
|
||||
if (errEl) {
|
||||
errEl.textContent = '网络错误,请稍后重试';
|
||||
errEl.style.display = 'block';
|
||||
errEl.hidden = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -122,20 +130,26 @@
|
||||
try { layer.close(_gate.index); } catch (e) { /* ignore */ }
|
||||
}
|
||||
_gate.opts = opts;
|
||||
var width = Math.min(420, window.innerWidth - 24);
|
||||
var width = Math.min(400, window.innerWidth - 32);
|
||||
layer.open({
|
||||
type: 1,
|
||||
skin: 'soon-layer',
|
||||
title: false,
|
||||
closeBtn: 1,
|
||||
shade: [0.62, '#000'],
|
||||
shadeClose: true,
|
||||
area: [width + 'px', 'auto'],
|
||||
offset: 'auto',
|
||||
content: shellHtml(opts.reason),
|
||||
success: function (layero, index) {
|
||||
var layerEl = layero && layero[0] ? layero[0] : layero;
|
||||
if (layerEl && layerEl.classList) layerEl.classList.add('soon-layer--subscribe');
|
||||
if (layerEl && layerEl.classList) layerEl.classList.add('soon-layer--login-gate');
|
||||
var content = layerEl && layerEl.querySelector ? layerEl.querySelector('.layui-layer-content') : null;
|
||||
if (content) content.style.padding = '0';
|
||||
if (content) {
|
||||
content.style.padding = '0';
|
||||
content.style.overflow = 'visible';
|
||||
content.style.background = 'transparent';
|
||||
}
|
||||
var modal = layerEl.querySelector('.soon-login-gate');
|
||||
_gate.index = index;
|
||||
_gate.opts = opts;
|
||||
|
||||
@@ -473,22 +473,29 @@
|
||||
displayChannels: channels,
|
||||
});
|
||||
var payCtrl = null;
|
||||
var width = Math.min(480, window.innerWidth - 32);
|
||||
var idx = layer.open({
|
||||
type: 1,
|
||||
skin: 'soon-layer',
|
||||
title: false,
|
||||
closeBtn: 1,
|
||||
fixed: true,
|
||||
offset: 'auto',
|
||||
shade: [0.72, '#000'],
|
||||
shadeClose: !!opts.shadeClose,
|
||||
area: ['480px'],
|
||||
maxWidth: width,
|
||||
area: [width + 'px', 'auto'],
|
||||
content: html,
|
||||
success: function (layero) {
|
||||
success: function (layero, index) {
|
||||
var layerEl = layero && layero[0] ? layero[0] : layero;
|
||||
if (layerEl && layerEl.classList) {
|
||||
layerEl.classList.add('soon-layer--pay');
|
||||
var layerContent = layerEl.querySelector('.layui-layer-content');
|
||||
if (layerContent) {
|
||||
layerContent.style.padding = '0';
|
||||
layerContent.style.overflow = 'visible';
|
||||
layerContent.style.maxHeight = 'none';
|
||||
layerContent.style.background = 'transparent';
|
||||
}
|
||||
}
|
||||
var sheet = layerEl.querySelector('.soon-pay-sheet');
|
||||
@@ -505,6 +512,9 @@
|
||||
},
|
||||
onBack: opts.onBack,
|
||||
});
|
||||
requestAnimationFrame(function () {
|
||||
if (typeof layer.style === 'function') layer.style(index);
|
||||
});
|
||||
},
|
||||
end: function () {
|
||||
if (payCtrl) payCtrl.destroy();
|
||||
|
||||
@@ -19,8 +19,43 @@
|
||||
}
|
||||
}
|
||||
|
||||
function initClearCache() {
|
||||
var btn = document.getElementById('auth_clear_cache');
|
||||
if (!btn) return;
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
if (typeof layui === 'undefined' || typeof window.soonClearLocalDesignCache !== 'function') return;
|
||||
layui.use('layer', function () {
|
||||
var layer = layui.layer;
|
||||
layer.open({
|
||||
type: 1,
|
||||
skin: 'soon-layer',
|
||||
title: '清除本地缓存',
|
||||
content: '<div class="soon-dialog-body">将清除最近文件与本地缓存,不会退出登录。未上传的本地编辑将丢失。</div>',
|
||||
btn: ['确定清除', '取消'],
|
||||
btnAlign: 'r',
|
||||
area: ['320px', 'auto'],
|
||||
shadeClose: true,
|
||||
yes: function (index) {
|
||||
layer.close(index);
|
||||
window.soonClearLocalDesignCache().then(function () {
|
||||
if (typeof window.soonToast === 'function') window.soonToast('已清除本地缓存', 'success');
|
||||
else layer.msg('已清除本地缓存', { icon: 1, time: 1200 });
|
||||
setTimeout(function () {
|
||||
if (/design[12]\.web\.html/i.test(location.pathname || '')) location.href = 'index.web.html';
|
||||
else location.reload();
|
||||
}, 400);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function initTopbar(options) {
|
||||
options = options || {};
|
||||
initClearCache();
|
||||
|
||||
var tok = localStorage.getItem('soon_access') || '';
|
||||
var login = document.getElementById('auth_login');
|
||||
var reg = document.getElementById('auth_register');
|
||||
|
||||
@@ -37,7 +37,8 @@
|
||||
function toolsBlock(lang, linksHtml) {
|
||||
return '<div class="soon-portal-topbar__tools">' +
|
||||
lang +
|
||||
(linksHtml ? '<nav class="soon-portal-topbar__links" aria-label="导航">' + linksHtml + '</nav>' : '') +
|
||||
(linksHtml ? '<nav class="soon-portal-topbar__links" aria-label="导航">' + linksHtml +
|
||||
'<a href="#" id="auth_clear_cache" class="soon-portal-topbar__link">清除缓存</a></nav>' : '') +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var DB_NAME = 'soondesign_local';
|
||||
var DB_VERSION = 1;
|
||||
var MAX_BLOBS = 20;
|
||||
var THUMB_MAX_BYTES = 102400;
|
||||
var dbPromise = null;
|
||||
var idbAvailable = typeof indexedDB !== 'undefined';
|
||||
|
||||
function soonExtractThumbFromJson(json) {
|
||||
if (!json || !json.frontDisplayPic || typeof json.frontDisplayPic !== 'string') return '';
|
||||
var candidate = json.frontDisplayPic.trim();
|
||||
if (candidate.indexOf('data:image/') !== 0) return '';
|
||||
if (candidate.length > THUMB_MAX_BYTES) return '';
|
||||
return candidate;
|
||||
}
|
||||
|
||||
function txDone(tx) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
tx.oncomplete = function () { resolve(); };
|
||||
tx.onerror = function () { reject(tx.error || new Error('idb_tx_error')); };
|
||||
tx.onabort = function () { reject(tx.error || new Error('idb_tx_abort')); };
|
||||
});
|
||||
}
|
||||
|
||||
function reqPromise(req) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
req.onsuccess = function () { resolve(req.result); };
|
||||
req.onerror = function () { reject(req.error || new Error('idb_req_error')); };
|
||||
});
|
||||
}
|
||||
|
||||
function soonLocalOpen() {
|
||||
if (!idbAvailable) return Promise.resolve(null);
|
||||
if (dbPromise) return dbPromise;
|
||||
dbPromise = new Promise(function (resolve, reject) {
|
||||
var req = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
req.onupgradeneeded = function (e) {
|
||||
var db = e.target.result;
|
||||
if (!db.objectStoreNames.contains('blobs')) {
|
||||
db.createObjectStore('blobs', { keyPath: 'cacheKey' });
|
||||
}
|
||||
if (!db.objectStoreNames.contains('thumbs')) {
|
||||
db.createObjectStore('thumbs', { keyPath: 'cacheKey' });
|
||||
}
|
||||
};
|
||||
req.onsuccess = function () { resolve(req.result); };
|
||||
req.onerror = function () {
|
||||
idbAvailable = false;
|
||||
dbPromise = null;
|
||||
reject(req.error || new Error('idb_open_failed'));
|
||||
};
|
||||
}).catch(function () {
|
||||
idbAvailable = false;
|
||||
dbPromise = null;
|
||||
return null;
|
||||
});
|
||||
return dbPromise;
|
||||
}
|
||||
|
||||
function soonLocalGet(cacheKey) {
|
||||
if (!cacheKey || !idbAvailable) return Promise.resolve(null);
|
||||
return soonLocalOpen().then(function (db) {
|
||||
if (!db) return null;
|
||||
var tx = db.transaction('blobs', 'readonly');
|
||||
return reqPromise(tx.objectStore('blobs').get(cacheKey)).then(function (row) {
|
||||
if (!row || row.json == null) return null;
|
||||
return {
|
||||
json: row.json,
|
||||
meta: {
|
||||
name: row.name,
|
||||
type: row.type,
|
||||
source: row.source,
|
||||
updatedAt: row.updatedAt,
|
||||
savedAt: row.savedAt,
|
||||
bytes: row.bytes
|
||||
}
|
||||
};
|
||||
});
|
||||
}).catch(function () { return null; });
|
||||
}
|
||||
|
||||
function soonLocalEvictLRU(max) {
|
||||
max = max || MAX_BLOBS;
|
||||
return soonLocalOpen().then(function (db) {
|
||||
if (!db) return;
|
||||
var tx = db.transaction(['blobs', 'thumbs'], 'readwrite');
|
||||
var blobStore = tx.objectStore('blobs');
|
||||
return reqPromise(blobStore.getAll()).then(function (rows) {
|
||||
if (!rows || rows.length <= max) return txDone(tx);
|
||||
rows.sort(function (a, b) { return (a.savedAt || 0) - (b.savedAt || 0); });
|
||||
var toRemove = rows.length - max;
|
||||
for (var i = 0; i < toRemove; i++) {
|
||||
var k = rows[i].cacheKey;
|
||||
blobStore.delete(k);
|
||||
tx.objectStore('thumbs').delete(k);
|
||||
}
|
||||
return txDone(tx);
|
||||
});
|
||||
}).catch(function () { /* ignore */ });
|
||||
}
|
||||
|
||||
function soonLocalPut(cacheKey, json, meta) {
|
||||
if (!cacheKey || json == null || !idbAvailable) return Promise.resolve(false);
|
||||
meta = meta || {};
|
||||
var jsonObj = typeof json === 'string' ? (function () {
|
||||
try { return JSON.parse(json); } catch (e) { return null; }
|
||||
})() : json;
|
||||
if (!jsonObj) return Promise.resolve(false);
|
||||
var bytes = 0;
|
||||
try { bytes = JSON.stringify(jsonObj).length; } catch (e) { bytes = 0; }
|
||||
var row = {
|
||||
cacheKey: cacheKey,
|
||||
json: jsonObj,
|
||||
name: meta.name || '',
|
||||
type: meta.type != null ? meta.type : (jsonObj.soonType || 1),
|
||||
source: meta.source || '',
|
||||
updatedAt: meta.updatedAt || '',
|
||||
savedAt: Date.now(),
|
||||
bytes: bytes
|
||||
};
|
||||
return soonLocalOpen().then(function (db) {
|
||||
if (!db) return false;
|
||||
var tx = db.transaction('blobs', 'readwrite');
|
||||
tx.objectStore('blobs').put(row);
|
||||
return txDone(tx).then(function () {
|
||||
return soonLocalEvictLRU(MAX_BLOBS).then(function () { return true; });
|
||||
});
|
||||
}).catch(function () { return false; });
|
||||
}
|
||||
|
||||
function soonLocalPutThumb(cacheKey, dataUrl) {
|
||||
if (!cacheKey || !dataUrl || !idbAvailable) return Promise.resolve(false);
|
||||
if (String(dataUrl).length > THUMB_MAX_BYTES) return Promise.resolve(false);
|
||||
return soonLocalOpen().then(function (db) {
|
||||
if (!db) return false;
|
||||
var tx = db.transaction('thumbs', 'readwrite');
|
||||
tx.objectStore('thumbs').put({ cacheKey: cacheKey, dataUrl: dataUrl, savedAt: Date.now() });
|
||||
return txDone(tx).then(function () { return true; });
|
||||
}).catch(function () { return false; });
|
||||
}
|
||||
|
||||
function soonLocalGetThumb(cacheKey) {
|
||||
if (!cacheKey || !idbAvailable) return Promise.resolve('');
|
||||
return soonLocalOpen().then(function (db) {
|
||||
if (!db) return '';
|
||||
var tx = db.transaction('thumbs', 'readonly');
|
||||
return reqPromise(tx.objectStore('thumbs').get(cacheKey)).then(function (row) {
|
||||
return (row && row.dataUrl) ? row.dataUrl : '';
|
||||
});
|
||||
}).catch(function () { return ''; });
|
||||
}
|
||||
|
||||
function soonLocalRemove(cacheKey) {
|
||||
if (!cacheKey || !idbAvailable) return Promise.resolve();
|
||||
return soonLocalOpen().then(function (db) {
|
||||
if (!db) return;
|
||||
var tx = db.transaction(['blobs', 'thumbs'], 'readwrite');
|
||||
tx.objectStore('blobs').delete(cacheKey);
|
||||
tx.objectStore('thumbs').delete(cacheKey);
|
||||
return txDone(tx);
|
||||
}).catch(function () { /* ignore */ });
|
||||
}
|
||||
|
||||
function soonLocalRenameKey(oldKey, newKey, meta) {
|
||||
if (!oldKey || !newKey || oldKey === newKey) return Promise.resolve(false);
|
||||
return soonLocalGet(oldKey).then(function (hit) {
|
||||
if (!hit) return false;
|
||||
return soonLocalPut(newKey, hit.json, Object.assign({}, hit.meta, meta || {})).then(function (ok) {
|
||||
if (!ok) return false;
|
||||
return soonLocalGetThumb(oldKey).then(function (thumb) {
|
||||
var chain = Promise.resolve();
|
||||
if (thumb) chain = soonLocalPutThumb(newKey, thumb);
|
||||
return chain.then(function () { return soonLocalRemove(oldKey); }).then(function () { return true; });
|
||||
});
|
||||
});
|
||||
}).catch(function () { return false; });
|
||||
}
|
||||
|
||||
function soonLocalCacheAndThumb(cacheKey, json, meta) {
|
||||
var thumb = soonExtractThumbFromJson(json);
|
||||
return soonLocalPut(cacheKey, json, meta).then(function (ok) {
|
||||
if (!ok) return false;
|
||||
if (thumb) return soonLocalPutThumb(cacheKey, thumb).then(function () { return true; });
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function soonLocalIsTemplateStale(cacheKey, blobMeta) {
|
||||
if (!cacheKey || cacheKey.indexOf('soondesign_template:') !== 0) return false;
|
||||
var tplMeta = window._soonTemplateMeta;
|
||||
if (!tplMeta || !tplMeta.updated_at) return false;
|
||||
if (!blobMeta || !blobMeta.updatedAt) return false;
|
||||
return String(tplMeta.updated_at) !== String(blobMeta.updatedAt);
|
||||
}
|
||||
|
||||
window.soonLocalOpen = soonLocalOpen;
|
||||
window.soonLocalGet = soonLocalGet;
|
||||
window.soonLocalPut = soonLocalPut;
|
||||
window.soonLocalPutThumb = soonLocalPutThumb;
|
||||
window.soonLocalGetThumb = soonLocalGetThumb;
|
||||
window.soonLocalRemove = soonLocalRemove;
|
||||
window.soonLocalRenameKey = soonLocalRenameKey;
|
||||
window.soonLocalEvictLRU = soonLocalEvictLRU;
|
||||
window.soonLocalCacheAndThumb = soonLocalCacheAndThumb;
|
||||
window.soonLocalIsTemplateStale = soonLocalIsTemplateStale;
|
||||
window.soonExtractThumbFromJson = soonExtractThumbFromJson;
|
||||
|
||||
var LS_DROP = ['soondesign_recent', 'soondesign_history', 'soondesign_recent_migrated'];
|
||||
var SS_DROP = ['soondesign_open_file', 'soondesign_open_type', 'soondesign_open_meta', 'soondesign_pending_import'];
|
||||
|
||||
function dropStorageKeys(storage, prefix) {
|
||||
if (!storage) return;
|
||||
try {
|
||||
var rm = [];
|
||||
for (var i = 0; i < storage.length; i++) {
|
||||
var k = storage.key(i);
|
||||
if (k && k.indexOf(prefix) === 0) rm.push(k);
|
||||
}
|
||||
rm.forEach(function (k) { storage.removeItem(k); });
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
function soonClearLocalDesignCache() {
|
||||
try {
|
||||
LS_DROP.forEach(function (k) { localStorage.removeItem(k); });
|
||||
dropStorageKeys(localStorage, 'soondesign_session:');
|
||||
SS_DROP.forEach(function (k) { sessionStorage.removeItem(k); });
|
||||
dropStorageKeys(sessionStorage, 'soondesign_session:');
|
||||
} catch (e) { /* ignore */ }
|
||||
dbPromise = null;
|
||||
if (typeof indexedDB === 'undefined') return Promise.resolve();
|
||||
return new Promise(function (resolve) {
|
||||
var done = function () { resolve(); };
|
||||
try {
|
||||
var req = indexedDB.deleteDatabase(DB_NAME);
|
||||
req.onsuccess = req.onerror = req.onblocked = done;
|
||||
} catch (e2) { done(); }
|
||||
});
|
||||
}
|
||||
|
||||
window.soonClearLocalDesignCache = soonClearLocalDesignCache;
|
||||
})();
|
||||
@@ -0,0 +1,217 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var RECENT_KEY = 'soondesign_recent';
|
||||
var MIGRATED_KEY = 'soondesign_recent_migrated';
|
||||
var HISTORY_KEY = 'soondesign_history';
|
||||
var MAX_ITEMS = 20;
|
||||
|
||||
function nowTimeStr() {
|
||||
var d = new Date();
|
||||
function pad(n) { return n < 10 ? '0' + n : String(n); }
|
||||
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' +
|
||||
pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds());
|
||||
}
|
||||
|
||||
function readRaw() {
|
||||
try {
|
||||
var raw = localStorage.getItem(RECENT_KEY);
|
||||
if (!raw) return { version: 1, items: [] };
|
||||
var j = JSON.parse(raw);
|
||||
if (!j || !Array.isArray(j.items)) return { version: 1, items: [] };
|
||||
return j;
|
||||
} catch (e) {
|
||||
return { version: 1, items: [] };
|
||||
}
|
||||
}
|
||||
|
||||
function writeRaw(data) {
|
||||
try {
|
||||
localStorage.setItem(RECENT_KEY, JSON.stringify(data));
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function soonRecentKindFromKey(key) {
|
||||
if (!key) return 'local';
|
||||
if (key.indexOf('soondesign_file:') === 0) return 'cloud';
|
||||
if (key.indexOf('soondesign_template:') === 0) return 'template';
|
||||
return 'local';
|
||||
}
|
||||
|
||||
function parseFileId(key) {
|
||||
var m = String(key).match(/^soondesign_file:(\d+)/);
|
||||
return m ? parseInt(m[1], 10) : null;
|
||||
}
|
||||
|
||||
function soonRecentList() {
|
||||
return readRaw().items.slice();
|
||||
}
|
||||
|
||||
function soonRecentRemove(key) {
|
||||
if (!key) return;
|
||||
var data = readRaw();
|
||||
data.items = data.items.filter(function (it) { return it.key !== key; });
|
||||
writeRaw(data);
|
||||
}
|
||||
|
||||
function soonRecentUpsert(item) {
|
||||
if (!item || !item.key) return;
|
||||
var key = item.key;
|
||||
var kind = item.kind || soonRecentKindFromKey(key);
|
||||
var type = item.type != null ? item.type : 1;
|
||||
var name = item.name || '';
|
||||
if (!name && typeof window.soonDisplayFileName === 'function') {
|
||||
name = window.soonDisplayFileName(key);
|
||||
}
|
||||
var entry = {
|
||||
key: key,
|
||||
name: name || 'design.soon',
|
||||
type: type,
|
||||
kind: kind,
|
||||
fileId: item.fileId != null ? item.fileId : parseFileId(key),
|
||||
thumbRef: item.thumbRef || key,
|
||||
time: item.time || nowTimeStr()
|
||||
};
|
||||
var data = readRaw();
|
||||
data.items = data.items.filter(function (it) { return it.key !== key; });
|
||||
data.items.unshift(entry);
|
||||
if (data.items.length > MAX_ITEMS) data.items = data.items.slice(0, MAX_ITEMS);
|
||||
writeRaw(data);
|
||||
}
|
||||
|
||||
function soonRecentUpsertFromOpen(fileKey, json) {
|
||||
if (!fileKey) return;
|
||||
var type = 1;
|
||||
if (json) {
|
||||
type = json.soonType ? json.soonType : (json.backBlackPic ? 2 : 1);
|
||||
}
|
||||
var name = '';
|
||||
if (typeof window.soonDisplayFileName === 'function') name = window.soonDisplayFileName(fileKey);
|
||||
var tplMeta = window._soonTemplateMeta;
|
||||
if (tplMeta && tplMeta.name && fileKey.indexOf('soondesign_template:') === 0) {
|
||||
name = tplMeta.name;
|
||||
if (tplMeta.type) type = tplMeta.type;
|
||||
}
|
||||
var fileMeta = window._soonFileMeta;
|
||||
if (fileMeta && fileMeta.name && fileKey.indexOf('soondesign_file:') === 0) {
|
||||
name = fileMeta.name;
|
||||
}
|
||||
soonRecentUpsert({
|
||||
key: fileKey,
|
||||
name: name,
|
||||
type: type,
|
||||
kind: soonRecentKindFromKey(fileKey),
|
||||
fileId: parseFileId(fileKey),
|
||||
thumbRef: fileKey,
|
||||
time: nowTimeStr(),
|
||||
updated_at: (tplMeta && tplMeta.updated_at) ? tplMeta.updated_at : undefined
|
||||
});
|
||||
}
|
||||
|
||||
function soonRecentMigrateFromHistory() {
|
||||
try {
|
||||
if (localStorage.getItem(MIGRATED_KEY) === '1') return;
|
||||
} catch (e) { return; }
|
||||
var histRaw;
|
||||
try {
|
||||
histRaw = localStorage.getItem(HISTORY_KEY);
|
||||
} catch (e) { return; }
|
||||
if (!histRaw) {
|
||||
try { localStorage.setItem(MIGRATED_KEY, '1'); } catch (e2) { /* ignore */ }
|
||||
return;
|
||||
}
|
||||
var hist;
|
||||
try {
|
||||
hist = JSON.parse(histRaw);
|
||||
} catch (e) {
|
||||
try { localStorage.setItem(MIGRATED_KEY, '1'); } catch (e2) { /* ignore */ }
|
||||
return;
|
||||
}
|
||||
var list = (hist && Array.isArray(hist.history)) ? hist.history : [];
|
||||
list.forEach(function (h) {
|
||||
if (!h || !h.path) return;
|
||||
soonRecentUpsert({
|
||||
key: h.path,
|
||||
name: typeof window.soonDisplayFileName === 'function' ? window.soonDisplayFileName(h.path) : h.path,
|
||||
type: h.type || 1,
|
||||
kind: soonRecentKindFromKey(h.path),
|
||||
fileId: parseFileId(h.path),
|
||||
thumbRef: h.path,
|
||||
time: h.time || nowTimeStr()
|
||||
});
|
||||
});
|
||||
try { localStorage.setItem(MIGRATED_KEY, '1'); } catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
function soonRecentEnrichFromCloud(items, cloudItems) {
|
||||
if (!items || !items.length || !cloudItems || !cloudItems.length) return items;
|
||||
var byId = {};
|
||||
cloudItems.forEach(function (c) {
|
||||
if (c && c.id != null) byId[c.id] = c;
|
||||
});
|
||||
return items.map(function (it) {
|
||||
if (!it.fileId || !byId[it.fileId]) return it;
|
||||
var c = byId[it.fileId];
|
||||
var copy = Object.assign({}, it);
|
||||
if (c.name) copy.name = c.name;
|
||||
var curKey = it.filePath || it.key;
|
||||
if (c.version != null && typeof window.soonMakeFileKey === 'function') {
|
||||
var newKey = window.soonMakeFileKey(c.id, c.version);
|
||||
if (newKey !== curKey) {
|
||||
soonRecentRemove(curKey);
|
||||
copy.filePath = newKey;
|
||||
copy.key = newKey;
|
||||
copy.thumbRef = newKey;
|
||||
soonRecentUpsert({
|
||||
key: newKey,
|
||||
name: copy.name,
|
||||
type: copy.type,
|
||||
kind: soonRecentKindFromKey(newKey),
|
||||
fileId: c.id,
|
||||
thumbRef: newKey
|
||||
});
|
||||
}
|
||||
}
|
||||
return copy;
|
||||
});
|
||||
}
|
||||
|
||||
function soonRecentOnCloudSave(res, prevKey, type) {
|
||||
if (!res || !res.fileKey) return;
|
||||
var dropKeys = {};
|
||||
if (prevKey && prevKey !== res.fileKey) dropKeys[prevKey] = 1;
|
||||
if (res.fileId != null) {
|
||||
readRaw().items.forEach(function (it) {
|
||||
if (it.fileId === res.fileId && it.key !== res.fileKey) dropKeys[it.key] = 1;
|
||||
});
|
||||
}
|
||||
Object.keys(dropKeys).forEach(function (k) {
|
||||
soonRecentRemove(k);
|
||||
if (typeof window.soonLocalRemove === 'function') window.soonLocalRemove(k);
|
||||
});
|
||||
var name = res.name || '';
|
||||
if (!name && typeof window.soonDisplayFileName === 'function') {
|
||||
name = window.soonDisplayFileName(res.fileKey);
|
||||
}
|
||||
soonRecentUpsert({
|
||||
key: res.fileKey,
|
||||
name: name || 'design.soon',
|
||||
type: type != null ? type : 1,
|
||||
kind: soonRecentKindFromKey(res.fileKey),
|
||||
fileId: res.fileId,
|
||||
thumbRef: res.fileKey
|
||||
});
|
||||
}
|
||||
|
||||
window.soonRecentList = soonRecentList;
|
||||
window.soonRecentRemove = soonRecentRemove;
|
||||
window.soonRecentUpsert = soonRecentUpsert;
|
||||
window.soonRecentUpsertFromOpen = soonRecentUpsertFromOpen;
|
||||
window.soonRecentMigrateFromHistory = soonRecentMigrateFromHistory;
|
||||
window.soonRecentKindFromKey = soonRecentKindFromKey;
|
||||
window.soonRecentEnrichFromCloud = soonRecentEnrichFromCloud;
|
||||
window.soonRecentOnCloudSave = soonRecentOnCloudSave;
|
||||
})();
|
||||
Reference in New Issue
Block a user