mirror of
https://github.com/Jonnyan404/memos-bber.git
synced 2026-06-25 06:46:21 +09:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b36fda137 | |||
| da150b8788 | |||
| 85cc964836 | |||
| adfd797e84 | |||
| 8f51bb399b | |||
| 30b7cf2491 |
@@ -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/)。
|
||||
|
||||
## 更新日志
|
||||
- 20260309 右键发送选中文本保持原格式,增加全屏和窗口放大功能
|
||||
### 20260308 向前兼容到0.18.0,可能再往前也行,只测试到0.18.0
|
||||
- 20260307 增加语言切换按钮以及韩语和日语支持,
|
||||
- 2026年03月06日 右键菜单发送选中文本附带原文链接
|
||||
- 2026年03月05日 向前兼容到0.24.0,可能再往前也行,因为只测试了0.24.0和0.25.0以及当前最新版本,如有更早版本需求,可issue反馈
|
||||
|
||||
@@ -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"
|
||||
@@ -163,5 +163,11 @@
|
||||
},
|
||||
"langKorean": {
|
||||
"message": "한국어"
|
||||
},
|
||||
"tipFullscreen": {
|
||||
"message": "Open fullscreen editor"
|
||||
},
|
||||
"tipResize": {
|
||||
"message": "Drag to resize (min: default size)"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -163,5 +163,11 @@
|
||||
},
|
||||
"langKorean": {
|
||||
"message": "한국어"
|
||||
},
|
||||
"tipFullscreen": {
|
||||
"message": "全画面で編集"
|
||||
},
|
||||
"tipResize": {
|
||||
"message": "ドラッグで拡大/縮小(最小:初期サイズ)"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -158,10 +158,16 @@
|
||||
"langChineseSimplified": {
|
||||
"message": "简体中文"
|
||||
},
|
||||
"langJapanese": {
|
||||
"langJapanese": {
|
||||
"message": "日本語"
|
||||
},
|
||||
"langKorean": {
|
||||
"message": "한국어"
|
||||
},
|
||||
"tipFullscreen": {
|
||||
"message": "전체화면 편집"
|
||||
},
|
||||
"tipResize": {
|
||||
"message": "드래그로 확대/축소(최소: 기본 크기)"
|
||||
}
|
||||
}
|
||||
@@ -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 主页网址"
|
||||
@@ -163,5 +163,11 @@
|
||||
},
|
||||
"langKorean": {
|
||||
"message": "한국어"
|
||||
},
|
||||
"tipFullscreen": {
|
||||
"message": "全屏编辑"
|
||||
},
|
||||
"tipResize": {
|
||||
"message": "拖拽缩放编辑框(最小为默认大小)"
|
||||
}
|
||||
}
|
||||
+87
-3
@@ -48,6 +48,49 @@ a{color: #555;}
|
||||
transition-timing-function: cubic-bezier(.4,0,.2,1);
|
||||
transition-duration: .15s;
|
||||
}
|
||||
.memo-editor{
|
||||
position: relative;
|
||||
resize: none;
|
||||
overflow: visible;
|
||||
box-sizing: border-box;
|
||||
contain: layout;
|
||||
}
|
||||
|
||||
.memo-editor-header{
|
||||
position: sticky;
|
||||
top: .5rem;
|
||||
z-index: 3;
|
||||
height: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#editor-resize-handle{
|
||||
position: absolute;
|
||||
right: .35rem;
|
||||
bottom: .35rem;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-right: 2px solid #bbb;
|
||||
border-bottom: 2px solid #bbb;
|
||||
cursor: nwse-resize;
|
||||
opacity: .8;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
#editor-resize-handle:hover{
|
||||
opacity: 1;
|
||||
border-right-color: #888;
|
||||
border-bottom-color: #888;
|
||||
}
|
||||
|
||||
.body.fullscreen #editor-resize-handle{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.body.fullscreen #fullscreen{
|
||||
display: none;
|
||||
}
|
||||
.random-item{
|
||||
border: 1px solid rgb(229,231,235);
|
||||
color: #666;
|
||||
@@ -66,6 +109,32 @@ a{color: #555;}
|
||||
overflow-wrap: anywhere;
|
||||
word-break: normal;}
|
||||
.btns-container{text-align:right;}
|
||||
.memo-editor #fullscreen{
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
border: 1px solid rgb(229,231,235);
|
||||
border-radius: .25rem;
|
||||
background-color: rgb(255,255,255);
|
||||
color: #666;
|
||||
font-size: .75rem;
|
||||
line-height: 1;
|
||||
padding: .25rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
opacity: .9;
|
||||
}
|
||||
|
||||
.memo-editor #fullscreen svg{
|
||||
display: block;
|
||||
}
|
||||
.memo-editor #fullscreen:hover{
|
||||
opacity: 1;
|
||||
background-color: rgb(243,244,246);
|
||||
}
|
||||
.common-editor-inputer,input.inputer{
|
||||
font-family: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;
|
||||
height: 100%;
|
||||
@@ -77,10 +146,27 @@ a{color: #555;}
|
||||
background-color: transparent;
|
||||
font-size: 1rem;
|
||||
min-height: 40px;
|
||||
max-height: 400px;
|
||||
scrollbar-width: none;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
.common-editor-inputer{
|
||||
padding-right: 1.5rem;
|
||||
height: auto;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.body.fullscreen{
|
||||
min-width: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.body.fullscreen .common-editor-inputer{
|
||||
min-height: 60vh;
|
||||
}
|
||||
input.inputer{border-bottom: 1px solid #ccc;width:75%;}
|
||||
|
||||
#saveKey{margin:0;flex:1;}
|
||||
@@ -248,9 +334,7 @@ input.inputer{border-bottom: 1px solid #ccc;width:75%;}
|
||||
padding: .15rem .35rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
#blog_info{
|
||||
|
||||
}
|
||||
|
||||
.tip{
|
||||
margin-left: 36%;
|
||||
|
||||
+118
-27
@@ -23,6 +23,89 @@ function updateContextMenu(id, update) {
|
||||
})
|
||||
}
|
||||
|
||||
function pageReadSelectionText() {
|
||||
try {
|
||||
const active = document.activeElement
|
||||
const isTextInput =
|
||||
active &&
|
||||
(active.tagName === 'TEXTAREA' ||
|
||||
(active.tagName === 'INPUT' &&
|
||||
/^(text|search|url|tel|email|password)$/i.test(active.type || 'text')))
|
||||
|
||||
if (isTextInput && typeof active.selectionStart === 'number' && typeof active.selectionEnd === 'number') {
|
||||
return String(active.value || '').slice(active.selectionStart, active.selectionEnd).replace(/\r\n?/g, '\n')
|
||||
}
|
||||
|
||||
const sel = window.getSelection && window.getSelection()
|
||||
if (!sel) return ''
|
||||
return String(sel.toString() || '').replace(/\r\n?/g, '\n')
|
||||
} catch (_) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function getSelectionTextFromTab(tabId, fallbackText) {
|
||||
return new Promise((resolve) => {
|
||||
const fallback = typeof fallbackText === 'string' ? fallbackText : ''
|
||||
if (!tabId || !chrome.scripting || typeof chrome.scripting.executeScript !== 'function') {
|
||||
resolve(fallback)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
chrome.scripting.executeScript(
|
||||
{
|
||||
target: { tabId },
|
||||
func: pageReadSelectionText
|
||||
},
|
||||
(results) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
resolve(fallback)
|
||||
return
|
||||
}
|
||||
const first = Array.isArray(results) ? results[0] : null
|
||||
const text = first && typeof first.result === 'string' ? first.result : ''
|
||||
resolve(text || fallback)
|
||||
}
|
||||
)
|
||||
} catch (_) {
|
||||
resolve(fallback)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function tryOpenActionPopup(tab) {
|
||||
try {
|
||||
if (!chrome.action || typeof chrome.action.openPopup !== 'function') return
|
||||
const windowId = tab && typeof tab.windowId === 'number' ? tab.windowId : undefined
|
||||
|
||||
const open = () => {
|
||||
try {
|
||||
if (typeof windowId === 'number') {
|
||||
chrome.action.openPopup({ windowId }, () => void chrome.runtime.lastError)
|
||||
} else {
|
||||
chrome.action.openPopup({}, () => void chrome.runtime.lastError)
|
||||
}
|
||||
} catch (_) {
|
||||
// best-effort only
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid: "Cannot show popup for an inactive window".
|
||||
if (typeof windowId === 'number' && chrome.windows && typeof chrome.windows.update === 'function') {
|
||||
chrome.windows.update(windowId, { focused: true }, () => {
|
||||
void chrome.runtime.lastError
|
||||
open()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
open()
|
||||
} catch (_) {
|
||||
// best-effort only
|
||||
}
|
||||
}
|
||||
|
||||
let cachedUiLanguage = null
|
||||
let cachedOverrideMessages = null
|
||||
|
||||
@@ -93,35 +176,43 @@ chrome.storage.onChanged.addListener((changes, areaName) => {
|
||||
refreshContextMenus()
|
||||
})
|
||||
|
||||
chrome.contextMenus.onClicked.addListener((info) => {
|
||||
let tempCont = ''
|
||||
switch (info.menuItemId) {
|
||||
case 'Memos-send-selection':
|
||||
tempCont =
|
||||
info.selectionText +
|
||||
'\n' +
|
||||
`[Reference Link](${info.linkUrl || info.pageUrl})` +
|
||||
'\n'
|
||||
break
|
||||
case 'Memos-send-link':
|
||||
tempCont = (info.linkUrl || info.pageUrl) + '\n'
|
||||
break
|
||||
case 'Memos-send-image':
|
||||
tempCont = `` + '\n'
|
||||
break
|
||||
}
|
||||
|
||||
chrome.storage.sync.get(
|
||||
{ open_action: 'save_text', open_content: '' },
|
||||
function (items) {
|
||||
chrome.contextMenus.onClicked.addListener((info, tab) => {
|
||||
const appendContent = (tempCont, { openPopup } = { openPopup: false }) => {
|
||||
chrome.storage.sync.get({ open_action: 'save_text', open_content: '' }, function (items) {
|
||||
if (items.open_action === 'upload_image') {
|
||||
t('picPending').then((m) => alert(m))
|
||||
} else {
|
||||
chrome.storage.sync.set({
|
||||
return
|
||||
}
|
||||
|
||||
chrome.storage.sync.set(
|
||||
{
|
||||
open_action: 'save_text',
|
||||
open_content: items.open_content + tempCont
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
function () {
|
||||
if (openPopup) tryOpenActionPopup(tab)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
if (info.menuItemId === 'Memos-send-selection') {
|
||||
const ref = info.linkUrl || info.pageUrl
|
||||
const tabId = tab && tab.id
|
||||
|
||||
getSelectionTextFromTab(tabId, info.selectionText).then((selectionText) => {
|
||||
const tempCont = selectionText + '\n' + `[Reference Link](${ref})` + '\n'
|
||||
appendContent(tempCont, { openPopup: true })
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (info.menuItemId === 'Memos-send-link') {
|
||||
appendContent((info.linkUrl || info.pageUrl) + '\n')
|
||||
return
|
||||
}
|
||||
|
||||
if (info.menuItemId === 'Memos-send-image') {
|
||||
appendContent(`` + '\n')
|
||||
}
|
||||
})
|
||||
@@ -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)
|
||||
@@ -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' }
|
||||
]
|
||||
@@ -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)
|
||||
@@ -86,6 +86,8 @@ function applyStaticI18n() {
|
||||
setText('lockPublic', 'lockPublic')
|
||||
|
||||
setText('content_submit_text', 'submitBtn')
|
||||
const fullscreen = document.getElementById('fullscreen')
|
||||
if (fullscreen) fullscreen.setAttribute('aria-label', t('tipFullscreen'))
|
||||
|
||||
setPlaceholder('hideInput', 'placeHideInput')
|
||||
setPlaceholder('showInput', 'placeShowInput')
|
||||
@@ -111,6 +113,8 @@ function applyStaticI18n() {
|
||||
setTitle('search', 'tipSearch')
|
||||
setTitle('lock', 'tipVisibility')
|
||||
setTitle('content_submit_text', 'tipSend')
|
||||
setTitle('fullscreen', 'tipFullscreen')
|
||||
setTitle('editor-resize-handle', 'tipResize')
|
||||
}
|
||||
|
||||
async function setUiLanguage(nextLang, { persist = true } = {}) {
|
||||
|
||||
+705
-73
@@ -1,6 +1,136 @@
|
||||
dayjs.extend(window.dayjs_plugin_relativeTime)
|
||||
let currentMemoLock = ''
|
||||
|
||||
function isFullscreenMode() {
|
||||
try {
|
||||
const params = new URLSearchParams(window.location.search || '')
|
||||
return params.get('mode') === 'full'
|
||||
} catch (_) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function openFullscreenTab() {
|
||||
try {
|
||||
const url = chrome.runtime.getURL('popup.html?mode=full')
|
||||
chrome.tabs.create({ url })
|
||||
} catch (_) {
|
||||
// best-effort only
|
||||
}
|
||||
}
|
||||
|
||||
function initProportionalEditorResize() {
|
||||
try {
|
||||
if (isFullscreenMode()) return
|
||||
|
||||
const editor = document.querySelector('.memo-editor')
|
||||
const tools = document.querySelector('.common-tools-wrapper')
|
||||
const handle = document.getElementById('editor-resize-handle')
|
||||
if (!editor || !tools || !handle) return
|
||||
|
||||
const safety = 8
|
||||
const initialRect = editor.getBoundingClientRect()
|
||||
const baseW = Math.ceil(initialRect.width)
|
||||
const baseH = Math.ceil(initialRect.height)
|
||||
|
||||
// Lock the base size. Scaling will be applied by setting width/height.
|
||||
editor.style.width = `${baseW}px`
|
||||
editor.style.height = `${baseH}px`
|
||||
editor.style.minWidth = `${baseW}px`
|
||||
editor.style.minHeight = `${baseH}px`
|
||||
|
||||
let maxScale = 1
|
||||
const computeMaxScale = () => {
|
||||
// In popup mode, allow scaling up to Chrome's max popup size.
|
||||
// Do not clamp by current window.innerWidth/innerHeight, otherwise the popup can't grow to the max.
|
||||
const viewportW = 800
|
||||
const viewportH = 600
|
||||
|
||||
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 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 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
|
||||
}
|
||||
|
||||
handle.addEventListener('pointerup', endDrag)
|
||||
handle.addEventListener('pointercancel', endDrag)
|
||||
} catch (_) {
|
||||
// best-effort only
|
||||
}
|
||||
}
|
||||
|
||||
function msg(key) {
|
||||
if (typeof window.t === 'function') return window.t(key)
|
||||
return chrome.i18n.getMessage(key) || ''
|
||||
@@ -57,6 +187,10 @@ function updateLockNowText(lockType) {
|
||||
|
||||
applyDayjsLocaleByUiLanguage(typeof window.getUiLanguage === 'function' ? window.getUiLanguage() : 'auto')
|
||||
|
||||
if (isFullscreenMode()) {
|
||||
document.body.classList.add('fullscreen')
|
||||
}
|
||||
|
||||
window.addEventListener('i18n:changed', (ev) => {
|
||||
applyDayjsLocaleByUiLanguage(ev && ev.detail ? ev.detail.lang : 'auto')
|
||||
updateLockNowText(currentMemoLock)
|
||||
@@ -70,6 +204,7 @@ function get_info(callback) {
|
||||
{
|
||||
apiUrl: '',
|
||||
apiTokens: '',
|
||||
apiFlavor: '',
|
||||
hidetag: '',
|
||||
showtag: '',
|
||||
memo_lock: '',
|
||||
@@ -90,6 +225,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 +240,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) {
|
||||
//已经有绑定信息了,折叠
|
||||
@@ -126,11 +277,14 @@ get_info(function (info) {
|
||||
//打开的时候就是上传图片
|
||||
uploadImage(info.open_content)
|
||||
} else {
|
||||
$("textarea[name=text]").val(info.open_content)
|
||||
const $textarea = $("textarea[name=text]")
|
||||
$textarea.val(info.open_content)
|
||||
focusTextareaToEnd($textarea)
|
||||
}
|
||||
|
||||
relistNow = Array.isArray(info.resourceIdList) ? info.resourceIdList : []
|
||||
renderUploadList(relistNow)
|
||||
initProportionalEditorResize()
|
||||
//从localstorage 里面读取数据
|
||||
setTimeout(get_info, 1)
|
||||
})
|
||||
@@ -144,7 +298,7 @@ chrome.storage.onChanged.addListener(function (changes, areaName) {
|
||||
renderUploadList(relistNow)
|
||||
})
|
||||
|
||||
$("textarea[name=text]").focus()
|
||||
// focus is handled after textarea content is set
|
||||
|
||||
//监听输入结束,保存未发送内容到本地
|
||||
$("textarea[name=text]").blur(function () {
|
||||
@@ -159,6 +313,11 @@ $("textarea[name=text]").on('keydown', function (ev) {
|
||||
}
|
||||
})
|
||||
|
||||
$('#fullscreen').on('click', function () {
|
||||
if (isFullscreenMode()) return
|
||||
openFullscreenTab()
|
||||
})
|
||||
|
||||
//监听拖拽事件,实现拖拽到窗口上传图片
|
||||
initDrag()
|
||||
|
||||
@@ -222,6 +381,224 @@ function escapeHtml(input) {
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
function focusTextareaToEnd($textarea) {
|
||||
try {
|
||||
const el = $textarea && $textarea[0]
|
||||
if (!el) return
|
||||
el.focus()
|
||||
const len = typeof el.value === 'string' ? el.value.length : 0
|
||||
if (typeof el.setSelectionRange === 'function') {
|
||||
el.setSelectionRange(len, len)
|
||||
}
|
||||
} catch (_) {
|
||||
// best-effort only
|
||||
}
|
||||
}
|
||||
|
||||
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 +620,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 +640,8 @@ function renderUploadList(list) {
|
||||
'</div>' +
|
||||
'<button type="button" class="upload-del" data-name="' +
|
||||
escapeHtml(name) +
|
||||
'" data-id="' +
|
||||
escapeHtml(id) +
|
||||
'" title="' +
|
||||
tipDelete +
|
||||
'">×</button>' +
|
||||
@@ -327,6 +709,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 +718,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 +753,10 @@ $(document).on('click', '.upload-del', function () {
|
||||
renderUploadList(relistNow)
|
||||
})
|
||||
},
|
||||
error: function () {
|
||||
function () {
|
||||
$.message({ message: msg('attachmentDeleteFailed') })
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
function uploadImage(file) {
|
||||
@@ -359,6 +764,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 +779,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 +862,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 +881,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 +925,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 +961,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 +1072,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 +1103,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 +1115,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 +1136,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 +1159,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 +1196,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 +1228,7 @@ function randDom(randomData){
|
||||
randomDom += '</div>'
|
||||
window.ViewImage && ViewImage.init('.random-image')
|
||||
$("#randomlist").html(randomDom).slideDown(500);
|
||||
hydrateV1PreviewImages(info)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -683,6 +1244,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 +1358,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 +1396,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",
|
||||
|
||||
+4
-2
@@ -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.10",
|
||||
"version_name": "Supports 0.18.0 to the latest version",
|
||||
"action": {
|
||||
"default_popup": "popup.html",
|
||||
"default_icon": "assets/logo_24x24.png",
|
||||
@@ -21,6 +21,8 @@
|
||||
},
|
||||
"permissions": [
|
||||
"tabs",
|
||||
"scripting",
|
||||
"windows",
|
||||
"storage",
|
||||
"activeTab",
|
||||
"contextMenus"
|
||||
|
||||
+11
-1
@@ -51,6 +51,13 @@
|
||||
</div>
|
||||
|
||||
<div class="memo-editor">
|
||||
<div class="memo-editor-header">
|
||||
<button id="fullscreen" class="action-btn" type="button" aria-label="">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-fullscreen" viewBox="0 0 16 16">
|
||||
<path d="M1.5 1a.5.5 0 0 0-.5.5v4a.5.5 0 0 1-1 0v-4A1.5 1.5 0 0 1 1.5 0h4a.5.5 0 0 1 0 1zM10 .5a.5.5 0 0 1 .5-.5h4A1.5 1.5 0 0 1 16 1.5v4a.5.5 0 0 1-1 0v-4a.5.5 0 0 0-.5-.5h-4a.5.5 0 0 1-.5-.5M.5 10a.5.5 0 0 1 .5.5v4a.5.5 0 0 0 .5.5h4a.5.5 0 0 1 0 1h-4A1.5 1.5 0 0 1 0 14.5v-4a.5.5 0 0 1 .5-.5m15 0a.5.5 0 0 1 .5.5v4a1.5 1.5 0 0 1-1.5 1.5h-4a.5.5 0 0 1 0-1h4a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 1 .5-.5"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
class="common-editor-inputer"
|
||||
rows="4"
|
||||
@@ -59,6 +66,7 @@
|
||||
placeholder=""
|
||||
required=""
|
||||
></textarea>
|
||||
<div id="editor-resize-handle" aria-label="Resize"></div>
|
||||
</div>
|
||||
|
||||
<div class="common-tools-wrapper">
|
||||
@@ -151,7 +159,9 @@
|
||||
<script src="../js/ko.js"></script>
|
||||
<script src="../js/relativeTime.js"></script>
|
||||
<script src="../js/view-image.js"></script>
|
||||
<script src="../js/memosApi.js"></script>
|
||||
<script src="../js/compat/memosApi.v024.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>
|
||||
|
||||
Reference in New Issue
Block a user