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
This commit is contained in:
jonny
2026-04-22 16:13:18 +08:00
parent 6c4801cb16
commit 3968b6896c
14 changed files with 840 additions and 681 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ Chrome 应用商店:<https://chrome.google.com/webstore/detail/memos-bber/cbhj
一个通过浏览器插件发布 [Memos](https://usememos.com/) 的插件。基于 iSpeak-bber 修改,原作者为 [DreamyTZK](https://www.antmoe.com/)。
## 更新日志
- .
- 20260422 调整发送设置,支持仅发送附件
#### 20260421 更新匹配 0.27.x
- 20260325 优化语言按钮样式
- 20260323 优化中文显示效果
+15
View File
@@ -23,6 +23,18 @@
"supportedMemosVersion": {
"message": "Compatible with Memos v0.15.0 - 0.27.x"
},
"settingsConnectionTitle": {
"message": "Connection"
},
"settingsConnectionDesc": {
"message": "Configure the Memos site URL and access token."
},
"settingsPostingTitle": {
"message": "Posting"
},
"settingsPostingDesc": {
"message": "Default text for attachment-only sends"
},
"placeApiUrl":{
"message": "Memos site URL"
},
@@ -50,6 +62,9 @@
"placeShowInput":{
"message": "Default 'Everyone can see' Tag name"
},
"placeAttachmentOnlyDefaultText": {
"message": "Default text for attachment-only sends (leave blank to use built-in text)"
},
"uploadedListTitle": {
"message": "Uploaded files, Drag to reorder"
},
+15
View File
@@ -23,6 +23,18 @@
"supportedMemosVersion": {
"message": "Memos v0.15.0 - 0.27.x に対応"
},
"settingsConnectionTitle": {
"message": "接続設定"
},
"settingsConnectionDesc": {
"message": "Memos のURLとアクセストークンを設定します。"
},
"settingsPostingTitle": {
"message": "投稿設定"
},
"settingsPostingDesc": {
"message": "添付ファイルのみ送信時の既定テキスト"
},
"placeApiUrl": {
"message": "Memos サイトURL"
},
@@ -50,6 +62,9 @@
"placeShowInput": {
"message": "既定の「全員に公開」タグ名"
},
"placeAttachmentOnlyDefaultText": {
"message": "添付ファイルのみ送信時の既定テキスト(空欄で内蔵文言を使用)"
},
"uploadedListTitle": {
"message": "アップロード済みファイル(ドラッグで並べ替え)"
},
+15
View File
@@ -23,6 +23,18 @@
"supportedMemosVersion": {
"message": "Memos v0.15.0 - 0.27.x 호환"
},
"settingsConnectionTitle": {
"message": "연결 설정"
},
"settingsConnectionDesc": {
"message": "Memos 사이트 URL과 액세스 토큰을 설정합니다."
},
"settingsPostingTitle": {
"message": "전송 설정"
},
"settingsPostingDesc": {
"message": "첨부만 전송할 때의 기본 텍스트"
},
"placeApiUrl": {
"message": "Memos 사이트 URL"
},
@@ -50,6 +62,9 @@
"placeShowInput": {
"message": "기본 '모두 공개' 태그 이름"
},
"placeAttachmentOnlyDefaultText": {
"message": "첨부만 전송할 때의 기본 텍스트(비워두면 내장 문구 사용)"
},
"uploadedListTitle": {
"message": "업로드된 파일(드래그로 순서 변경)"
},
+15
View File
@@ -23,6 +23,18 @@
"supportedMemosVersion": {
"message": "兼容 Memos v0.15.0 - 0.27.x"
},
"settingsConnectionTitle": {
"message": "连接设置"
},
"settingsConnectionDesc": {
"message": "配置 Memos 服务地址和访问令牌。"
},
"settingsPostingTitle": {
"message": "发送设置"
},
"settingsPostingDesc": {
"message": "仅发送附件时的默认文本"
},
"placeApiUrl":{
"message": "请填入 Memos 主页网址"
},
@@ -50,6 +62,9 @@
"placeShowInput":{
"message": "默认“公开”标签名"
},
"placeAttachmentOnlyDefaultText":{
"message": "仅发送附件时的默认文本(留空则使用内置文案)"
},
"picDrag":{
"message": "拖拽到窗口上传该图片"
},
+54
View File
@@ -170,6 +170,60 @@ a{color: #555;}
}
input.inputer{border-bottom: 1px solid #ccc;width:75%;}
.settings-panel{
display: flex;
flex-direction: column;
gap: .75rem;
margin-top: .75rem;
}
.settings-section{
border: 1px solid rgb(229,231,235);
border-radius: .5rem;
background-color: rgb(255,255,255);
padding: .75rem;
display: flex;
flex-direction: column;
gap: .6rem;
}
.settings-section-title{
font-size: .9rem;
font-weight: 700;
color: rgb(55,65,81);
}
.settings-section-desc{
font-size: .75rem;
line-height: 1.35;
color: #7a7a7a;
}
.settings-input{
width: 100% !important;
box-sizing: border-box;
border: 1px solid rgb(229,231,235);
border-radius: .35rem;
background-color: #fafafa;
padding: .55rem .7rem;
}
.settings-input:focus{
border-color: rgb(22,163,74);
background-color: rgb(255,255,255);
}
.settings-textarea{
resize: vertical;
min-height: 4.5rem;
white-space: pre-wrap;
}
.settings-actions{
display: flex;
justify-content: flex-end;
}
#saveKey{margin:0;flex:1;}
.common-tools-wrapper {
+521
View File
@@ -0,0 +1,521 @@
(function (global) {
'use strict'
const FLAVOR_V020_V021 = 'v020-v021'
const KNOWN_FLAVORS = [FLAVOR_V020_V021, 'v023', 'modern']
function requestJson(options, success, fail) {
global.$
.ajax(options)
.done(function (data) {
if (success) success(data)
})
.fail(function (xhr) {
if (fail) fail(xhr)
})
}
function extractMemos(data) {
if (global.MemosApiModern && typeof global.MemosApiModern.extractMemosListFromResponse === 'function') {
return global.MemosApiModern.extractMemosListFromResponse(data)
}
return []
}
function getFlavor(info) {
if (!info) return 'legacy'
if (info.apiFlavor === 'modern' && global.MemosApiV023) return 'modern'
if (info.apiFlavor === 'v023' && global.MemosApiV023) return 'v023'
if ((info.apiFlavor === FLAVOR_V020_V021 || info.apiFlavor === 'v1') && global.MemosApiV020V021) {
return FLAVOR_V020_V021
}
return 'legacy'
}
function normalizeDetectedFlavor(flavor) {
const value = typeof flavor === 'string' ? flavor : ''
if (value === 'v020' || value === 'v021' || value === 'v1') return FLAVOR_V020_V021
return value
}
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
if (typeof data.error === 'string' || typeof data.message === 'string') return false
return false
}
function isNotFoundLikeProbeXhr(xhr) {
const status = xhr && xhr.status
return status === 404 || status === 405
}
function probeFlavor(apiUrl, apiTokens, callback) {
const headers = { Authorization: 'Bearer ' + apiTokens }
const modernQ =
'api/v1/memos?pageSize=1&filter=' +
encodeURIComponent('visibility in ["PUBLIC","PROTECTED"]')
const v023Q =
'api/v1/memos?pageSize=1&filter=' +
encodeURIComponent('visibilities == ["PUBLIC","PROTECTED"]')
const v020V021Q = 'api/v1/memo?limit=1&rowStatus=NORMAL'
function finish(flavor) {
const normalized = normalizeDetectedFlavor(flavor)
if (KNOWN_FLAVORS.indexOf(normalized) !== -1) {
if (callback) callback({ flavor: normalized })
return
}
if (callback) callback({ flavor: 'unknown' })
}
function probeV023() {
global.$
.ajax({
url: apiUrl + v023Q,
method: 'GET',
headers: headers,
dataType: 'json'
})
.done(function (data) {
if (looksLikeMemosListPayload(data)) finish('v023')
else finish('unknown')
})
.fail(function () {
finish('unknown')
})
}
global.$
.ajax({
url: apiUrl + modernQ,
method: 'GET',
headers: headers,
dataType: 'json'
})
.done(function (data) {
if (looksLikeMemosListPayload(data)) {
finish('modern')
return
}
probeV023()
})
.fail(function (xhr) {
if (xhr && xhr.status === 400) {
probeV023()
return
}
if (isNotFoundLikeProbeXhr(xhr)) {
global.$
.ajax({
url: apiUrl + v020V021Q,
method: 'GET',
headers: headers,
dataType: 'json'
})
.done(function (data) {
if (looksLikeMemosListPayload(data)) finish(FLAVOR_V020_V021)
else finish('unknown')
})
.fail(function () {
finish('unknown')
})
return
}
finish('unknown')
})
}
function keepLegacyVisibleMemos(list) {
const items = Array.isArray(list) ? list : []
return items.filter(function (memo) {
if (!memo) return false
const visibility = typeof memo.visibility === 'string' ? memo.visibility.toUpperCase() : ''
if (!visibility) return true
return visibility === 'PUBLIC' || visibility === 'PROTECTED'
})
}
function extractTagsFromGenericMemo(memo) {
if (!memo) return []
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 []
}
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)
@@ -500,7 +500,7 @@
doPatchAttachments()
}
global.MemosApi = {
global.MemosApiModern = {
extractUserIdFromAuthResponse: extractUserIdFromAuthResponse,
extractMemosListFromResponse: extractMemosListFromResponse,
isNotFoundLikeXhr: isNotFoundLikeXhr,
@@ -274,7 +274,7 @@
})
}
global.MemosApiV1 = {
global.MemosApiV020V021 = {
listMemos: listMemos,
createMemo: createMemo,
patchMemo: patchMemo,
+1 -104
View File
@@ -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)
+6 -1
View File
@@ -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')
+152 -548
View File
@@ -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 <img src> 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 += '<img class="random-image" data-auth-src="'+resLink+'"/>'
} else {
searchDom += '<img class="random-image" src="'+resLink+'"/>'
@@ -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 = '<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>'
@@ -1285,7 +982,7 @@ function randDom(randomData){
continue
}
if(restype == 'image'){
if (isV1Flavor(info)) {
if (adapter.needsAuthenticatedImagePreview()) {
randomDom += '<img class="random-image" data-auth-src="'+resLink+'"/>'
} else {
randomDom += '<img class="random-image" src="'+resLink+'"/>'
@@ -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')
+1 -1
View File
@@ -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",
+42 -24
View File
@@ -34,10 +34,13 @@
<path d="M914 432c-5-26-21-43-41-43h-4c-54 0-99-44-99-99 0-17 9-37 9-38 10-22 2-50-18-65l-103-57h-1c-21-9-49-4-64 12-12 12-50 44-79 44s-68-33-79-45a60 60 0 0 0-64-13l-106 58-2 1a54 54 0 0 0-18 65c0 1 9 21 9 38 0 55-45 99-99 99h-5c-19 0-35 17-40 43 0 2-9 45-9 80s9 79 9 81c5 25 21 42 41 42h4c54 0 99 45 99 99 0 18-9 37-9 38-10 23-2 51 18 65l101 56 1 1c21 9 49 3 65-13 14-15 52-47 80-47 30 0 69 35 81 48a58 58 0 0 0 64 14l104-58 2-1c20-14 28-42 18-65 0-1-9-20-9-38 0-54 45-99 99-99h5c19 0 35-17 40-42 0-2 9-46 9-81s-9-78-9-80m-51 80c0 23-5 52-7 64a158 158 0 0 0-134 215l-89 49c-4-5-17-18-35-31-31-23-61-35-88-35s-57 12-88 34c-17 13-30 26-34 31l-86-48a159 159 0 0 0-134-215c-2-12-7-41-7-64 0-22 5-51 7-64a157 157 0 0 0 134-214l91-50c4 4 17 17 35 29 30 22 59 33 86 33s55-11 85-32c18-13 31-25 35-29l88 49a159 159 0 0 0 134 214c2 13 7 42 7 64"/>
<path d="M510 366a146 146 0 1 0 1 292 146 146 0 0 0-1-292m87 146a87 87 0 1 1-173-1 87 87 0 0 1 173 1"/>
</svg></div>
<div id="blog_info" class="">
<div id="blog_info" class="settings-panel">
<div class="settings-section">
<div id="settingsConnectionTitle" class="settings-section-title"></div>
<div class="settings-section-desc" id="settingsConnectionDesc"></div>
<input
id="apiUrl"
class="inputer"
class="inputer settings-input"
name="apiUrl"
type="text"
value=""
@@ -47,7 +50,7 @@
/>
<input
id="apiTokens"
class="inputer"
class="inputer settings-input"
name="apiTokens"
type="text"
value=""
@@ -55,8 +58,23 @@
placeholder=""
required
/>
<span id="saveKey" class="action-btn confirm-btn"></span>
<div id="supportedMemosVersion" class="upload-list-title"></div>
</div>
<div class="settings-section">
<div id="settingsPostingTitle" class="settings-section-title"></div>
<div class="settings-section-desc" id="settingsPostingDesc"></div>
<textarea
id="attachmentOnlyDefaultText"
class="inputer settings-input settings-textarea"
name="attachmentOnlyDefaultText"
rows="2"
maxlength="500"
placeholder=""
></textarea>
</div>
<div class="settings-actions">
<span id="saveSettings" class="action-btn confirm-btn"></span>
</div>
</div>
<div class="memo-editor">
@@ -135,26 +153,25 @@
<div class="tag-list" id="taglist"></div>
<div class="tag-hide" id="taghide">
<input
id="hideInput"
class="inputer"
name="hideInput"
type="text"
value=""
maxlength="50"
placeholder=""
/>
id="hideInput"
class="inputer"
name="hideInput"
type="text"
value=""
maxlength="50"
placeholder=""
/>
<input
id="showInput"
class="inputer"
name="showInput"
type="text"
value=""
maxlength="50"
placeholder=""
/>
<span id="saveTag" class="action-btn confirm-btn"></span>
id="showInput"
class="inputer"
name="showInput"
type="text"
value=""
maxlength="50"
placeholder=""
/>
<span id="saveTag" class="action-btn confirm-btn"></span>
</div>
<div class="" id="randomlist"></div>
<input type="file" id="inFile" style="display:none;">
@@ -168,9 +185,10 @@
<script src="../js/ko.js"></script>
<script src="../js/relativeTime.js"></script>
<script src="../js/view-image.js"></script>
<script src="../js/compat/memosApi.v024.js"></script>
<script src="../js/compat/memosApi.v1.js"></script>
<script src="../js/compat/memosApi.modern.js"></script>
<script src="../js/compat/memosApi.v020-v021.js"></script>
<script src="../js/compat/memosApi.v023.js"></script>
<script src="../js/compat/memosApi.adapter.js"></script>
<script src="../js/oper.js"></script>
</body>
</html>