向前兼容memos到v0.18

Introduce compatibility layers and runtime detection for older Memos API variants (v0.18/v0.19/v0.20/v0.21/v0.23 and older). Added js/compat/memosApi.v023.js and js/compat/memosApi.v1.js to provide filter building, listing, upload and resource helpers for legacy endpoints. Updated js/oper.js to track apiFlavor, probe/detect flavor on save, and to adapt uploads, deletes, tag listing, search and preview image hydration to the appropriate API flavor (including buildV1ResourceStreamUrl, normalizeUnixTimeToMs, getMemoUid and other helpers). Also updated user-facing strings and README to reflect broader compatibility (locales and README changes) and minor UI/manifest popup adjustments. These changes enable the extension to work with a wider range of Memos server versions while preserving existing behavior for modern endpoints.
This commit is contained in:
jonny
2026-03-06 18:51:12 +08:00
parent 9a90c2e2bf
commit 30b7cf2491
11 changed files with 1067 additions and 77 deletions
+2
View File
@@ -5,6 +5,8 @@ Chrome 应用商店:<https://chrome.google.com/webstore/detail/memos-bber/cbhj
一个通过浏览器插件发布 [Memos](https://usememos.com/) 的插件。基于 iSpeak-bber 修改,原作者为 [DreamyTZK](https://www.antmoe.com/)。
## 更新日志
### 20260308 向前兼容到0.18.0,可能再往前也行,只测试到0.18.0
- 20260307 增加语言切换按钮以及韩语和日语支持,
- 2026年03月06日 右键菜单发送选中文本附带原文链接
- 2026年03月05日 向前兼容到0.24.0,可能再往前也行,因为只测试了0.24.0和0.25.0以及当前最新版本,如有更早版本需求,可issue反馈
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "Save"
},
"supportedMemosVersion": {
"message": "Compatible with Memos v0.24.0+ (tested: 0.24.0 / 0.25.0 / 0.26.x)"
"message": "Compatible with Memos v0.18.0 - 0.26.x"
},
"placeApiUrl":{
"message": "Memos site URL"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "保存"
},
"supportedMemosVersion": {
"message": "Memos v0.24.0+ に対応(テスト済み: 0.24.0 / 0.25.0 / 0.26.x"
"message": "Memos v0.18.0 - 0.26.x に対応"
},
"placeApiUrl": {
"message": "Memos サイトURL"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "저장"
},
"supportedMemosVersion": {
"message": "Memos v0.24.0+ 호환 (테스트됨: 0.24.0 / 0.25.0 / 0.26.x)"
"message": "Memos v0.18.0 - 0.26.x 호환"
},
"placeApiUrl": {
"message": "Memos 사이트 URL"
+1 -1
View File
@@ -21,7 +21,7 @@
"message": "保存"
},
"supportedMemosVersion": {
"message": "兼容 Memos v0.24.0+(已测试:0.24.0 / 0.25.0 / 0.26.x"
"message": "兼容 Memos v0.18.0 - 0.26.x"
},
"placeApiUrl":{
"message": "请填入 Memos 主页网址"
+222
View File
@@ -0,0 +1,222 @@
(function (global) {
'use strict'
function buildFilter(parts) {
const p = parts || {}
const exprs = []
if (p.creator) {
// v0.23 expects a CEL string variable `creator`.
exprs.push('creator == ' + JSON.stringify(String(p.creator)))
}
if (Array.isArray(p.visibilities) && p.visibilities.length > 0) {
const list = p.visibilities.map(function (v) {
return JSON.stringify(String(v))
})
exprs.push('visibilities == [' + list.join(',') + ']')
}
if (typeof p.contentSearch === 'string' && p.contentSearch.length > 0) {
exprs.push('content_search == [' + JSON.stringify(String(p.contentSearch)) + ']')
}
if (typeof p.rowStatus === 'string' && p.rowStatus.length > 0) {
exprs.push('row_status == ' + JSON.stringify(String(p.rowStatus)))
}
if (Array.isArray(p.tagSearch) && p.tagSearch.length > 0) {
const list = p.tagSearch.map(function (t) {
return JSON.stringify(String(t).replace(/^#/, ''))
})
exprs.push('tag_search == [' + list.join(',') + ']')
}
if (typeof p.random === 'boolean') {
exprs.push('random == ' + (p.random ? 'true' : 'false'))
}
if (typeof p.limit === 'number' && Number.isFinite(p.limit) && p.limit > 0) {
exprs.push('limit == ' + String(Math.floor(p.limit)))
}
return exprs.join(' && ')
}
function extractTagsFromMemo(memo) {
if (!memo) return []
// v0.23: tags live in memo.property.tags
if (memo.property && Array.isArray(memo.property.tags)) return memo.property.tags
// Defensive: some versions/serializers may use `properties` instead of `property`.
if (memo.properties && Array.isArray(memo.properties.tags)) return memo.properties.tags
// Defensive: some JSON serializers may wrap repeated fields.
if (memo.property && memo.property.tags && Array.isArray(memo.property.tags.values)) {
return memo.property.tags.values
}
if (memo.properties && memo.properties.tags && Array.isArray(memo.properties.tags.values)) {
return memo.properties.tags.values
}
// Fallback: parse tags from content, e.g. "#tag".
const content = typeof memo.content === 'string' ? memo.content : ''
if (!content) return []
const found = []
// Match any hashtag token; server-side parser is stricter, but we want a lenient UI fallback.
const re = /#([^\s#]+)/g
let m
while ((m = re.exec(content))) {
let tag = m[1] || ''
// Trim trailing punctuation/brackets commonly attached in markdown.
tag = tag.replace(/[\]\[\)\(\}\{"'.,;:!?]+$/g, '')
tag = tag.replace(/^#+/, '')
tag = tag.trim()
if (!tag) continue
if (tag.length > 64) tag = tag.slice(0, 64)
found.push(tag)
}
return Array.from(new Set(found))
}
function listMemos(info, options, success, fail) {
const opt = options || {}
const pageSize = opt.pageSize && Number.isFinite(opt.pageSize) ? Math.max(1, Math.floor(opt.pageSize)) : 1000
const filterExpr = typeof opt.filterExpr === 'string' ? opt.filterExpr : ''
const qs =
'?pageSize=' +
encodeURIComponent(String(pageSize)) +
(filterExpr ? '&filter=' + encodeURIComponent(filterExpr) : '')
// v0.23 removed the user-scoped memos endpoint: `/api/v1/users/{id}/memos`.
// Don't reuse fetchMemosWithFallback() because it will always emit an extra 404 first.
global.$
.ajax({
url: info.apiUrl + 'api/v1/memos' + qs,
type: 'GET',
contentType: 'application/json',
dataType: 'json',
headers: { Authorization: 'Bearer ' + info.apiTokens }
})
.done(function (data) {
success(data)
})
.fail(function (xhr) {
if (fail) fail(xhr)
})
}
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
}
})(window)
+286
View File
@@ -0,0 +1,286 @@
(function (global) {
'use strict'
function isNotFoundLikeXhr(jqXhr) {
const status = jqXhr && jqXhr.status
return status === 404 || status === 405
}
function extractMemoListFromResponse(data) {
if (!data) return []
if (Array.isArray(data)) return data
if (Array.isArray(data.memos)) return data.memos
if (data.data && Array.isArray(data.data.memos)) return data.data.memos
if (Array.isArray(data.list)) return data.list
return []
}
function extractMemoEntityFromResponse(data) {
if (!data) return data
if (data.memo) return data.memo
if (data.data && data.data.memo) return data.data.memo
if (data.data && (data.data.id != null || data.data.name || data.data.content)) return data.data
return data
}
function extractResourceEntityFromResponse(data) {
if (!data) return data
if (data.resource) return data.resource
if (data.data && data.data.resource) return data.data.resource
if (data.data && (data.data.id != null || data.data.name || data.data.filename)) return data.data
return data
}
function requestGet(url, headers, success, fail) {
global.$
.ajax({
url: url,
type: 'GET',
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
if (success) success(data)
})
.fail(function (xhr) {
if (fail) fail(xhr)
})
}
function requestPostJson(url, headers, body, success, fail) {
global.$
.ajax({
url: url,
type: 'POST',
contentType: 'application/json',
dataType: 'json',
data: body != null ? JSON.stringify(body) : null,
headers: headers
})
.done(function (data) {
if (success) success(data)
})
.fail(function (xhr) {
if (fail) fail(xhr)
})
}
function requestPatchJson(url, headers, body, success, fail) {
global.$
.ajax({
url: url,
type: 'PATCH',
contentType: 'application/json',
dataType: 'json',
data: body != null ? JSON.stringify(body) : null,
headers: headers
})
.done(function (data) {
if (success) success(data)
})
.fail(function (xhr) {
if (fail) fail(xhr)
})
}
// v1 memo list: GET /api/v1/memo
// Query params (v0.20/v0.21): limit/offset/rowStatus/content/tag (best-effort)
function listMemos(info, options, success, fail) {
const opt = options || {}
const headers = { Authorization: 'Bearer ' + info.apiTokens }
const limit = opt.limit && Number.isFinite(opt.limit) ? Math.max(1, Math.floor(opt.limit)) : 1000
const offset = opt.offset && Number.isFinite(opt.offset) ? Math.max(0, Math.floor(opt.offset)) : null
const rowStatus = typeof opt.rowStatus === 'string' && opt.rowStatus ? opt.rowStatus : 'NORMAL'
const content = typeof opt.contentSearch === 'string' ? opt.contentSearch : ''
const tag = typeof opt.tagSearch === 'string' ? opt.tagSearch : ''
let qs = '?limit=' + encodeURIComponent(String(limit))
if (offset != null) qs += '&offset=' + encodeURIComponent(String(offset))
if (rowStatus) qs += '&rowStatus=' + encodeURIComponent(String(rowStatus))
if (content) qs += '&content=' + encodeURIComponent(String(content))
if (tag) qs += '&tag=' + encodeURIComponent(String(tag).replace(/^#/, ''))
requestGet(
info.apiUrl + 'api/v1/memo' + qs,
headers,
function (data) {
if (success) success({ memos: extractMemoListFromResponse(data) })
},
function (xhr) {
// Some builds might expose plural `/api/v1/memos`; try as a last resort (still v1).
if (isNotFoundLikeXhr(xhr)) {
requestGet(
info.apiUrl + 'api/v1/memos' + qs,
headers,
function (data2) {
if (success) success({ memos: extractMemoListFromResponse(data2) })
},
fail
)
return
}
if (fail) fail(xhr)
}
)
}
function createMemo(info, body, success, fail) {
const headers = { Authorization: 'Bearer ' + info.apiTokens }
requestPostJson(
info.apiUrl + 'api/v1/memo',
headers,
body,
function (data) {
if (success) success(extractMemoEntityFromResponse(data))
},
function (xhr) {
// Last resort: plural route.
if (isNotFoundLikeXhr(xhr)) {
requestPostJson(
info.apiUrl + 'api/v1/memos',
headers,
body,
function (data2) {
if (success) success(extractMemoEntityFromResponse(data2))
},
fail
)
return
}
if (fail) fail(xhr)
}
)
}
function patchMemo(info, memoId, patch, success, fail) {
const headers = { Authorization: 'Bearer ' + info.apiTokens }
const id = memoId != null ? String(memoId) : ''
if (!id) {
if (fail) fail({ status: 400 })
return
}
requestPatchJson(
info.apiUrl + 'api/v1/memo/' + encodeURIComponent(id),
headers,
patch,
function (data) {
if (success) success(extractMemoEntityFromResponse(data))
},
fail
)
}
function getTagList(info, success, fail) {
const headers = { Authorization: 'Bearer ' + info.apiTokens }
requestGet(
info.apiUrl + 'api/v1/tag',
headers,
function (data) {
const list = Array.isArray(data) ? data : Array.isArray(data.tags) ? data.tags : []
const out = list
.map(function (t) {
if (!t) return ''
if (typeof t === 'string') return t
if (typeof t.name === 'string') return t.name
if (typeof t.tag === 'string') return t.tag
return ''
})
.map(function (s) {
return String(s).replace(/^#/, '').trim()
})
.filter(Boolean)
if (success) success(out)
},
fail
)
}
function getTagSuggestion(info, success, fail) {
const headers = { Authorization: 'Bearer ' + info.apiTokens }
requestGet(
info.apiUrl + 'api/v1/tag/suggestion',
headers,
function (data) {
const list = Array.isArray(data) ? data : []
const out = list
.map(function (s) {
return String(s).replace(/^#/, '').trim()
})
.filter(Boolean)
if (success) success(out)
},
function (xhr) {
// Some forks might only expose list.
if (isNotFoundLikeXhr(xhr)) {
getTagList(info, success, fail)
return
}
if (fail) fail(xhr)
}
)
}
function uploadResourceBlob(info, file, meta, success, fail) {
const headers = { Authorization: 'Bearer ' + info.apiTokens }
const url = info.apiUrl + 'api/v1/resource/blob'
const m = meta || {}
const filename = String(m.filename || (file && file.name) || 'upload')
const form = new FormData()
if (file) form.append('file', file, filename)
global.$
.ajax({
url: url,
type: 'POST',
data: form,
processData: false,
contentType: false,
dataType: 'json',
headers: headers
})
.done(function (data) {
if (success) success(extractResourceEntityFromResponse(data))
})
.fail(function (xhr) {
if (fail) fail(xhr)
})
}
function deleteResource(info, resourceId, success, fail) {
const headers = { Authorization: 'Bearer ' + info.apiTokens }
const id = resourceId != null ? String(resourceId) : ''
if (!id) {
if (fail) fail({ status: 400 })
return
}
global.$
.ajax({
url: info.apiUrl + 'api/v1/resource/' + encodeURIComponent(id),
type: 'DELETE',
headers: headers
})
.done(function (data) {
if (success) success(data)
})
.fail(function (xhr) {
if (fail) fail(xhr)
})
}
global.MemosApiV1 = {
listMemos: listMemos,
createMemo: createMemo,
patchMemo: patchMemo,
getTagList: getTagList,
getTagSuggestion: getTagSuggestion,
uploadResourceBlob: uploadResourceBlob,
deleteResource: deleteResource
}
})(window)
+2
View File
@@ -45,6 +45,8 @@
{ method: 'GET', path: 'api/v1/auth/me', uiPath: 'memos' },
// v0.25: session-based auth service still accepts bearer tokens and returns { user: ... }.
{ method: 'GET', path: 'api/v1/auth/sessions/current', uiPath: 'memos' },
// v0.20: current user endpoint.
{ method: 'GET', path: 'api/v1/user/me', uiPath: 'm' },
{ method: 'POST', path: 'api/v1/auth/status', uiPath: 'm' },
{ method: 'GET', path: 'api/v1/auth/status', uiPath: 'm' }
]
+547 -71
View File
@@ -70,6 +70,7 @@ function get_info(callback) {
{
apiUrl: '',
apiTokens: '',
apiFlavor: '',
hidetag: '',
showtag: '',
memo_lock: '',
@@ -90,6 +91,7 @@ function get_info(callback) {
returnObject.status = flag
returnObject.apiUrl = items.apiUrl
returnObject.apiTokens = items.apiTokens
returnObject.apiFlavor = items.apiFlavor
returnObject.hidetag = items.hidetag
returnObject.showtag = items.showtag
returnObject.memo_lock = items.memo_lock
@@ -104,6 +106,21 @@ function get_info(callback) {
)
}
function isV023Flavor(info) {
return info && info.apiFlavor === 'v023' && window.MemosApiV023
}
function isV1Flavor(info) {
return info && info.apiFlavor === 'v1' && window.MemosApiV1
}
function getMemoUid(memo) {
if (!memo) return ''
if (memo.uid != null && memo.uid !== '') return String(memo.uid)
if (typeof memo.name === 'string' && memo.name) return memo.name.split('/').pop()
return ''
}
get_info(function (info) {
if (info.status) {
//已经有绑定信息了,折叠
@@ -222,6 +239,210 @@ function escapeHtml(input) {
.replace(/'/g, '&#39;')
}
function buildV1ResourceStreamUrl(info, resource) {
if (!info || !info.apiUrl || !resource) return ''
// Use the configured apiUrl as the base (may include a reverse-proxy subpath).
// Do NOT reduce to origin-only, otherwise deployments like https://host/memos/ will break.
let root = String(info.apiUrl)
try {
const u = new URL(root)
u.hash = ''
u.search = ''
root = u.toString()
} catch (_) {
// keep as-is
}
if (root && !root.endsWith('/')) root += '/'
function isImageResource(r) {
if (!r) return false
const t = typeof r.type === 'string' ? r.type.toLowerCase() : ''
if (t.startsWith('image/')) return true
const fn = typeof r.filename === 'string' ? r.filename.toLowerCase() : ''
return /\.(png|jpe?g|gif|webp|bmp|svg|avif|heic)$/.test(fn)
}
function isProbablyUid(s) {
if (typeof s !== 'string') return false
const v = s.trim()
if (!v) return false
if (v.indexOf('/') !== -1) return false
if (/^\d+$/.test(v)) return false
// shortuuid v4 typically uses URL-safe base57-ish; allow a conservative charset.
return /^[A-Za-z0-9_-]{8,}$/.test(v)
}
function buildStreamUrl(uid) {
const base = root + 'o/r/' + encodeURIComponent(uid)
return isImageResource(resource) ? base + '?thumbnail=1' : base
}
const uidRaw = resource.uid != null ? resource.uid : resource.UID != null ? resource.UID : resource.Uid
const uid = typeof uidRaw === 'string' ? uidRaw : uidRaw != null ? String(uidRaw) : ''
if (uid.trim() !== '') return buildStreamUrl(uid.trim())
// Legacy versions (e.g. v0.18) may only expose numeric `id` without `uid/name`.
const idRaw = resource.id != null ? resource.id : resource.ID != null ? resource.ID : resource.Id
const id = typeof idRaw === 'number' && Number.isFinite(idRaw)
? String(Math.floor(idRaw))
: typeof idRaw === 'string' && idRaw.trim() !== '' && !Number.isNaN(Number(idRaw))
? String(Math.floor(Number(idRaw)))
: ''
if (id) return buildStreamUrl(id)
// Fallback for older resource shapes.
const name = typeof resource.name === 'string' ? resource.name : ''
// In some memo payloads, the uid may appear as `name` directly.
// Example: name="ETU6hjuR..." should map to /o/r/:uid, not /file/:name/:filename.
if (isProbablyUid(name)) return buildStreamUrl(name.trim())
const fileId = resource.publicId || resource.filename
if (name && fileId) return root + 'file/' + name + '/' + fileId
return ''
}
function normalizeUnixTimeToMs(input) {
if (input == null) return null
if (typeof input === 'number' && Number.isFinite(input)) {
// Heuristic: seconds are typically 10 digits; milliseconds are 13 digits.
if (input > 0 && input < 1e12) return input * 1000
return input
}
if (typeof input === 'string') {
const s = input.trim()
if (/^\d+$/.test(s)) {
const n = Number(s)
if (!Number.isFinite(n)) return null
if (n > 0 && n < 1e12) return n * 1000
return n
}
// ISO/RFC3339 etc.
return s
}
return null
}
function memoFromNow(memo) {
if (!memo) return ''
const raw = memo.createTime || memo.createdAt || memo.createdTs
const normalized = normalizeUnixTimeToMs(raw)
if (!normalized) return ''
return dayjs(normalized).fromNow()
}
function hydrateV1PreviewImages(info) {
if (!isV1Flavor(info)) return
if (!info || !info.apiUrl || !info.apiTokens) return
const token = String(info.apiTokens)
let root = String(info.apiUrl)
let apiOrigin = ''
try {
const u = new URL(root)
u.hash = ''
u.search = ''
root = u.toString()
apiOrigin = u.origin
} catch (_) {
// keep as-is
}
if (root && !root.endsWith('/')) root += '/'
const nodes = document.querySelectorAll('img.random-image')
if (!nodes || nodes.length === 0) return
// Revoke blob URLs on popup unload to avoid leaking memory.
if (!window.__memosBberObjectUrls) {
window.__memosBberObjectUrls = []
window.addEventListener('unload', function () {
const list = window.__memosBberObjectUrls || []
for (let i = 0; i < list.length; i++) {
try { URL.revokeObjectURL(list[i]) } catch (_) {}
}
window.__memosBberObjectUrls = []
})
}
const transparentPixel = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=='
function resolveToAbsoluteUrl(url) {
const u = String(url || '').trim()
if (!u) return ''
if (u.startsWith('data:') || u.startsWith('blob:') || u.startsWith('chrome-extension:')) return ''
if (u.startsWith('#')) return ''
try {
return new URL(u, root).toString()
} catch (_) {
return ''
}
}
function isSameOrigin(url) {
if (!apiOrigin) return false
try {
return new URL(url).origin === apiOrigin
} catch (_) {
return false
}
}
function looksLikeMemosResourceUrl(absUrl) {
const s = String(absUrl || '')
return s.indexOf('/o/r/') !== -1 || s.indexOf('/file/') !== -1
}
nodes.forEach(function (img) {
const hasAuthAttr = img.hasAttribute('data-auth-src')
const url = img.getAttribute('data-auth-src') || img.getAttribute('src')
if (!url) return
if (img.getAttribute('data-auth-loaded') === '1') return
const abs = resolveToAbsoluteUrl(url)
if (!abs) return
// Only hydrate same-origin resources that require Authorization.
if (!isSameOrigin(abs)) return
// Reduce unnecessary fetches: only hydrate known resource endpoints,
// or images explicitly marked as auth-required.
if (!hasAuthAttr && !looksLikeMemosResourceUrl(abs)) return
img.setAttribute('data-auth-loaded', '1')
// Prevent a broken-image icon before hydration completes.
// Only do this for images explicitly marked as auth-required.
if (hasAuthAttr) {
const currentSrc = img.getAttribute('src')
if (!currentSrc || currentSrc === abs) {
img.setAttribute('src', transparentPixel)
}
}
fetch(abs, {
method: 'GET',
headers: {
Authorization: 'Bearer ' + token
}
})
.then(function (res) {
if (!res || !res.ok) throw new Error('HTTP ' + (res ? res.status : '0'))
const ct = (res.headers && typeof res.headers.get === 'function') ? (res.headers.get('content-type') || '') : ''
if (ct && !ct.toLowerCase().startsWith('image/')) throw new Error('Not an image')
return res.blob()
})
.then(function (blob) {
const objectUrl = URL.createObjectURL(blob)
window.__memosBberObjectUrls.push(objectUrl)
img.src = objectUrl
})
.catch(function () {
// Don't break previews for modern versions where plain <img src> may already work.
if (hasAuthAttr) {
try { img.removeAttribute('src') } catch (_) {}
}
})
})
}
function renderUploadList(list) {
const $wrapper = $('.upload-list-wrapper')
const $list = $('#uploadlist')
@@ -243,12 +464,15 @@ function renderUploadList(list) {
for (let i = 0; i < items.length; i++) {
const att = items[i] || {}
const name = att.name || ''
const id = att.id != null ? String(att.id) : ''
const filename = att.filename || name
html +=
'<div class="upload-item" draggable="true" data-index="' +
i +
'" data-name="' +
escapeHtml(name) +
'" data-id="' +
escapeHtml(id) +
'">' +
'<div class="upload-left">' +
'<span class="upload-drag" title="' +
@@ -260,6 +484,8 @@ function renderUploadList(list) {
'</div>' +
'<button type="button" class="upload-del" data-name="' +
escapeHtml(name) +
'" data-id="' +
escapeHtml(id) +
'" title="' +
tipDelete +
'">×</button>' +
@@ -327,6 +553,7 @@ $(document).on('drop', '.upload-item', function (e) {
$(document).on('click', '.upload-del', function () {
const name = $(this).data('name')
const rid = $(this).data('id')
if (!name) return
get_info(function (info) {
@@ -335,11 +562,33 @@ $(document).on('click', '.upload-del', function () {
return
}
$.ajax({
url: info.apiUrl + 'api/v1/' + name,
type: 'DELETE',
headers: { Authorization: 'Bearer ' + info.apiTokens },
success: function () {
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(
function () {
const next = (Array.isArray(relistNow) ? relistNow : []).filter(function (x) {
return x && x.name !== name
})
@@ -348,10 +597,10 @@ $(document).on('click', '.upload-del', function () {
renderUploadList(relistNow)
})
},
error: function () {
function () {
$.message({ message: msg('attachmentDeleteFailed') })
}
})
)
})
})
function uploadImage(file) {
@@ -359,6 +608,12 @@ function uploadImage(file) {
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];
@@ -368,8 +623,62 @@ function uploadImage(file) {
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,
file,
{ filename: new_name, type: file.type },
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
})
chrome.storage.sync.set({ open_action: '', open_content: '', resourceIdList: relistNow }, function () {
$.message({ message: msg('picSuccess') })
})
return
}
chrome.storage.sync.set({ open_action: '', open_content: '' }, function () {
$.message({ message: msg('picFailed') })
})
},
function () {
$.message({ message: msg('picFailed') })
}
)
})
}
function uploadImageNow(base64String, file) {
get_info(function(info) {
if (info.status) {
@@ -397,13 +706,14 @@ function uploadImageNow(base64String, file) {
window.MemosApi.uploadAttachmentOrResource(
info,
data,
function (resp, kind) {
if (resp && resp.name) {
function (resp) {
const entity = (resp && resp.resource) || resp
if (entity && entity.name) {
relistNow.push({
name: resp.name,
filename: resp.filename || new_name,
createTime: resp.createTime,
type: resp.type
name: entity.name,
filename: entity.filename || new_name,
createTime: entity.createTime,
type: entity.type
})
chrome.storage.sync.set(
{
@@ -415,18 +725,19 @@ function uploadImageNow(base64String, file) {
$.message({ message: msg('picSuccess') })
}
)
} else {
chrome.storage.sync.set(
{
open_action: '',
open_content: '',
resourceIdList: []
},
function () {
$.message({ message: msg('picFailed') })
}
)
return
}
chrome.storage.sync.set(
{
open_action: '',
open_content: '',
resourceIdList: []
},
function () {
$.message({ message: msg('picFailed') })
}
)
},
function () {
$.message({ message: msg('picFailed') })
@@ -458,11 +769,23 @@ $('#saveKey').click(function () {
apiUrl: apiUrl,
apiTokens: apiTokens,
userid: auth.userId,
memoUiPath: auth.uiPath || 'memos'
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) {
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 })
}
})
}
}
)
})
@@ -482,30 +805,71 @@ $('#tags').click(function () {
// 从最近的1000条memo中获取tags,因此不保证获取能全部的
var tagDom = "";
window.MemosApi.fetchMemosWithFallback(
info,
'?pageSize=1000',
function (data) {
const memos = window.MemosApi.extractMemosListFromResponse(data)
const renderTags = function (tags) {
const uniTags = [...new Set((Array.isArray(tags) ? tags : []).filter(Boolean))]
$.each(uniTags, function (_, tag) {
tagDom += '<span class="item-container">#' + tag + '</span>';
});
tagDom += '<svg id="hideTag" class="hidetag" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M78.807 362.435c201.539 314.275 666.962 314.188 868.398-.241 16.056-24.99 13.143-54.241-4.04-62.54-17.244-8.377-40.504 3.854-54.077 24.887-174.484 272.338-577.633 272.41-752.19.195-13.573-21.043-36.874-33.213-54.113-24.837-17.177 8.294-20.06 37.545-3.978 62.536z" fill="#fff"/><path d="M894.72 612.67L787.978 494.386l38.554-34.785 106.742 118.251-38.554 34.816zM635.505 727.51l-49.04-147.123 49.255-16.41 49.054 147.098-49.27 16.435zm-236.18-12.001l-49.568-15.488 43.29-138.48 49.557 15.513-43.28 138.455zM154.49 601.006l-38.743-34.565 95.186-106.732 38.763 34.566-95.206 106.731z" fill="#fff"/></svg>'
$("#taglist").html(tagDom).slideToggle(500)
}
const allTags = memos.flatMap(function (memo) {
if (!memo) return []
if (Array.isArray(memo.tags)) return memo.tags
if (Array.isArray(memo.tagList)) return memo.tagList
return []
})
const uniTags = [...new Set(allTags.filter(Boolean))]
const onTagsData = function (data) {
const memos = window.MemosApi.extractMemosListFromResponse(data)
$.each(uniTags, function (_, tag) {
tagDom += '<span class="item-container">#' + tag + '</span>';
});
tagDom += '<svg id="hideTag" class="hidetag" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M78.807 362.435c201.539 314.275 666.962 314.188 868.398-.241 16.056-24.99 13.143-54.241-4.04-62.54-17.244-8.377-40.504 3.854-54.077 24.887-174.484 272.338-577.633 272.41-752.19.195-13.573-21.043-36.874-33.213-54.113-24.837-17.177 8.294-20.06 37.545-3.978 62.536z" fill="#fff"/><path d="M894.72 612.67L787.978 494.386l38.554-34.785 106.742 118.251-38.554 34.816zM635.505 727.51l-49.04-147.123 49.255-16.41 49.054 147.098-49.27 16.435zm-236.18-12.001l-49.568-15.488 43.29-138.48 49.557 15.513-43.28 138.455zM154.49 601.006l-38.743-34.565 95.186-106.732 38.763 34.566-95.206 106.731z" fill="#fff"/></svg>'
$("#taglist").html(tagDom).slideToggle(500)
},
function () {
$.message({ message: msg('placeApiUrl') })
}
)
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') })
}
)
}
} else {
$.message({
message: msg('placeApiUrl')
@@ -552,14 +916,29 @@ $('#search').click(function () {
get_info(function (info) {
const pattern = $("textarea[name=text]").val()
var parent = `users/${info.userid}`;
var filter = "?filter=" + encodeURIComponent(`visibility in ["PUBLIC","PROTECTED"] && content.contains("${pattern}")`);
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){
window.MemosApi.fetchMemosWithFallback(
info,
filter,
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)
}
runSearch(
function (data) {
let searchData = window.MemosApi.extractMemosListFromResponse(data)
if(searchData.length == 0){
@@ -568,10 +947,10 @@ $('#search').click(function () {
})
}else{
for(var i=0;i < searchData.length;i++){
var memosID = searchData[i].name.split('/').pop();
var memoTime = searchData[i].createTime || searchData[i].createdTs || searchData[i].createdAt
searchDom += '<div class="random-item"><div class="random-time"><span id="random-link" data-uid="'+memosID+'"><svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M864 640a32 32 0 0 1 64 0v224.096A63.936 63.936 0 0 1 864.096 928H159.904A63.936 63.936 0 0 1 96 864.096V159.904C96 124.608 124.64 96 159.904 96H384a32 32 0 0 1 0 64H192.064A31.904 31.904 0 0 0 160 192.064v639.872A31.904 31.904 0 0 0 192.064 864h639.872A31.904 31.904 0 0 0 864 831.936V640zm-485.184 52.48a31.84 31.84 0 0 1-45.12-.128 31.808 31.808 0 0 1-.128-45.12L815.04 166.048l-176.128.736a31.392 31.392 0 0 1-31.584-31.744 32.32 32.32 0 0 1 31.84-32l255.232-1.056a31.36 31.36 0 0 1 31.584 31.584L924.928 388.8a32.32 32.32 0 0 1-32 31.84 31.392 31.392 0 0 1-31.712-31.584l.736-179.392L378.816 692.48z" fill="#666" data-spm-anchor-id="a313x.7781069.0.i12" class="selected"/></svg></span><span id="random-delete" data-name="'+searchData[i].name+'" data-uid="'+memosID+'"><svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M224 322.6h576c16.6 0 30-13.4 30-30s-13.4-30-30-30H224c-16.6 0-30 13.4-30 30 0 16.5 13.5 30 30 30zm66.1-144.2h443.8c16.6 0 30-13.4 30-30s-13.4-30-30-30H290.1c-16.6 0-30 13.4-30 30s13.4 30 30 30zm339.5 435.5H394.4c-16.6 0-30 13.4-30 30s13.4 30 30 30h235.2c16.6 0 30-13.4 30-30s-13.4-30-30-30z" fill="#666"/><path d="M850.3 403.9H173.7c-33 0-60 27-60 60v360c0 33 27 60 60 60h676.6c33 0 60-27 60-60v-360c0-33-27-60-60-60zm-.1 419.8l-.1.1H173.9l-.1-.1V464l.1-.1h676.2l.1.1v359.7z" fill="#666"/></svg></span>'+(memoTime ? dayjs(memoTime).fromNow() : '')+'</div><div class="random-content">'+(searchData[i].content || '').replace(/!\[.*?\]\((.*?)\)/g,' <img class="random-image" src="$1"/> ').replace(/\[(.*?)\]\((.*?)\)/g,' <a href="$2" target="_blank">$1</a> ')+'</div>'
var resources = (searchData[i].attachments && searchData[i].attachments.length > 0) ? searchData[i].attachments : (searchData[i].resources || []);
var memosID = getMemoUid(searchData[i])
var timeText = memoFromNow(searchData[i])
searchDom += '<div class="random-item"><div class="random-time"><span id="random-link" data-uid="'+memosID+'"><svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M864 640a32 32 0 0 1 64 0v224.096A63.936 63.936 0 0 1 864.096 928H159.904A63.936 63.936 0 0 1 96 864.096V159.904C96 124.608 124.64 96 159.904 96H384a32 32 0 0 1 0 64H192.064A31.904 31.904 0 0 0 160 192.064v639.872A31.904 31.904 0 0 0 192.064 864h639.872A31.904 31.904 0 0 0 864 831.936V640zm-485.184 52.48a31.84 31.84 0 0 1-45.12-.128 31.808 31.808 0 0 1-.128-45.12L815.04 166.048l-176.128.736a31.392 31.392 0 0 1-31.584-31.744 32.32 32.32 0 0 1 31.84-32l255.232-1.056a31.36 31.36 0 0 1 31.584 31.584L924.928 388.8a32.32 32.32 0 0 1-32 31.84 31.392 31.392 0 0 1-31.712-31.584l.736-179.392L378.816 692.48z" fill="#666" data-spm-anchor-id="a313x.7781069.0.i12" class="selected"/></svg></span><span id="random-delete" data-name="'+searchData[i].name+'" data-id="'+(searchData[i].id != null ? searchData[i].id : '')+'" data-uid="'+memosID+'"><svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M224 322.6h576c16.6 0 30-13.4 30-30s-13.4-30-30-30H224c-16.6 0-30 13.4-30 30 0 16.5 13.5 30 30 30zm66.1-144.2h443.8c16.6 0 30-13.4 30-30s-13.4-30-30-30H290.1c-16.6 0-30 13.4-30 30s13.4 30 30 30zm339.5 435.5H394.4c-16.6 0-30 13.4-30 30s13.4 30 30 30h235.2c16.6 0 30-13.4 30-30s-13.4-30-30-30z" fill="#666"/><path d="M850.3 403.9H173.7c-33 0-60 27-60 60v360c0 33 27 60 60 60h676.6c33 0 60-27 60-60v-360c0-33-27-60-60-60zm-.1 419.8l-.1.1H173.9l-.1-.1V464l.1-.1h676.2l.1.1v359.7z" fill="#666"/></svg></span>'+timeText+'</div><div class="random-content">'+(searchData[i].content || '').replace(/!\[.*?\]\((.*?)\)/g,' <img class="random-image" src="$1"/> ').replace(/\[(.*?)\]\((.*?)\)/g,' <a href="$2" target="_blank">$1</a> ')+'</div>'
var resources = (searchData[i].attachments && searchData[i].attachments.length > 0) ? searchData[i].attachments : ((searchData[i].resources && searchData[i].resources.length > 0) ? searchData[i].resources : (searchData[i].resourceList || []));
if(resources && resources.length > 0){
for(var j=0;j < resources.length;j++){
var restype = (resources[j].type || '').slice(0,5);
@@ -580,11 +959,17 @@ $('#search').click(function () {
if(resexlink){
resLink = resexlink
}else{
fileId = resources[j].publicId || resources[j].filename
resLink = info.apiUrl+'file/'+resources[j].name+'/'+fileId
resLink = buildV1ResourceStreamUrl(info, resources[j])
}
if (!resLink) {
continue
}
if(restype == 'image'){
searchDom += '<img class="random-image" src="'+resLink+'"/>'
if (isV1Flavor(info)) {
searchDom += '<img class="random-image" data-auth-src="'+resLink+'"/>'
} else {
searchDom += '<img class="random-image" src="'+resLink+'"/>'
}
}
if(restype !== 'image'){
searchDom += '<a target="_blank" rel="noreferrer" href="'+resLink+'">'+resources[j].filename+'</a>'
@@ -595,9 +980,10 @@ $('#search').click(function () {
}
window.ViewImage && ViewImage.init('.random-image')
$("#randomlist").html(searchDom).slideDown(500);
hydrateV1PreviewImages(info)
}
},
function () {
function (xhr) {
$.message({ message: msg('searchNone') })
}
)
@@ -617,12 +1003,23 @@ $('#search').click(function () {
$('#random').click(function () {
get_info(function (info) {
var parent = `users/${info.userid}`;
var filter = "?filter=" + encodeURIComponent(`visibility in ["PUBLIC","PROTECTED"]`);
if (info.status) {
$("#randomlist").html('').hide()
window.MemosApi.fetchMemosWithFallback(
info,
filter,
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)
}
runRandom(
function (data) {
const memos = window.MemosApi.extractMemosListFromResponse(data)
let randomNum = Math.floor(Math.random() * (memos.length));
@@ -643,22 +1040,29 @@ $('#random').click(function () {
function randDom(randomData){
get_info(function (info) {
var memosID = randomData.name.split('/').pop();
var randomDom = '<div class="random-item"><div class="random-time"><span id="random-link" data-uid="'+memosID+'"><svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M864 640a32 32 0 0 1 64 0v224.096A63.936 63.936 0 0 1 864.096 928H159.904A63.936 63.936 0 0 1 96 864.096V159.904C96 124.608 124.64 96 159.904 96H384a32 32 0 0 1 0 64H192.064A31.904 31.904 0 0 0 160 192.064v639.872A31.904 31.904 0 0 0 192.064 864h639.872A31.904 31.904 0 0 0 864 831.936V640zm-485.184 52.48a31.84 31.84 0 0 1-45.12-.128 31.808 31.808 0 0 1-.128-45.12L815.04 166.048l-176.128.736a31.392 31.392 0 0 1-31.584-31.744 32.32 32.32 0 0 1 31.84-32l255.232-1.056a31.36 31.36 0 0 1 31.584 31.584L924.928 388.8a32.32 32.32 0 0 1-32 31.84 31.392 31.392 0 0 1-31.712-31.584l.736-179.392L378.816 692.48z" fill="#666" data-spm-anchor-id="a313x.7781069.0.i12" class="selected"/></svg></span><span id="random-delete" data-uid="'+memosID+'" data-name="'+randomData.name+'"><svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M224 322.6h576c16.6 0 30-13.4 30-30s-13.4-30-30-30H224c-16.6 0-30 13.4-30 30 0 16.5 13.5 30 30 30zm66.1-144.2h443.8c16.6 0 30-13.4 30-30s-13.4-30-30-30H290.1c-16.6 0-30 13.4-30 30s13.4 30 30 30zm339.5 435.5H394.4c-16.6 0-30 13.4-30 30s13.4 30 30 30h235.2c16.6 0 30-13.4 30-30s-13.4-30-30-30z" fill="#666"/><path d="M850.3 403.9H173.7c-33 0-60 27-60 60v360c0 33 27 60 60 60h676.6c33 0 60-27 60-60v-360c0-33-27-60-60-60zm-.1 419.8l-.1.1H173.9l-.1-.1V464l.1-.1h676.2l.1.1v359.7z" fill="#666"/></svg></span>'+dayjs(randomData.createTime).fromNow()+'</div><div class="random-content">'+randomData.content.replace(/!\[.*?\]\((.*?)\)/g,' <img class="random-image" src="$1"/> ').replace(/\[(.*?)\]\((.*?)\)/g,' <a href="$2" target="_blank">$1</a> ')+'</div>'
var resources = (randomData.attachments && randomData.attachments.length > 0) ? randomData.attachments : (randomData.resources || []);
var memosID = getMemoUid(randomData)
var timeText = memoFromNow(randomData)
var randomDom = '<div class="random-item"><div class="random-time"><span id="random-link" data-uid="'+memosID+'"><svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M864 640a32 32 0 0 1 64 0v224.096A63.936 63.936 0 0 1 864.096 928H159.904A63.936 63.936 0 0 1 96 864.096V159.904C96 124.608 124.64 96 159.904 96H384a32 32 0 0 1 0 64H192.064A31.904 31.904 0 0 0 160 192.064v639.872A31.904 31.904 0 0 0 192.064 864h639.872A31.904 31.904 0 0 0 864 831.936V640zm-485.184 52.48a31.84 31.84 0 0 1-45.12-.128 31.808 31.808 0 0 1-.128-45.12L815.04 166.048l-176.128.736a31.392 31.392 0 0 1-31.584-31.744 32.32 32.32 0 0 1 31.84-32l255.232-1.056a31.36 31.36 0 0 1 31.584 31.584L924.928 388.8a32.32 32.32 0 0 1-32 31.84 31.392 31.392 0 0 1-31.712-31.584l.736-179.392L378.816 692.48z" fill="#666" data-spm-anchor-id="a313x.7781069.0.i12" class="selected"/></svg></span><span id="random-delete" data-uid="'+memosID+'" data-name="'+randomData.name+'" data-id="'+(randomData && randomData.id != null ? randomData.id : '')+'"><svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M224 322.6h576c16.6 0 30-13.4 30-30s-13.4-30-30-30H224c-16.6 0-30 13.4-30 30 0 16.5 13.5 30 30 30zm66.1-144.2h443.8c16.6 0 30-13.4 30-30s-13.4-30-30-30H290.1c-16.6 0-30 13.4-30 30s13.4 30 30 30zm339.5 435.5H394.4c-16.6 0-30 13.4-30 30s13.4 30 30 30h235.2c16.6 0 30-13.4 30-30s-13.4-30-30-30z" fill="#666"/><path d="M850.3 403.9H173.7c-33 0-60 27-60 60v360c0 33 27 60 60 60h676.6c33 0 60-27 60-60v-360c0-33-27-60-60-60zm-.1 419.8l-.1.1H173.9l-.1-.1V464l.1-.1h676.2l.1.1v359.7z" fill="#666"/></svg></span>'+timeText+'</div><div class="random-content">'+(randomData && randomData.content ? randomData.content : '').replace(/!\[.*?\]\((.*?)\)/g,' <img class="random-image" src="$1"/> ').replace(/\[(.*?)\]\((.*?)\)/g,' <a href="$2" target="_blank">$1</a> ')+'</div>'
var resources = (randomData.attachments && randomData.attachments.length > 0) ? randomData.attachments : ((randomData.resources && randomData.resources.length > 0) ? randomData.resources : (randomData.resourceList || []));
if(resources && resources.length > 0){
for(var j=0;j < resources.length;j++){
var restype = resources[j].type.slice(0,5);
var restype = (resources[j].type || '').slice(0,5);
var resexlink = resources[j].externalLink
var resLink = '',fileId=''
if(resexlink){
resLink = resexlink
}else{
fileId = resources[j].publicId || resources[j].filename
resLink = info.apiUrl+'file/'+resources[j].name+'/'+fileId
resLink = buildV1ResourceStreamUrl(info, resources[j])
}
if (!resLink) {
continue
}
if(restype == 'image'){
randomDom += '<img class="random-image" src="'+resLink+'"/>'
if (isV1Flavor(info)) {
randomDom += '<img class="random-image" data-auth-src="'+resLink+'"/>'
} else {
randomDom += '<img class="random-image" src="'+resLink+'"/>'
}
}
if(restype !== 'image'){
randomDom += '<a target="_blank" rel="noreferrer" href="'+resLink+'">'+resources[j].filename+'</a>'
@@ -668,6 +1072,7 @@ function randDom(randomData){
randomDom += '</div>'
window.ViewImage && ViewImage.init('.random-image')
$("#randomlist").html(randomDom).slideDown(500);
hydrateV1PreviewImages(info)
})
}
@@ -683,6 +1088,25 @@ $(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,
@@ -778,7 +1202,7 @@ function getOne(memosId){
get_info(function (info) {
if (info.apiUrl) {
$("#randomlist").html('').hide()
var getUrl = info.apiUrl+'api/v1/'+memosId
var getUrl = isV1Flavor(info) ? info.apiUrl+'api/v1/memo/'+memosId : info.apiUrl+'api/v1/'+memosId
$.ajax({
url:getUrl,
type:"GET",
@@ -816,6 +1240,58 @@ function sendText() {
sendvisi = 'PRIVATE'
}
}
// 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",
+2 -2
View File
@@ -2,8 +2,8 @@
"manifest_version": 3,
"name": "__MSG_extName__",
"default_locale": "en",
"version": "2026.03.07",
"version_name": "Supports 0.24.0 to the latest version",
"version": "2026.03.08",
"version_name": "Supports 0.18.0 to the latest version",
"action": {
"default_popup": "popup.html",
"default_icon": "assets/logo_24x24.png",
+2
View File
@@ -152,6 +152,8 @@
<script src="../js/relativeTime.js"></script>
<script src="../js/view-image.js"></script>
<script src="../js/memosApi.js"></script>
<script src="../js/compat/memosApi.v1.js"></script>
<script src="../js/compat/memosApi.v023.js"></script>
<script src="../js/oper.js"></script>
</body>
</html>