From 3968b6896cfe1ad066174a81a370fdbd051a8bf9 Mon Sep 17 00:00:00 2001 From: jonny Date: Wed, 22 Apr 2026 16:13:18 +0800 Subject: [PATCH] Add Memos API adapter and settings UI Introduce a Memos API adapter to unify compatibility across API flavors (v020-v021, v023, modern) and probe/detect server flavor. Add js/compat/memosApi.adapter.js and rename existing compat modules (memosApi.v024.js -> memosApi.modern.js, memosApi.v1.js -> memosApi.v020-v021.js) and trim probe from v023. Add settings UI support: new i18n keys in en/ja/ko/zh_CN, settings panel styles in css/main.css, new settings input binding and save flow in js (attachment-only default text persisted). Refactor oper.js to use the adapter for uploads, deletes, tag listing, search and image preview auth handling; add helper functions for attachment-only default text and settings payload. Update i18n bindings, popup, manifest and README changelog accordingly. closed #5 --- README.md | 2 +- _locales/en/messages.json | 15 + _locales/ja/messages.json | 15 + _locales/ko/messages.json | 15 + _locales/zh_CN/messages.json | 15 + css/main.css | 54 ++ js/compat/memosApi.adapter.js | 521 +++++++++++++ .../{memosApi.v024.js => memosApi.modern.js} | 2 +- .../{memosApi.v1.js => memosApi.v020-v021.js} | 2 +- js/compat/memosApi.v023.js | 105 +-- js/i18n.js | 7 +- js/oper.js | 700 ++++-------------- manifest.json | 2 +- popup.html | 66 +- 14 files changed, 840 insertions(+), 681 deletions(-) create mode 100644 js/compat/memosApi.adapter.js rename js/compat/{memosApi.v024.js => memosApi.modern.js} (99%) rename js/compat/{memosApi.v1.js => memosApi.v020-v021.js} (99%) diff --git a/README.md b/README.md index e3cb482..da64a7b 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Chrome 应用商店: 0) return memo.tags + if (Array.isArray(memo.tagList) && memo.tagList.length > 0) return memo.tagList + if (memo.property && Array.isArray(memo.property.tags) && memo.property.tags.length > 0) { + return memo.property.tags + } + return [] + } + + function collectTags(info, memos) { + const items = Array.isArray(memos) ? memos : [] + const out = items.flatMap(function (memo) { + if (!memo) return [] + if (getFlavor(info) === 'v023' && global.MemosApiV023 && typeof global.MemosApiV023.extractTagsFromMemo === 'function') { + return global.MemosApiV023.extractTagsFromMemo(memo) + } + return extractTagsFromGenericMemo(memo) + }) + return [...new Set(out.filter(Boolean))] + } + + function buildUploadVisibility(editorContent, hideTag, showTag, memoLock) { + const content = typeof editorContent === 'string' ? editorContent : '' + const nowTag = content.match(/(#[^\s#]+)/) + let visibility = memoLock || '' + if (nowTag) { + if (nowTag[1] === showTag) visibility = 'PUBLIC' + else if (nowTag[1] === hideTag) visibility = 'PRIVATE' + } + return visibility + } + + function buildModernFilter(parts) { + const p = parts || {} + const exprs = [] + + if (typeof p.contentSearch === 'string' && p.contentSearch.length > 0) { + exprs.push('content.contains(' + JSON.stringify(String(p.contentSearch)) + ')') + } + + return exprs.join(' && ') + } + + function normalizeUploadedItem(entity, fallbackFilename) { + if (!entity) return null + const inferredId = (function () { + const value = entity.id != null ? entity.id : entity.ID != null ? entity.ID : entity.Id + if (typeof value === 'number' && Number.isFinite(value)) return Math.floor(value) + if (typeof value === 'string' && value.trim() !== '' && !Number.isNaN(Number(value))) { + return Math.floor(Number(value)) + } + return null + })() + + const name = entity.name || (inferredId != null ? 'resources/' + String(inferredId) : '') + if (!name && inferredId == null) return null + + return { + id: inferredId != null ? inferredId : entity.id, + name: name, + filename: entity.filename || fallbackFilename || name, + createTime: entity.createTime || entity.createdTs || entity.createdAt, + type: entity.type + } + } + + function unwrapLegacyMemoEntity(data) { + if (!data) return data + if (data.memo) return data.memo + if (data.data && data.data.memo) return data.data.memo + return data + } + + function normalizeLegacyResourceIdList(list) { + const items = Array.isArray(list) ? list : [] + return items + .map(function (item) { + if (!item) return null + if (typeof item.id === 'number' && Number.isFinite(item.id)) return Math.floor(item.id) + if (typeof item.id === 'string' && item.id.trim() !== '' && !Number.isNaN(Number(item.id))) { + return Math.floor(Number(item.id)) + } + const name = typeof item.name === 'string' ? item.name : '' + const tail = name ? name.split('/').pop() : '' + if (tail && !Number.isNaN(Number(tail))) return Math.floor(Number(tail)) + return null + }) + .filter(function (value) { + return value != null && Number.isFinite(value) + }) + } + + function resolve(info) { + const flavor = getFlavor(info) + + function listTags(success, fail) { + if (flavor === FLAVOR_V020_V021 && global.MemosApiV020V021) { + global.MemosApiV020V021.getTagSuggestion(info, success, fail) + return + } + + if (flavor === 'v023' && global.MemosApiV023) { + const filterExpr = global.MemosApiV023.buildFilter({ + rowStatus: 'NORMAL', + creator: 'users/' + info.userid + }) + global.MemosApiV023.listMemos( + info, + { pageSize: 1000, filterExpr: filterExpr }, + function (data) { + if (success) success(collectTags(info, extractMemos(data))) + }, + fail + ) + return + } + + if (global.MemosApiModern) { + global.MemosApiModern.fetchMemosWithFallback( + info, + '?pageSize=1000', + function (data) { + if (success) success(collectTags(info, extractMemos(data))) + }, + fail + ) + } + } + + function searchMemos(pattern, success, fail) { + const text = String(pattern || '') + const patternLiteral = JSON.stringify(text) + const legacyFilter = '?filter=' + encodeURIComponent('visibility in ["PUBLIC","PROTECTED"] && content.contains(' + patternLiteral + ')') + + if (flavor === 'modern' && global.MemosApiV023) { + const filterExpr = buildModernFilter({ contentSearch: text }) + global.MemosApiV023.listMemos(info, { pageSize: 1000, filterExpr: filterExpr }, function (data) { + if (success) success(extractMemos(data)) + }, fail) + return + } + + if (flavor === 'v023' && global.MemosApiV023) { + const filterExpr = global.MemosApiV023.buildFilter({ + visibilities: ['PUBLIC', 'PROTECTED'], + contentSearch: text + }) + global.MemosApiV023.listMemos(info, { pageSize: 1000, filterExpr: filterExpr }, function (data) { + if (success) success(keepLegacyVisibleMemos(extractMemos(data))) + }, fail) + return + } + + if (flavor === FLAVOR_V020_V021 && global.MemosApiV020V021) { + global.MemosApiV020V021.listMemos(info, { limit: 1000, rowStatus: 'NORMAL', contentSearch: text }, function (data) { + if (success) success(keepLegacyVisibleMemos(extractMemos(data))) + }, fail) + return + } + + if (global.MemosApiModern) { + global.MemosApiModern.fetchMemosWithFallback(info, legacyFilter, function (data) { + if (success) success(keepLegacyVisibleMemos(extractMemos(data))) + }, fail) + } + } + + function listRandomMemos(success, fail) { + if (flavor === 'modern' && global.MemosApiV023) { + const filterExpr = global.MemosApiV023.buildFilter({}) + global.MemosApiV023.listMemos(info, { pageSize: 1000, filterExpr: filterExpr }, function (data) { + if (success) success(extractMemos(data)) + }, fail) + return + } + + if (flavor === 'v023' && global.MemosApiV023) { + const filterExpr = global.MemosApiV023.buildFilter({ visibilities: ['PUBLIC', 'PROTECTED'] }) + global.MemosApiV023.listMemos(info, { pageSize: 1000, filterExpr: filterExpr }, function (data) { + if (success) success(keepLegacyVisibleMemos(extractMemos(data))) + }, fail) + return + } + + if (flavor === FLAVOR_V020_V021 && global.MemosApiV020V021) { + global.MemosApiV020V021.listMemos(info, { limit: 1000, rowStatus: 'NORMAL' }, function (data) { + if (success) success(keepLegacyVisibleMemos(extractMemos(data))) + }, fail) + return + } + + if (global.MemosApiModern) { + const legacyFilter = '?filter=' + encodeURIComponent('visibility in ["PUBLIC","PROTECTED"]') + global.MemosApiModern.fetchMemosWithFallback(info, legacyFilter, function (data) { + if (success) success(keepLegacyVisibleMemos(extractMemos(data))) + }, fail) + } + } + + function deleteResource(item, success, fail) { + const name = item && item.name ? item.name : '' + const rid = item && item.id != null ? item.id : '' + const inferredId = (function () { + if (rid != null && String(rid).trim() !== '' && !Number.isNaN(Number(rid))) return Math.floor(Number(rid)) + const tail = String(name || '').split('/').pop() + if (tail && !Number.isNaN(Number(tail))) return Math.floor(Number(tail)) + return null + })() + + if (flavor === FLAVOR_V020_V021 && global.MemosApiV020V021 && typeof global.MemosApiV020V021.deleteResource === 'function' && inferredId != null) { + global.MemosApiV020V021.deleteResource(info, inferredId, success, fail) + return + } + + requestJson({ + url: info.apiUrl + 'api/v1/' + name, + type: 'DELETE', + headers: { Authorization: 'Bearer ' + info.apiTokens } + }, success, fail) + } + + function uploadFile(file, options, success, fail) { + const oldName = String(file && file.name ? file.name : 'upload').split('.') + const fileExt = String(file && file.name ? file.name : '').split('.').pop() + const now = global.dayjs().format('YYYYMMDDHHmmss') + const nextName = oldName[0] + '_' + now + (fileExt ? '.' + fileExt : '') + + if (flavor === FLAVOR_V020_V021 && global.MemosApiV020V021) { + global.MemosApiV020V021.uploadResourceBlob( + info, + file, + { filename: nextName, type: file.type }, + function (entity) { + if (success) success(normalizeUploadedItem(entity, nextName)) + }, + fail + ) + return + } + + const reader = new FileReader() + reader.onload = function (e) { + const base64String = e && e.target && e.target.result ? String(e.target.result).split(',')[1] : '' + const payload = { + content: base64String, + visibility: buildUploadVisibility(options && options.editorContent, options && options.hideTag, options && options.showTag, options && options.memoLock), + filename: nextName, + type: file.type + } + + global.MemosApiModern.uploadAttachmentOrResource( + info, + payload, + function (resp) { + const entity = (resp && resp.resource) || resp + if (success) success(normalizeUploadedItem(entity, nextName)) + }, + fail + ) + } + reader.onerror = fail + reader.readAsDataURL(file) + } + + function archiveMemo(memo, success, fail) { + const memoId = memo && memo.id != null ? memo.id : '' + const memoName = memo && memo.name ? memo.name : '' + + if (flavor === FLAVOR_V020_V021 && global.MemosApiV020V021 && memoId !== '') { + global.MemosApiV020V021.patchMemo(info, memoId, { rowStatus: 'ARCHIVED' }, success, fail) + return + } + + requestJson({ + url: info.apiUrl + 'api/v1/' + memoName, + type: 'PATCH', + data: JSON.stringify({ state: 'ARCHIVED' }), + contentType: 'application/json', + dataType: 'json', + headers: { Authorization: 'Bearer ' + info.apiTokens } + }, success, fail) + } + + function getMemo(memoRef, success, fail) { + const url = flavor === FLAVOR_V020_V021 + ? info.apiUrl + 'api/v1/memo/' + memoRef + : info.apiUrl + 'api/v1/' + memoRef + + requestJson({ + url: url, + type: 'GET', + contentType: 'application/json', + dataType: 'json', + headers: { Authorization: 'Bearer ' + info.apiTokens } + }, function (data) { + if (success) success(flavor === FLAVOR_V020_V021 ? unwrapLegacyMemoEntity(data) : data) + }, fail) + } + + function createMemo(params, success, fail) { + const payload = params || {} + + if (flavor === FLAVOR_V020_V021 && global.MemosApiV020V021) { + global.MemosApiV020V021.createMemo( + info, + { + content: payload.content, + visibility: payload.visibility, + resourceIdList: normalizeLegacyResourceIdList(payload.resourceIdList) + }, + success, + fail + ) + return + } + + requestJson({ + url: info.apiUrl + 'api/v1/memos', + type: 'POST', + data: JSON.stringify({ + content: payload.content, + visibility: payload.visibility + }), + contentType: 'application/json', + dataType: 'json', + headers: { Authorization: 'Bearer ' + info.apiTokens } + }, function (data) { + const createdName = data && data.name ? data.name : data && data.memo && data.memo.name ? data.memo.name : '' + const resources = Array.isArray(payload.resourceIdList) ? payload.resourceIdList : [] + if (!createdName) { + if (success) success(data) + return + } + if (resources.length === 0) { + getMemo(createdName, success, fail) + return + } + + global.MemosApiModern.patchMemoWithAttachmentsOrResources( + info, + createdName, + resources, + function () { + getMemo(createdName, success, fail) + }, + function () { + getMemo(createdName, success, fail) + } + ) + }, fail) + } + + return { + flavor: flavor, + needsAuthenticatedImagePreview: function () { + return flavor === FLAVOR_V020_V021 + }, + listTags: listTags, + searchMemos: searchMemos, + listRandomMemos: listRandomMemos, + deleteResource: deleteResource, + uploadFile: uploadFile, + archiveMemo: archiveMemo, + getMemo: getMemo, + createMemo: createMemo + } + } + + global.MemosApiAdapter = { + FLAVOR_V020_V021: FLAVOR_V020_V021, + KNOWN_FLAVORS: KNOWN_FLAVORS.slice(), + getFlavor: getFlavor, + normalizeDetectedFlavor: normalizeDetectedFlavor, + probeFlavor: probeFlavor, + resolve: resolve + } +})(window) \ No newline at end of file diff --git a/js/compat/memosApi.v024.js b/js/compat/memosApi.modern.js similarity index 99% rename from js/compat/memosApi.v024.js rename to js/compat/memosApi.modern.js index c45e9e3..4449eed 100644 --- a/js/compat/memosApi.v024.js +++ b/js/compat/memosApi.modern.js @@ -500,7 +500,7 @@ doPatchAttachments() } - global.MemosApi = { + global.MemosApiModern = { extractUserIdFromAuthResponse: extractUserIdFromAuthResponse, extractMemosListFromResponse: extractMemosListFromResponse, isNotFoundLikeXhr: isNotFoundLikeXhr, diff --git a/js/compat/memosApi.v1.js b/js/compat/memosApi.v020-v021.js similarity index 99% rename from js/compat/memosApi.v1.js rename to js/compat/memosApi.v020-v021.js index 0a8f900..bb874fd 100644 --- a/js/compat/memosApi.v1.js +++ b/js/compat/memosApi.v020-v021.js @@ -274,7 +274,7 @@ }) } - global.MemosApiV1 = { + global.MemosApiV020V021 = { listMemos: listMemos, createMemo: createMemo, patchMemo: patchMemo, diff --git a/js/compat/memosApi.v023.js b/js/compat/memosApi.v023.js index 1f947f7..c438e55 100644 --- a/js/compat/memosApi.v023.js +++ b/js/compat/memosApi.v023.js @@ -111,112 +111,9 @@ }) } - function probeApiFlavor(apiUrl, apiTokens, callback) { - const headers = { Authorization: 'Bearer ' + apiTokens } - - function looksLikeMemosListPayload(data) { - if (!data) return false - if (Array.isArray(data)) return true - if (Array.isArray(data.memos)) return true - if (data.data && Array.isArray(data.data.memos)) return true - if (Array.isArray(data.list)) return true - // Common JSON error shapes should not be treated as success. - if (typeof data.error === 'string' || typeof data.message === 'string') return false - return false - } - - function isNotFoundLike(xhr) { - const status = xhr && xhr.status - return status === 404 || status === 405 - } - - // Modern-style filter probe. - const modernQ = - 'api/v1/memos?pageSize=1&filter=' + - encodeURIComponent('visibility in ["PUBLIC","PROTECTED"]') - - // v0.23-style filter probe. - const v023Q = - 'api/v1/memos?pageSize=1&filter=' + - encodeURIComponent('visibilities == ["PUBLIC","PROTECTED"]') - - // v0.20/v0.21 unified API v1 probe. - const v1Q = 'api/v1/memo?limit=1&rowStatus=NORMAL' - - global.$ - .ajax({ - url: apiUrl + modernQ, - method: 'GET', - headers: headers, - dataType: 'json' - }) - .done(function (data) { - if (looksLikeMemosListPayload(data)) { - callback({ flavor: 'modern' }) - return - } - // Treat unexpected success payload as unknown and continue probing. - global.$ - .ajax({ - url: apiUrl + v023Q, - method: 'GET', - headers: headers, - dataType: 'json' - }) - .done(function (data2) { - if (looksLikeMemosListPayload(data2)) callback({ flavor: 'v023' }) - else callback({ flavor: 'unknown' }) - }) - .fail(function () { - callback({ flavor: 'unknown' }) - }) - }) - .fail(function (xhr) { - if (xhr && xhr.status === 400) { - global.$ - .ajax({ - url: apiUrl + v023Q, - method: 'GET', - headers: headers, - dataType: 'json' - }) - .done(function (data2) { - if (looksLikeMemosListPayload(data2)) callback({ flavor: 'v023' }) - else callback({ flavor: 'unknown' }) - }) - .fail(function () { - callback({ flavor: 'unknown' }) - }) - return - } - - // If /api/v1/memos is missing, check /api/v1/memo (v0.20/v0.21 unified). - if (isNotFoundLike(xhr)) { - global.$ - .ajax({ - url: apiUrl + v1Q, - method: 'GET', - headers: headers, - dataType: 'json' - }) - .done(function (data2) { - if (looksLikeMemosListPayload(data2)) callback({ flavor: 'v1' }) - else callback({ flavor: 'unknown' }) - }) - .fail(function () { - callback({ flavor: 'unknown' }) - }) - return - } - - callback({ flavor: 'unknown' }) - }) - } - global.MemosApiV023 = { buildFilter: buildFilter, listMemos: listMemos, - extractTagsFromMemo: extractTagsFromMemo, - probeApiFlavor: probeApiFlavor + extractTagsFromMemo: extractTagsFromMemo } })(window) diff --git a/js/i18n.js b/js/i18n.js index ade00f3..734718a 100644 --- a/js/i18n.js +++ b/js/i18n.js @@ -102,10 +102,14 @@ function setTitle(id, messageKey) { } function applyStaticI18n() { - setText('saveKey', 'saveBtn') + setText('saveSettings', 'saveBtn') setText('saveTag', 'saveBtn') setText('supportedMemosVersion', 'supportedMemosVersion') + setText('settingsConnectionTitle', 'settingsConnectionTitle') + setText('settingsConnectionDesc', 'settingsConnectionDesc') + setText('settingsPostingTitle', 'settingsPostingTitle') + setText('settingsPostingDesc', 'settingsPostingDesc') setPlaceholder('apiUrl', 'placeApiUrl') setPlaceholder('apiTokens', 'placeApiTokens') @@ -121,6 +125,7 @@ function applyStaticI18n() { setPlaceholder('hideInput', 'placeHideInput') setPlaceholder('showInput', 'placeShowInput') + setPlaceholder('attachmentOnlyDefaultText', 'placeAttachmentOnlyDefaultText') setText('uploadlist-title', 'uploadedListTitle') diff --git a/js/oper.js b/js/oper.js index fadcf01..94431db 100644 --- a/js/oper.js +++ b/js/oper.js @@ -51,131 +51,6 @@ function initProportionalEditorResize() { const editorRect = editor.getBoundingClientRect() const toolsRect = tools.getBoundingClientRect() const toolsStyle = window.getComputedStyle(tools) - const gap = parseFloat(toolsStyle.marginTop || '0') || 0 - - const availW = Math.max(0, viewportW - safety - editorRect.left) - const availH = Math.max(0, viewportH - safety - toolsRect.height - editorRect.top - gap) - - const scaleW = baseW > 0 ? availW / baseW : 1 - const scaleH = baseH > 0 ? availH / baseH : 1 - maxScale = Math.max(1, Math.min(scaleW, scaleH)) - } - - computeMaxScale() - window.addEventListener('resize', computeMaxScale) - - let dragging = false - let startX = 0 - let startY = 0 - let startScale = 1 - let rafId = 0 - let pendingScale = null - - const parseScale = (raw) => { - const s = typeof raw === 'number' && Number.isFinite(raw) - ? raw - : typeof raw === 'string' && raw.trim() !== '' && !Number.isNaN(Number(raw)) - ? Number(raw) - : 1 - return s > 0 ? s : 1 - } - - const readCurrentScale = () => { - const w = parseFloat(editor.style.width || '') - const h = parseFloat(editor.style.height || '') - const sw = baseW > 0 && Number.isFinite(w) ? w / baseW : 1 - const sh = baseH > 0 && Number.isFinite(h) ? h / baseH : 1 - return Math.max(1, sw, sh) - } - - const applyScale = (scale) => { - const s = Math.max(1, Math.min(maxScale, scale)) - editor.style.width = `${Math.round(baseW * s)}px` - editor.style.height = `${Math.round(baseH * s)}px` - } - - const applyScaleInstant = (scale) => { - // In case CSS transitions exist (or get reintroduced), keep restores immediate. - const prevTransition = editor.style.transition - editor.style.transition = 'none' - applyScale(scale) - window.requestAnimationFrame(function () { - editor.style.transition = prevTransition - }) - } - - // Restore previously saved scale synchronously (localStorage) first. - // This makes the popup *feel* synchronous because it can apply before async chrome.storage returns. - let restoredFromLocal = false - let localScale = 1 - try { - const raw = window.localStorage ? window.localStorage.getItem(storageKey) : null - const s = parseScale(raw) - if (s && s !== 1) { - localScale = s - restoredFromLocal = true - applyScaleInstant(s) - } - } catch (_) { - // ignore - } - - // Restore from chrome.storage.sync (best-effort) and keep localStorage in sync. - try { - chrome.storage.sync.get({ [storageKey]: 1 }, function (items) { - const raw = items ? items[storageKey] : 1 - const s = parseScale(raw) - const shouldApply = !restoredFromLocal || Math.abs(s - localScale) > 1e-6 - if (shouldApply) applyScaleInstant(s) - try { - if (window.localStorage) window.localStorage.setItem(storageKey, String(s)) - } catch (_) {} - }) - } catch (_) { - // ignore - } - - const scheduleApply = () => { - if (rafId) return - rafId = window.requestAnimationFrame(() => { - rafId = 0 - if (pendingScale == null) return - const s = pendingScale - pendingScale = null - applyScale(s) - }) - } - - handle.addEventListener('pointerdown', (ev) => { - dragging = true - startX = ev.clientX - startY = ev.clientY - startScale = readCurrentScale() - computeMaxScale() - try { handle.setPointerCapture(ev.pointerId) } catch (_) {} - ev.preventDefault() - }) - - handle.addEventListener('pointermove', (ev) => { - if (!dragging) return - const dx = ev.clientX - startX - const dy = ev.clientY - startY - - // Proportional scale based on diagonal length for smoother, more linear feel. - const diag0 = Math.hypot(baseW, baseH) - const targetW = baseW * startScale + dx - const targetH = baseH * startScale + dy - const diag1 = Math.hypot(targetW, targetH) - const next = diag0 > 0 ? diag1 / diag0 : startScale - - pendingScale = next - scheduleApply() - }) - - const endDrag = () => { - dragging = false - - // Flush any pending RAF update before persisting. if (pendingScale != null) { applyScale(pendingScale) pendingScale = null @@ -270,6 +145,23 @@ window.addEventListener('i18n:changed', (ev) => { let relistNow = [] +const API_FLAVOR_V020_V021 = 'v020-v021' + +const DEFAULT_ATTACHMENT_ONLY_TEXT = '#附件 此为默认填充,如需自定义,请在发送附件前填写你的文本内容或者设置项里自定义.' + +function getAttachmentOnlyDefaultText(customText) { + const value = typeof customText === 'string' ? customText.trim() : '' + return value || DEFAULT_ATTACHMENT_ONLY_TEXT +} + +function resolveSendContent(rawContent, resources, customText) { + const value = typeof rawContent === 'string' ? rawContent : '' + if (value.trim() !== '') return value + const items = Array.isArray(resources) ? resources : [] + if (items.length === 0) return '' + return getAttachmentOnlyDefaultText(customText) +} + function get_info(callback) { chrome.storage.sync.get( { @@ -283,7 +175,8 @@ function get_info(callback) { open_content: '', userid: '', memoUiPath: 'memos', - resourceIdList: [] + resourceIdList: [], + attachmentOnlyDefaultText: '' }, function (items) { var flag = false @@ -305,18 +198,18 @@ function get_info(callback) { returnObject.userid = items.userid returnObject.memoUiPath = items.memoUiPath returnObject.resourceIdList = items.resourceIdList + returnObject.attachmentOnlyDefaultText = items.attachmentOnlyDefaultText if (callback) callback(returnObject) } ) } -function isV023Flavor(info) { - return info && info.apiFlavor === 'v023' && window.MemosApiV023 -} - -function isV1Flavor(info) { - return info && info.apiFlavor === 'v1' && window.MemosApiV1 +function getApiAdapter(info) { + if (window.MemosApiAdapter && typeof window.MemosApiAdapter.resolve === 'function') { + return window.MemosApiAdapter.resolve(info) + } + return null } function getMemoUid(memo) { @@ -344,6 +237,7 @@ get_info(function (info) { $('#apiTokens').val(info.apiTokens) $('#hideInput').val(info.hidetag) $('#showInput').val(info.showtag) + $('#attachmentOnlyDefaultText').val(info.attachmentOnlyDefaultText) if (info.open_action === 'upload_image') { //打开的时候就是上传图片 uploadImage(info.open_content) @@ -559,10 +453,11 @@ function memoFromNow(memo) { } function hydrateV1PreviewImages(info) { - if (!isV1Flavor(info)) return - if (!info || !info.apiUrl || !info.apiTokens) return + const adapter = getApiAdapter(info) + if (!adapter || !adapter.needsAuthenticatedImagePreview()) return + if (!info || !info.apiUrl) return - const token = String(info.apiTokens) + const token = info && info.apiTokens != null ? String(info.apiTokens).trim() : '' let root = String(info.apiUrl) let apiOrigin = '' try { @@ -646,9 +541,10 @@ function hydrateV1PreviewImages(info) { fetch(abs, { method: 'GET', - headers: { + credentials: 'include', + headers: token ? { Authorization: 'Bearer ' + token - } + } : {} }) .then(function (res) { if (!res || !res.ok) throw new Error('HTTP ' + (res ? res.status : '0')) @@ -662,9 +558,9 @@ function hydrateV1PreviewImages(info) { img.src = objectUrl }) .catch(function () { - // Don't break previews for modern versions where plain may already work. + // Fall back to the original URL so the browser can still try cookie-based auth. if (hasAuthAttr) { - try { img.removeAttribute('src') } catch (_) {} + try { img.setAttribute('src', abs) } catch (_) {} } }) }) @@ -789,32 +685,9 @@ $(document).on('click', '.upload-del', function () { return } - const inferredId = (function () { - if (rid != null && String(rid).trim() !== '' && !Number.isNaN(Number(rid))) return Math.floor(Number(rid)) - const tail = String(name).split('/').pop() - if (tail && !Number.isNaN(Number(tail))) return Math.floor(Number(tail)) - return null - })() - - const doDelete = isV1Flavor(info) && window.MemosApiV1 && typeof window.MemosApiV1.deleteResource === 'function' && inferredId != null - ? function (onOk, onFail) { - window.MemosApiV1.deleteResource(info, inferredId, onOk, onFail) - } - : function (onOk, onFail) { - $.ajax({ - url: info.apiUrl + 'api/v1/' + name, - type: 'DELETE', - headers: { Authorization: 'Bearer ' + info.apiTokens }, - success: function (data) { - onOk(data) - }, - error: function (xhr) { - onFail(xhr) - } - }) - } - - doDelete( + const adapter = getApiAdapter(info) + adapter.deleteResource( + { name: name, id: rid }, function () { const next = (Array.isArray(relistNow) ? relistNow : []).filter(function (x) { return x && x.name !== name @@ -830,193 +703,101 @@ $(document).on('click', '.upload-del', function () { ) }) }) + function uploadImage(file) { $.message({ message: msg('picUploading'), autoClose: false }); get_info(function (info) { - if (isV1Flavor(info)) { - uploadImageNowV1(file) - return - } - - const reader = new FileReader(); - reader.onload = function(e) { - const base64String = e.target.result.split(',')[1]; - uploadImageNow(base64String, file); - }; - reader.onerror = function(error) { - console.error('Error reading file:', error); - }; - reader.readAsDataURL(file); - }) -}; - -function uploadImageNowV1(file) { - get_info(function (info) { - if (!info.status) { - $.message({ message: msg('placeApiUrl') }) - return - } - - let old_name = file.name.split('.') - let file_ext = file.name.split('.').pop() - let now = dayjs().format('YYYYMMDDHHmmss') - let new_name = old_name[0] + '_' + now + '.' + file_ext - - window.MemosApiV1.uploadResourceBlob( - info, + const adapter = getApiAdapter(info) + adapter.uploadFile( file, - { filename: new_name, type: file.type }, + { + editorContent: $("textarea[name=text]").val(), + hideTag: info.hidetag, + showTag: info.showtag, + memoLock: info.memo_lock + }, function (entity) { - const inferredId = (function () { - if (!entity) return null - const v = entity.id != null ? entity.id : entity.ID != null ? entity.ID : entity.Id - if (typeof v === 'number' && Number.isFinite(v)) return Math.floor(v) - if (typeof v === 'string' && v.trim() !== '' && !Number.isNaN(Number(v))) return Math.floor(Number(v)) - return null - })() - - // v0.18: resource entity has no `name`, only `id/filename/type/...`. - // Treat having an id as a successful upload. - if (entity && (entity.name || inferredId != null)) { - const name = entity.name || (inferredId != null ? 'resources/' + String(inferredId) : '') - relistNow.push({ - id: inferredId != null ? inferredId : entity.id, - name: name, - filename: entity.filename || new_name, - createTime: entity.createTime || entity.createdTs || entity.createdAt, - type: entity.type - }) + if (entity) { + relistNow.push(entity) chrome.storage.sync.set({ open_action: '', open_content: '', resourceIdList: relistNow }, function () { $.message({ message: msg('picSuccess') }) + renderUploadList(relistNow) }) return } - chrome.storage.sync.set({ open_action: '', open_content: '' }, function () { $.message({ message: msg('picFailed') }) }) }, function () { - $.message({ message: msg('picFailed') }) + chrome.storage.sync.set({ open_action: '', open_content: '' }, function () { + $.message({ message: msg('picFailed') }) + }) } ) }) +}; + +function buildCustomSettingsPayload() { + return { + attachmentOnlyDefaultText: $('#attachmentOnlyDefaultText').val() + } } -function uploadImageNow(base64String, file) { - get_info(function(info) { - if (info.status) { - let old_name = file.name.split('.'); - let file_ext = file.name.split('.').pop(); - let now = dayjs().format('YYYYMMDDHHmmss'); - let new_name = old_name[0] + '_' + now + '.' + file_ext; - var hideTag = info.hidetag - var showTag = info.showtag - var nowTag = $("textarea[name=text]").val().match(/(#[^\s#]+)/) - var sendvisi = info.memo_lock || '' - if(nowTag){ - if(nowTag[1] == showTag){ - sendvisi = 'PUBLIC' - }else if(nowTag[1] == hideTag){ - sendvisi = 'PRIVATE' - } - } - const data = { - content: base64String, - visibility: sendvisi, - filename: new_name, - type: file.type - }; - window.MemosApi.uploadAttachmentOrResource( - info, - data, - function (resp) { - const entity = (resp && resp.resource) || resp - if (entity && entity.name) { - relistNow.push({ - name: entity.name, - filename: entity.filename || new_name, - createTime: entity.createTime, - type: entity.type - }) - chrome.storage.sync.set( - { - open_action: '', - open_content: '', - resourceIdList: relistNow - }, - function () { - $.message({ message: msg('picSuccess') }) - } - ) - return - } - - chrome.storage.sync.set( - { - open_action: '', - open_content: '', - resourceIdList: [] - }, - function () { - $.message({ message: msg('picFailed') }) - } - ) - }, - function () { - $.message({ message: msg('picFailed') }) - } - ) - }else { - $.message({ - message: msg('placeApiUrl') - }) - } - }); -} - -$('#saveKey').click(function () { +function saveSettingsPanel() { var apiUrl = $('#apiUrl').val() if (apiUrl.length > 0 && !apiUrl.endsWith('/')) { - apiUrl += '/'; + apiUrl += '/' } var apiTokens = $('#apiTokens').val() + var customSettings = buildCustomSettingsPayload() - window.MemosApi.authWithFallback(apiUrl, apiTokens, function (auth) { + if (!apiUrl && !apiTokens) { + chrome.storage.sync.set(customSettings, function () { + $.message({ message: msg('saveSuccess') }) + $('#blog_info').slideUp(200) + }) + return + } + + window.MemosApiModern.authWithFallback(apiUrl, apiTokens, function (auth) { if (!auth || auth.userId == null) { $.message({ message: msg('invalidToken') }) return } chrome.storage.sync.set( - { + Object.assign({}, customSettings, { apiUrl: apiUrl, apiTokens: apiTokens, userid: auth.userId, memoUiPath: auth.uiPath || 'memos', apiFlavor: '' - }, + }), function () { $.message({ message: msg('saveSuccess') }) $('#blog_info').hide() // Auto-detect API flavor once; keep default behavior when unknown. - if (window.MemosApiV023 && typeof window.MemosApiV023.probeApiFlavor === 'function') { - window.MemosApiV023.probeApiFlavor(apiUrl, apiTokens, function (res) { + if (window.MemosApiAdapter && typeof window.MemosApiAdapter.probeFlavor === 'function') { + window.MemosApiAdapter.probeFlavor(apiUrl, apiTokens, function (res) { const flavor = res && res.flavor ? res.flavor : '' - const normalized = flavor === 'v020' || flavor === 'v021' ? 'v1' : flavor - if (normalized === 'v1' || normalized === 'v023' || normalized === 'modern') { - chrome.storage.sync.set({ apiFlavor: normalized }) + if (window.MemosApiAdapter.KNOWN_FLAVORS.indexOf(flavor) !== -1) { + chrome.storage.sync.set({ apiFlavor: flavor }) } }) } } ) }) -}); +} + +$('#saveSettings').click(function () { + saveSettingsPanel() +}) $('#opensite').click(function () { get_info(function (info) { @@ -1028,9 +809,8 @@ $('#opensite').click(function () { $('#tags').click(function () { get_info(function (info) { if (info.apiUrl) { - var parent = `users/${info.userid}`; - // 从最近的1000条memo中获取tags,因此不保证获取能全部的 var tagDom = ""; + const adapter = getApiAdapter(info) const renderTags = function (tags) { const uniTags = [...new Set((Array.isArray(tags) ? tags : []).filter(Boolean))] @@ -1041,62 +821,9 @@ $('#tags').click(function () { $("#taglist").html(tagDom).slideToggle(500) } - const onTagsData = function (data) { - const memos = window.MemosApi.extractMemosListFromResponse(data) - - const allTags = memos.flatMap(function (memo) { - if (!memo) return [] - // v0.23 response may include `tags: []` while actual tags live in `memo.property.tags`. - // So when v0.23 flavor is detected, always use the compat extractor first. - if (isV023Flavor(info)) return window.MemosApiV023.extractTagsFromMemo(memo) - if (Array.isArray(memo.tags) && memo.tags.length > 0) return memo.tags - if (Array.isArray(memo.tagList) && memo.tagList.length > 0) return memo.tagList - if (memo.property && Array.isArray(memo.property.tags) && memo.property.tags.length > 0) { - return memo.property.tags - } - return [] - }) - const uniTags = [...new Set(allTags.filter(Boolean))] - - renderTags(uniTags) - } - - if (isV1Flavor(info)) { - window.MemosApiV1.getTagSuggestion( - info, - function (tags) { - renderTags(Array.isArray(tags) ? tags : []) - }, - function () { - $.message({ message: msg('placeApiUrl') }) - } - ) - } else if (isV023Flavor(info)) { - const filterExpr = window.MemosApiV023.buildFilter({ - rowStatus: 'NORMAL', - creator: 'users/' + info.userid - }) - window.MemosApiV023.listMemos( - info, - { - pageSize: 1000, - filterExpr: filterExpr - }, - onTagsData, - function () { - $.message({ message: msg('placeApiUrl') }) - } - ) - } else { - window.MemosApi.fetchMemosWithFallback( - info, - '?pageSize=1000', - onTagsData, - function () { - $.message({ message: msg('placeApiUrl') }) - } - ) - } + adapter.listTags(renderTags, function () { + $.message({ message: msg('placeApiUrl') }) + }) } else { $.message({ message: msg('placeApiUrl') @@ -1107,10 +834,10 @@ $('#tags').click(function () { $(document).on("click","#hideTag",function () { $('#taghide').slideToggle(500) + $('#hideInput').trigger('focus') }) $('#saveTag').click(function () { - // 保存数据 chrome.storage.sync.set( { hidetag: $('#hideInput').val(), @@ -1142,32 +869,15 @@ $(document).on("click",".item-lock",function () { $('#search').click(function () { get_info(function (info) { const pattern = $("textarea[name=text]").val() - var parent = `users/${info.userid}`; - const patternLiteral = JSON.stringify(String(pattern || '')) - var filter = "?filter=" + encodeURIComponent(`visibility in ["PUBLIC","PROTECTED"] && content.contains(${patternLiteral})`); if (info.status) { $("#randomlist").html('').hide() var searchDom = "" if(pattern){ - const runSearch = isV023Flavor(info) - ? function (onOk, onFail) { - const filterExpr = window.MemosApiV023.buildFilter({ - visibilities: ['PUBLIC', 'PROTECTED'], - contentSearch: String(pattern) - }) - window.MemosApiV023.listMemos(info, { pageSize: 1000, filterExpr: filterExpr }, onOk, onFail) - } - : isV1Flavor(info) - ? function (onOk, onFail) { - window.MemosApiV1.listMemos(info, { limit: 1000, rowStatus: 'NORMAL', contentSearch: String(pattern) }, onOk, onFail) - } - : function (onOk, onFail) { - window.MemosApi.fetchMemosWithFallback(info, filter, onOk, onFail) - } + const adapter = getApiAdapter(info) - runSearch( - function (data) { - let searchData = window.MemosApi.extractMemosListFromResponse(data) + adapter.searchMemos( + pattern, + function (searchData) { if(searchData.length == 0){ $.message({ message: msg('searchNone') @@ -1192,7 +902,7 @@ $('#search').click(function () { continue } if(restype == 'image'){ - if (isV1Flavor(info)) { + if (adapter.needsAuthenticatedImagePreview()) { searchDom += '' } else { searchDom += '' @@ -1229,26 +939,12 @@ $('#search').click(function () { $('#random').click(function () { get_info(function (info) { - var parent = `users/${info.userid}`; if (info.status) { $("#randomlist").html('').hide() - const runRandom = isV023Flavor(info) - ? function (onOk, onFail) { - const filterExpr = window.MemosApiV023.buildFilter({ visibilities: ['PUBLIC', 'PROTECTED'] }) - window.MemosApiV023.listMemos(info, { pageSize: 1000, filterExpr: filterExpr }, onOk, onFail) - } - : isV1Flavor(info) - ? function (onOk, onFail) { - window.MemosApiV1.listMemos(info, { limit: 1000, rowStatus: 'NORMAL' }, onOk, onFail) - } - : function (onOk, onFail) { - const filter = "?filter=" + encodeURIComponent(`visibility in ["PUBLIC","PROTECTED"]`); - window.MemosApi.fetchMemosWithFallback(info, filter, onOk, onFail) - } + const adapter = getApiAdapter(info) - runRandom( - function (data) { - const memos = window.MemosApi.extractMemosListFromResponse(data) + adapter.listRandomMemos( + function (memos) { let randomNum = Math.floor(Math.random() * (memos.length)); var randomData = memos[randomNum] randDom(randomData) @@ -1267,6 +963,7 @@ $('#random').click(function () { function randDom(randomData){ get_info(function (info) { + const adapter = getApiAdapter(info) var memosID = getMemoUid(randomData) var timeText = memoFromNow(randomData) var randomDom = '
'+timeText+'
'+(randomData && randomData.content ? randomData.content : '').replace(/!\[.*?\]\((.*?)\)/g,' ').replace(/\[(.*?)\]\((.*?)\)/g,' $1 ')+'
' @@ -1285,7 +982,7 @@ function randDom(randomData){ continue } if(restype == 'image'){ - if (isV1Flavor(info)) { + if (adapter.needsAuthenticatedImagePreview()) { randomDom += '' } else { randomDom += '' @@ -1313,49 +1010,20 @@ $(document).on("click","#random-link",function () { $(document).on("click","#random-delete",function () { get_info(function (info) { - // var memoUid = $("#random-delete").data('uid'); var memosName = $("#random-delete").data('name'); var memoId = $("#random-delete").data('id'); - // v0.20/v0.21: archive memo via API v1 PATCH /api/v1/memo/:id - if (isV1Flavor(info) && memoId) { - window.MemosApiV1.patchMemo( - info, - memoId, - { rowStatus: 'ARCHIVED' }, - function () { - $("#randomlist").html('').hide() - $.message({ message: msg('archiveSuccess') }) - }, - function () { - $.message({ message: msg('archiveFailed') }) - } - ) - return - } - - var deleteUrl = info.apiUrl+'api/v1/'+memosName - $.ajax({ - url:deleteUrl, - type:"PATCH", - data:JSON.stringify({ - // 'uid': memoUid, - 'state': "ARCHIVED" - }), - contentType:"application/json", - dataType:"json", - headers : {'Authorization':'Bearer ' + info.apiTokens}, - success: function(result){ - $("#randomlist").html('').hide() - $.message({ - message: msg('archiveSuccess') - }) - },error:function(err){//清空open_action(打开时候进行的操作),同时清空open_content - $.message({ - message: msg('archiveFailed') - }) - } - }) + const adapter = getApiAdapter(info) + adapter.archiveMemo( + { name: memosName, id: memoId }, + function () { + $("#randomlist").html('').hide() + $.message({ message: msg('archiveSuccess') }) + }, + function () { + $.message({ message: msg('archiveFailed') }) + } + ) }) }) @@ -1416,8 +1084,13 @@ $('#blog_info_edit').click(function () { $('#content_submit_text').click(function () { var contentVal = $("textarea[name=text]").val() - if(contentVal){ - sendText() + var contentToSend = resolveSendContent( + contentVal, + relistNow, + $('#attachmentOnlyDefaultText').val() + ) + if(contentToSend){ + sendText(contentToSend) }else{ $.message({ message: msg('placeContent') @@ -1429,16 +1102,9 @@ function getOne(memosId){ get_info(function (info) { if (info.apiUrl) { $("#randomlist").html('').hide() - var getUrl = isV1Flavor(info) ? info.apiUrl+'api/v1/memo/'+memosId : info.apiUrl+'api/v1/'+memosId - $.ajax({ - url:getUrl, - type:"GET", - contentType:"application/json", - dataType:"json", - headers : {'Authorization':'Bearer ' + info.apiTokens}, - success: function(data){ - randDom(data) - } + const adapter = getApiAdapter(info) + adapter.getMemo(memosId, function (memoEntity) { + randDom(memoEntity) }) } else { $.message({ @@ -1448,17 +1114,27 @@ function getOne(memosId){ }) } -function sendText() { +function sendText(preparedContent) { get_info(function (info) { if (info.status) { $.message({ message: msg('memoUploading') }) //$("#content_submit_text").attr('disabled','disabled'); - let content = $("textarea[name=text]").val() + let content = resolveSendContent( + typeof preparedContent === 'string' ? preparedContent : $("textarea[name=text]").val(), + info.resourceIdList, + info.attachmentOnlyDefaultText + ) + if (!content) { + $.message({ + message: msg('placeContent') + }) + return + } var hideTag = info.hidetag var showTag = info.showtag - var nowTag = $("textarea[name=text]").val().match(/(#[^\s#]+)/) + var nowTag = content.match(/(#[^\s#]+)/) var sendvisi = info.memo_lock || '' if(nowTag){ if(nowTag[1] == showTag){ @@ -1468,106 +1144,34 @@ function sendText() { } } - // Memos v0.20/v0.21: use /api/v1/memo and bind resources by numeric IDs. - if (isV1Flavor(info)) { - const items = Array.isArray(info.resourceIdList) ? info.resourceIdList : [] - const resourceIdList = items - .map(function (r) { - if (!r) return null - if (typeof r.id === 'number' && Number.isFinite(r.id)) return Math.floor(r.id) - if (typeof r.id === 'string' && r.id.trim() !== '' && !Number.isNaN(Number(r.id))) { - return Math.floor(Number(r.id)) - } - // Some versions store name as resources/{id}. - const n = typeof r.name === 'string' ? r.name : '' - const tail = n ? n.split('/').pop() : '' - if (tail && !Number.isNaN(Number(tail))) return Math.floor(Number(tail)) - return null - }) - .filter(function (x) { - return x != null && Number.isFinite(x) - }) - - window.MemosApiV1.createMemo( - info, - { - content: content, - visibility: sendvisi, - resourceIdList: resourceIdList - }, - function (data) { - chrome.storage.sync.set( - { open_action: '', open_content: '', resourceIdList: [] }, - function () { - $.message({ message: msg('memoSuccess') }) - $("textarea[name=text]").val('') - relistNow = [] - renderUploadList(relistNow) - randDom(data) - } - ) - }, - function () { - chrome.storage.sync.set( - { open_action: '', open_content: '' }, - function () { - $.message({ message: msg('memoFailed') }) - } - ) - } - ) - return - } - - $.ajax({ - url:info.apiUrl+'api/v1/memos', - type:"POST", - data:JSON.stringify({ - 'content': content, - 'visibility': sendvisi - }), - contentType:"application/json", - dataType:"json", - headers : {'Authorization':'Bearer ' + info.apiTokens}, - success: function(data){ - if(info.resourceIdList.length > 0 ){ - //匹配图片 - window.MemosApi.patchMemoWithAttachmentsOrResources( - info, - data.name, - info.resourceIdList, - function () { - getOne(data.name) - }, - function () { - getOne(data.name) - } - ) - }else{ - getOne(data.name) - } + const adapter = getApiAdapter(info) + adapter.createMemo( + { + content: content, + visibility: sendvisi, + resourceIdList: info.resourceIdList + }, + function (data) { chrome.storage.sync.set( - { open_action: '', open_content: '',resourceIdList:[]}, + { open_action: '', open_content: '', resourceIdList: [] }, function () { - $.message({ - message: msg('memoSuccess') - }) - //$("#content_submit_text").removeAttr('disabled'); + $.message({ message: msg('memoSuccess') }) $("textarea[name=text]").val('') relistNow = [] renderUploadList(relistNow) + randDom(data) } ) - },error:function(err){//清空open_action(打开时候进行的操作),同时清空open_content - chrome.storage.sync.set( - { open_action: '', open_content: '',resourceIdList:[] }, - function () { - $.message({ - message: msg('memoFailed') - }) - } - )}, - }) + }, + function () { + chrome.storage.sync.set( + { open_action: '', open_content: '', resourceIdList: [] }, + function () { + $.message({ message: msg('memoFailed') }) + } + ) + } + ) } else { $.message({ message: msg('placeApiUrl') diff --git a/manifest.json b/manifest.json index 757083e..5a245af 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 3, "name": "__MSG_extName__", "default_locale": "en", - "version": "2026.04.21", + "version": "2026.04.22", "version_name": "Supports 0.15.0 - 0.27.x", "action": { "default_popup": "popup.html", diff --git a/popup.html b/popup.html index c4bc72c..ad1741e 100644 --- a/popup.html +++ b/popup.html @@ -34,10 +34,13 @@
-
+
+
+
+
-
+
+
+
+
+ +
+
+ +
@@ -135,26 +153,25 @@
+ id="hideInput" + class="inputer" + name="hideInput" + type="text" + value="" + maxlength="50" + placeholder="" + /> - + id="showInput" + class="inputer" + name="showInput" + type="text" + value="" + maxlength="50" + placeholder="" + /> +
-
@@ -168,9 +185,10 @@ - - + + +