mirror of
https://github.com/Jonnyan404/memos-bber.git
synced 2026-06-25 06:46:21 +09:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 944be49edb | |||
| 25fbd486bf | |||
| 15d99cb51f | |||
| ed5fc86e39 | |||
| 0f468d49aa | |||
| f3e55ec53e | |||
| f05581f88a | |||
| d237a7f1c6 | |||
| 13cc7659ea | |||
| e9730b5839 | |||
| f2f1ff2c10 | |||
| 5fac00b5ce | |||
| 2b36fda137 | |||
| da150b8788 | |||
| 85cc964836 | |||
| adfd797e84 | |||
| 8f51bb399b | |||
| 30b7cf2491 |
@@ -5,6 +5,13 @@ Chrome 应用商店:<https://chrome.google.com/webstore/detail/memos-bber/cbhj
|
|||||||
一个通过浏览器插件发布 [Memos](https://usememos.com/) 的插件。基于 iSpeak-bber 修改,原作者为 [DreamyTZK](https://www.antmoe.com/)。
|
一个通过浏览器插件发布 [Memos](https://usememos.com/) 的插件。基于 iSpeak-bber 修改,原作者为 [DreamyTZK](https://www.antmoe.com/)。
|
||||||
|
|
||||||
## 更新日志
|
## 更新日志
|
||||||
|
- 20260421 更新匹配 0.27.x
|
||||||
|
- 20260325 优化语言按钮样式
|
||||||
|
- 20260323 优化中文显示效果
|
||||||
|
- 20260322 适配移动端竖屏窗口
|
||||||
|
- 20260310 记忆拖拽窗口大小,移除拖拽窗口动画
|
||||||
|
- 20260309 右键发送选中文本保持原格式,增加全屏和窗口放大功能
|
||||||
|
### 20260308 向前兼容到0.15.0,可能再往前也行,只测试到0.15.0
|
||||||
- 20260307 增加语言切换按钮以及韩语和日语支持,
|
- 20260307 增加语言切换按钮以及韩语和日语支持,
|
||||||
- 2026年03月06日 右键菜单发送选中文本附带原文链接
|
- 2026年03月06日 右键菜单发送选中文本附带原文链接
|
||||||
- 2026年03月05日 向前兼容到0.24.0,可能再往前也行,因为只测试了0.24.0和0.25.0以及当前最新版本,如有更早版本需求,可issue反馈
|
- 2026年03月05日 向前兼容到0.24.0,可能再往前也行,因为只测试了0.24.0和0.25.0以及当前最新版本,如有更早版本需求,可issue反馈
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
"message": "Save"
|
"message": "Save"
|
||||||
},
|
},
|
||||||
"supportedMemosVersion": {
|
"supportedMemosVersion": {
|
||||||
"message": "Compatible with Memos v0.24.0+ (tested: 0.24.0 / 0.25.0 / 0.26.x)"
|
"message": "Compatible with Memos v0.15.0 - 0.27.x"
|
||||||
},
|
},
|
||||||
"placeApiUrl":{
|
"placeApiUrl":{
|
||||||
"message": "Memos site URL"
|
"message": "Memos site URL"
|
||||||
@@ -163,5 +163,11 @@
|
|||||||
},
|
},
|
||||||
"langKorean": {
|
"langKorean": {
|
||||||
"message": "한국어"
|
"message": "한국어"
|
||||||
|
},
|
||||||
|
"tipFullscreen": {
|
||||||
|
"message": "Open fullscreen editor"
|
||||||
|
},
|
||||||
|
"tipResize": {
|
||||||
|
"message": "Drag to resize (min: default size)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"message": "保存"
|
"message": "保存"
|
||||||
},
|
},
|
||||||
"supportedMemosVersion": {
|
"supportedMemosVersion": {
|
||||||
"message": "Memos v0.24.0+ に対応(テスト済み: 0.24.0 / 0.25.0 / 0.26.x)"
|
"message": "Memos v0.15.0 - 0.27.x に対応"
|
||||||
},
|
},
|
||||||
"placeApiUrl": {
|
"placeApiUrl": {
|
||||||
"message": "Memos サイトURL"
|
"message": "Memos サイトURL"
|
||||||
@@ -163,5 +163,11 @@
|
|||||||
},
|
},
|
||||||
"langKorean": {
|
"langKorean": {
|
||||||
"message": "한국어"
|
"message": "한국어"
|
||||||
|
},
|
||||||
|
"tipFullscreen": {
|
||||||
|
"message": "全画面で編集"
|
||||||
|
},
|
||||||
|
"tipResize": {
|
||||||
|
"message": "ドラッグで拡大/縮小(最小:初期サイズ)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"message": "저장"
|
"message": "저장"
|
||||||
},
|
},
|
||||||
"supportedMemosVersion": {
|
"supportedMemosVersion": {
|
||||||
"message": "Memos v0.24.0+ 호환 (테스트됨: 0.24.0 / 0.25.0 / 0.26.x)"
|
"message": "Memos v0.15.0 - 0.27.x 호환"
|
||||||
},
|
},
|
||||||
"placeApiUrl": {
|
"placeApiUrl": {
|
||||||
"message": "Memos 사이트 URL"
|
"message": "Memos 사이트 URL"
|
||||||
@@ -163,5 +163,11 @@
|
|||||||
},
|
},
|
||||||
"langKorean": {
|
"langKorean": {
|
||||||
"message": "한국어"
|
"message": "한국어"
|
||||||
|
},
|
||||||
|
"tipFullscreen": {
|
||||||
|
"message": "전체화면 편집"
|
||||||
|
},
|
||||||
|
"tipResize": {
|
||||||
|
"message": "드래그로 확대/축소(최소: 기본 크기)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
"message": "保存"
|
"message": "保存"
|
||||||
},
|
},
|
||||||
"supportedMemosVersion": {
|
"supportedMemosVersion": {
|
||||||
"message": "兼容 Memos v0.24.0+(已测试:0.24.0 / 0.25.0 / 0.26.x)"
|
"message": "兼容 Memos v0.15.0 - 0.27.x"
|
||||||
},
|
},
|
||||||
"placeApiUrl":{
|
"placeApiUrl":{
|
||||||
"message": "请填入 Memos 主页网址"
|
"message": "请填入 Memos 主页网址"
|
||||||
@@ -33,22 +33,22 @@
|
|||||||
"message": "现在的想法是..."
|
"message": "现在的想法是..."
|
||||||
},
|
},
|
||||||
"lockPrivate":{
|
"lockPrivate":{
|
||||||
"message": "仅自己可见"
|
"message": "私有"
|
||||||
},
|
},
|
||||||
"lockProtected":{
|
"lockProtected":{
|
||||||
"message": "登录用户可见"
|
"message": "登录可见"
|
||||||
},
|
},
|
||||||
"lockPublic":{
|
"lockPublic":{
|
||||||
"message": "所有人可见"
|
"message": "公开"
|
||||||
},
|
},
|
||||||
"submitBtn":{
|
"submitBtn":{
|
||||||
"message": "记下"
|
"message": "记下"
|
||||||
},
|
},
|
||||||
"placeHideInput":{
|
"placeHideInput":{
|
||||||
"message": "默认“仅自己可见”标签名"
|
"message": "默认“私有”标签名"
|
||||||
},
|
},
|
||||||
"placeShowInput":{
|
"placeShowInput":{
|
||||||
"message": "默认“所有人可见”标签名"
|
"message": "默认“公开”标签名"
|
||||||
},
|
},
|
||||||
"picDrag":{
|
"picDrag":{
|
||||||
"message": "拖拽到窗口上传该图片"
|
"message": "拖拽到窗口上传该图片"
|
||||||
@@ -163,5 +163,11 @@
|
|||||||
},
|
},
|
||||||
"langKorean": {
|
"langKorean": {
|
||||||
"message": "한국어"
|
"message": "한국어"
|
||||||
|
},
|
||||||
|
"tipFullscreen": {
|
||||||
|
"message": "全屏编辑"
|
||||||
|
},
|
||||||
|
"tipResize": {
|
||||||
|
"message": "拖拽缩放编辑框(最小为默认大小)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+158
-11
@@ -20,7 +20,7 @@ input:focus::placeholder ,.common-editor-inputer:focus::placeholder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.body{
|
.body{
|
||||||
min-width:460px;
|
min-width:360px;
|
||||||
background-color: #f6f5f4;
|
background-color: #f6f5f4;
|
||||||
padding:0 1rem 1rem;
|
padding:0 1rem 1rem;
|
||||||
font-family: eafont,PingFang SC,Hiragino Sans GB,Microsoft YaHei,STHeiti,WenQuanYi Micro Hei,Helvetica,Arial,sans-serif;
|
font-family: eafont,PingFang SC,Hiragino Sans GB,Microsoft YaHei,STHeiti,WenQuanYi Micro Hei,Helvetica,Arial,sans-serif;
|
||||||
@@ -29,6 +29,7 @@ input:focus::placeholder ,.common-editor-inputer:focus::placeholder {
|
|||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
a{color: #555;}
|
a{color: #555;}
|
||||||
.title{
|
.title{
|
||||||
width: 100px;
|
width: 100px;
|
||||||
@@ -44,9 +45,51 @@ a{color: #555;}
|
|||||||
background-color: rgb(255,255,255);
|
background-color: rgb(255,255,255);
|
||||||
margin-top:0.8rem;
|
margin-top:0.8rem;
|
||||||
padding: 0.6rem;
|
padding: 0.6rem;
|
||||||
transition-property: all;
|
}
|
||||||
transition-timing-function: cubic-bezier(.4,0,.2,1);
|
.memo-editor{
|
||||||
transition-duration: .15s;
|
position: relative;
|
||||||
|
resize: none;
|
||||||
|
overflow: visible;
|
||||||
|
box-sizing: border-box;
|
||||||
|
contain: layout;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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{
|
.random-item{
|
||||||
border: 1px solid rgb(229,231,235);
|
border: 1px solid rgb(229,231,235);
|
||||||
@@ -66,6 +109,32 @@ a{color: #555;}
|
|||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
word-break: normal;}
|
word-break: normal;}
|
||||||
.btns-container{text-align:right;}
|
.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{
|
.common-editor-inputer,input.inputer{
|
||||||
font-family: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;
|
font-family: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -77,10 +146,28 @@ a{color: #555;}
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
max-height: 400px;
|
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
line-height: 1.5rem;
|
line-height: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.common-editor-inputer{
|
||||||
|
padding-right: 1.5rem;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
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%;}
|
input.inputer{border-bottom: 1px solid #ccc;width:75%;}
|
||||||
|
|
||||||
#saveKey{margin:0;flex:1;}
|
#saveKey{margin:0;flex:1;}
|
||||||
@@ -238,20 +325,80 @@ input.inputer{border-bottom: 1px solid #ccc;width:75%;}
|
|||||||
top: .55rem;
|
top: .55rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lang-select{
|
.lang-toggle{
|
||||||
border: 1px solid rgb(229,231,235);
|
border: none;
|
||||||
border-radius: .25rem;
|
border-radius: 0;
|
||||||
background-color: rgb(255,255,255);
|
background-color: transparent;
|
||||||
color: #666;
|
color: #666;
|
||||||
|
min-width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0;
|
||||||
|
opacity: .6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-toggle:hover,
|
||||||
|
.lang-toggle[aria-expanded="true"]{
|
||||||
|
background-color: transparent;
|
||||||
|
color: #666;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-toggle-text{
|
||||||
|
display: inline-block;
|
||||||
|
min-width: 24px;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-menu{
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + .35rem);
|
||||||
|
right: 0;
|
||||||
|
min-width: 8rem;
|
||||||
|
padding: .25rem;
|
||||||
|
border: 1px solid rgb(229,231,235);
|
||||||
|
border-radius: .5rem;
|
||||||
|
background-color: rgb(255,255,255);
|
||||||
|
box-shadow: 0 8px 24px rgba(15,23,42,.12);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-menu.hidden{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-menu-item{
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
text-align: left;
|
||||||
|
padding: .4rem .5rem;
|
||||||
|
border-radius: .35rem;
|
||||||
|
background: transparent;
|
||||||
|
color: #555;
|
||||||
font-size: .75rem;
|
font-size: .75rem;
|
||||||
line-height: 1.25rem;
|
line-height: 1.25rem;
|
||||||
padding: .15rem .35rem;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
#blog_info{
|
|
||||||
|
|
||||||
|
.lang-menu-item:hover{
|
||||||
|
background-color: rgb(243,244,246);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lang-menu-item.active{
|
||||||
|
background-color: rgb(220,252,231);
|
||||||
|
color: rgb(22,101,52);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.tip{
|
.tip{
|
||||||
margin-left: 36%;
|
margin-left: 36%;
|
||||||
max-width: 640px;
|
max-width: 640px;
|
||||||
|
|||||||
+116
-25
@@ -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 cachedUiLanguage = null
|
||||||
let cachedOverrideMessages = null
|
let cachedOverrideMessages = null
|
||||||
|
|
||||||
@@ -93,35 +176,43 @@ chrome.storage.onChanged.addListener((changes, areaName) => {
|
|||||||
refreshContextMenus()
|
refreshContextMenus()
|
||||||
})
|
})
|
||||||
|
|
||||||
chrome.contextMenus.onClicked.addListener((info) => {
|
chrome.contextMenus.onClicked.addListener((info, tab) => {
|
||||||
let tempCont = ''
|
const appendContent = (tempCont, { openPopup } = { openPopup: false }) => {
|
||||||
switch (info.menuItemId) {
|
chrome.storage.sync.get({ open_action: 'save_text', open_content: '' }, function (items) {
|
||||||
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) {
|
|
||||||
if (items.open_action === 'upload_image') {
|
if (items.open_action === 'upload_image') {
|
||||||
t('picPending').then((m) => alert(m))
|
t('picPending').then((m) => alert(m))
|
||||||
} else {
|
return
|
||||||
chrome.storage.sync.set({
|
}
|
||||||
|
|
||||||
|
chrome.storage.sync.set(
|
||||||
|
{
|
||||||
open_action: 'save_text',
|
open_action: 'save_text',
|
||||||
open_content: items.open_content + tempCont
|
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)
|
||||||
@@ -11,12 +11,19 @@
|
|||||||
return Number(user.id)
|
return Number(user.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof user.username === 'string' && user.username.trim() !== '') {
|
||||||
|
return user.username.trim()
|
||||||
|
}
|
||||||
|
|
||||||
const name = user.name || (user.user && user.user.name)
|
const name = user.name || (user.user && user.user.name)
|
||||||
if (typeof name === 'string') {
|
if (typeof name === 'string') {
|
||||||
const m = name.match(/\busers\/(\d+)\b/)
|
const m = name.match(/\busers\/(\d+)\b/)
|
||||||
if (m) return Number(m[1])
|
if (m) return Number(m[1])
|
||||||
const last = name.split('/').pop()
|
const last = name.split('/').pop()
|
||||||
if (last && !Number.isNaN(Number(last))) return Number(last)
|
if (last) {
|
||||||
|
if (!Number.isNaN(Number(last))) return Number(last)
|
||||||
|
if (last.trim() !== '') return last.trim()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
@@ -45,6 +52,8 @@
|
|||||||
{ method: 'GET', path: 'api/v1/auth/me', uiPath: 'memos' },
|
{ method: 'GET', path: 'api/v1/auth/me', uiPath: 'memos' },
|
||||||
// v0.25: session-based auth service still accepts bearer tokens and returns { user: ... }.
|
// v0.25: session-based auth service still accepts bearer tokens and returns { user: ... }.
|
||||||
{ method: 'GET', path: 'api/v1/auth/sessions/current', uiPath: 'memos' },
|
{ 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: 'POST', path: 'api/v1/auth/status', uiPath: 'm' },
|
||||||
{ method: 'GET', path: 'api/v1/auth/status', uiPath: 'm' }
|
{ method: 'GET', path: 'api/v1/auth/status', uiPath: 'm' }
|
||||||
]
|
]
|
||||||
@@ -86,7 +95,7 @@
|
|||||||
// the full set (including private), which affects tag extraction.
|
// the full set (including private), which affects tag extraction.
|
||||||
// Newer versions may not expose the user-scoped endpoint, so we fallback by 404/405.
|
// Newer versions may not expose the user-scoped endpoint, so we fallback by 404/405.
|
||||||
const urlUserScoped = info.userid
|
const urlUserScoped = info.userid
|
||||||
? info.apiUrl + 'api/v1/users/' + info.userid + '/memos' + qs
|
? info.apiUrl + 'api/v1/users/' + encodeURIComponent(String(info.userid)) + '/memos' + qs
|
||||||
: null
|
: null
|
||||||
const urlGlobal = info.apiUrl + 'api/v1/memos' + qs
|
const urlGlobal = info.apiUrl + 'api/v1/memos' + qs
|
||||||
|
|
||||||
@@ -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)
|
||||||
+68
-11
@@ -47,6 +47,36 @@ function formatSubstitutions(message, substitutions) {
|
|||||||
let currentUiLanguage = 'auto'
|
let currentUiLanguage = 'auto'
|
||||||
let overrideMessages = null
|
let overrideMessages = null
|
||||||
|
|
||||||
|
function getLanguageToggleLabel(lang) {
|
||||||
|
if (lang === 'en') return 'EN'
|
||||||
|
if (lang === 'zh_CN') return '中'
|
||||||
|
if (lang === 'ja') return '日'
|
||||||
|
if (lang === 'ko') return '한'
|
||||||
|
return 'A'
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncLanguageToggleText(lang) {
|
||||||
|
const text = document.getElementById('langToggleText')
|
||||||
|
if (text) text.textContent = getLanguageToggleLabel(lang)
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncLanguageMenuState(lang) {
|
||||||
|
const items = document.querySelectorAll('.lang-menu-item')
|
||||||
|
items.forEach((item) => {
|
||||||
|
const isActive = item.getAttribute('data-lang') === lang
|
||||||
|
item.classList.toggle('active', isActive)
|
||||||
|
item.setAttribute('aria-checked', isActive ? 'true' : 'false')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLanguageMenuOpen(isOpen) {
|
||||||
|
const toggle = document.getElementById('langToggle')
|
||||||
|
const menu = document.getElementById('langMenu')
|
||||||
|
if (!toggle || !menu) return
|
||||||
|
toggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false')
|
||||||
|
menu.classList.toggle('hidden', !isOpen)
|
||||||
|
}
|
||||||
|
|
||||||
function t(key, substitutions) {
|
function t(key, substitutions) {
|
||||||
const msg = overrideMessages && overrideMessages[key] && overrideMessages[key].message
|
const msg = overrideMessages && overrideMessages[key] && overrideMessages[key].message
|
||||||
if (typeof msg === 'string' && msg.length > 0) {
|
if (typeof msg === 'string' && msg.length > 0) {
|
||||||
@@ -86,6 +116,8 @@ function applyStaticI18n() {
|
|||||||
setText('lockPublic', 'lockPublic')
|
setText('lockPublic', 'lockPublic')
|
||||||
|
|
||||||
setText('content_submit_text', 'submitBtn')
|
setText('content_submit_text', 'submitBtn')
|
||||||
|
const fullscreen = document.getElementById('fullscreen')
|
||||||
|
if (fullscreen) fullscreen.setAttribute('aria-label', t('tipFullscreen'))
|
||||||
|
|
||||||
setPlaceholder('hideInput', 'placeHideInput')
|
setPlaceholder('hideInput', 'placeHideInput')
|
||||||
setPlaceholder('showInput', 'placeShowInput')
|
setPlaceholder('showInput', 'placeShowInput')
|
||||||
@@ -98,7 +130,9 @@ function applyStaticI18n() {
|
|||||||
setText('langOptionZhCN', 'langChineseSimplified')
|
setText('langOptionZhCN', 'langChineseSimplified')
|
||||||
setText('langOptionJa', 'langJapanese')
|
setText('langOptionJa', 'langJapanese')
|
||||||
setText('langOptionKo', 'langKorean')
|
setText('langOptionKo', 'langKorean')
|
||||||
setTitle('langSelect', 'tipLanguage')
|
setTitle('langToggle', 'tipLanguage')
|
||||||
|
const langToggle = document.getElementById('langToggle')
|
||||||
|
if (langToggle) langToggle.setAttribute('aria-label', t('tipLanguage'))
|
||||||
|
|
||||||
// Native hover tooltips (title)
|
// Native hover tooltips (title)
|
||||||
setTitle('opensite', 'tipOpenSite')
|
setTitle('opensite', 'tipOpenSite')
|
||||||
@@ -111,6 +145,8 @@ function applyStaticI18n() {
|
|||||||
setTitle('search', 'tipSearch')
|
setTitle('search', 'tipSearch')
|
||||||
setTitle('lock', 'tipVisibility')
|
setTitle('lock', 'tipVisibility')
|
||||||
setTitle('content_submit_text', 'tipSend')
|
setTitle('content_submit_text', 'tipSend')
|
||||||
|
setTitle('fullscreen', 'tipFullscreen')
|
||||||
|
setTitle('editor-resize-handle', 'tipResize')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setUiLanguage(nextLang, { persist = true } = {}) {
|
async function setUiLanguage(nextLang, { persist = true } = {}) {
|
||||||
@@ -118,26 +154,47 @@ async function setUiLanguage(nextLang, { persist = true } = {}) {
|
|||||||
currentUiLanguage = lang
|
currentUiLanguage = lang
|
||||||
overrideMessages = await loadLocaleMessages(lang)
|
overrideMessages = await loadLocaleMessages(lang)
|
||||||
applyStaticI18n()
|
applyStaticI18n()
|
||||||
|
syncLanguageToggleText(lang)
|
||||||
const select = document.getElementById('langSelect')
|
syncLanguageMenuState(lang)
|
||||||
if (select && select.value !== lang) select.value = lang
|
|
||||||
|
|
||||||
if (persist) await storageSyncSet({ [UI_LANGUAGE_STORAGE_KEY]: lang })
|
if (persist) await storageSyncSet({ [UI_LANGUAGE_STORAGE_KEY]: lang })
|
||||||
window.dispatchEvent(new CustomEvent('i18n:changed', { detail: { lang } }))
|
window.dispatchEvent(new CustomEvent('i18n:changed', { detail: { lang } }))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initLanguageSwitcher() {
|
async function initLanguageSwitcher() {
|
||||||
const select = document.getElementById('langSelect')
|
const switcher = document.getElementById('lang_switcher')
|
||||||
if (select) {
|
const toggle = document.getElementById('langToggle')
|
||||||
select.addEventListener('change', async () => {
|
const langItems = document.querySelectorAll('.lang-menu-item')
|
||||||
await setUiLanguage(select.value)
|
|
||||||
|
if (toggle) {
|
||||||
|
toggle.addEventListener('click', (event) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
const isOpen = toggle.getAttribute('aria-expanded') === 'true'
|
||||||
|
setLanguageMenuOpen(!isOpen)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = await storageSyncGet({ [UI_LANGUAGE_STORAGE_KEY]: 'auto' })
|
langItems.forEach((item) => {
|
||||||
const stored = normalizeUiLanguage(items[UI_LANGUAGE_STORAGE_KEY])
|
item.addEventListener('click', async (event) => {
|
||||||
if (select) select.value = stored
|
event.stopPropagation()
|
||||||
|
setLanguageMenuOpen(false)
|
||||||
|
await setUiLanguage(item.getAttribute('data-lang'))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
document.addEventListener('click', (event) => {
|
||||||
|
if (!switcher || switcher.contains(event.target)) return
|
||||||
|
setLanguageMenuOpen(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === 'Escape') setLanguageMenuOpen(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
const storedItems = await storageSyncGet({ [UI_LANGUAGE_STORAGE_KEY]: 'auto' })
|
||||||
|
const stored = normalizeUiLanguage(storedItems[UI_LANGUAGE_STORAGE_KEY])
|
||||||
await setUiLanguage(stored, { persist: false })
|
await setUiLanguage(stored, { persist: false })
|
||||||
|
setLanguageMenuOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
window.t = t
|
window.t = t
|
||||||
|
|||||||
+753
-50
File diff suppressed because it is too large
Load Diff
+4
-2
@@ -2,8 +2,8 @@
|
|||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "__MSG_extName__",
|
"name": "__MSG_extName__",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"version": "2026.03.07",
|
"version": "2026.04.21",
|
||||||
"version_name": "Supports 0.24.0 to the latest version",
|
"version_name": "Supports 0.15.0 - 0.27.x",
|
||||||
"action": {
|
"action": {
|
||||||
"default_popup": "popup.html",
|
"default_popup": "popup.html",
|
||||||
"default_icon": "assets/logo_24x24.png",
|
"default_icon": "assets/logo_24x24.png",
|
||||||
@@ -21,6 +21,8 @@
|
|||||||
},
|
},
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"tabs",
|
"tabs",
|
||||||
|
"scripting",
|
||||||
|
"windows",
|
||||||
"storage",
|
"storage",
|
||||||
"activeTab",
|
"activeTab",
|
||||||
"contextMenus"
|
"contextMenus"
|
||||||
|
|||||||
+27
-8
@@ -13,13 +13,22 @@
|
|||||||
<body class="body">
|
<body class="body">
|
||||||
<div class="title" id="opensite">MEMOS</div>
|
<div class="title" id="opensite">MEMOS</div>
|
||||||
<div id="lang_switcher" class="lang-switcher">
|
<div id="lang_switcher" class="lang-switcher">
|
||||||
<select id="langSelect" class="lang-select" aria-label="Language" title="">
|
<button
|
||||||
<option id="langOptionAuto" value="auto"></option>
|
id="langToggle"
|
||||||
<option id="langOptionEn" value="en"></option>
|
class="lang-toggle"
|
||||||
<option id="langOptionZhCN" value="zh_CN"></option>
|
type="button"
|
||||||
<option id="langOptionJa" value="ja"></option>
|
aria-haspopup="true"
|
||||||
<option id="langOptionKo" value="ko"></option>
|
aria-expanded="false"
|
||||||
</select>
|
>
|
||||||
|
<span id="langToggleText" class="lang-toggle-text" aria-hidden="true">A</span>
|
||||||
|
</button>
|
||||||
|
<div id="langMenu" class="lang-menu hidden" role="menu" aria-labelledby="langToggle">
|
||||||
|
<button id="langOptionAuto" class="lang-menu-item" type="button" data-lang="auto" role="menuitemradio" aria-checked="false"></button>
|
||||||
|
<button id="langOptionEn" class="lang-menu-item" type="button" data-lang="en" role="menuitemradio" aria-checked="false"></button>
|
||||||
|
<button id="langOptionZhCN" class="lang-menu-item" type="button" data-lang="zh_CN" role="menuitemradio" aria-checked="false"></button>
|
||||||
|
<button id="langOptionJa" class="lang-menu-item" type="button" data-lang="ja" role="menuitemradio" aria-checked="false"></button>
|
||||||
|
<button id="langOptionKo" class="lang-menu-item" type="button" data-lang="ko" role="menuitemradio" aria-checked="false"></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="blog_info_edit"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024">
|
<div id="blog_info_edit"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024">
|
||||||
<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="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"/>
|
||||||
@@ -51,6 +60,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="memo-editor">
|
<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
|
<textarea
|
||||||
class="common-editor-inputer"
|
class="common-editor-inputer"
|
||||||
rows="4"
|
rows="4"
|
||||||
@@ -59,6 +75,7 @@
|
|||||||
placeholder=""
|
placeholder=""
|
||||||
required=""
|
required=""
|
||||||
></textarea>
|
></textarea>
|
||||||
|
<div id="editor-resize-handle" aria-label="Resize"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="common-tools-wrapper">
|
<div class="common-tools-wrapper">
|
||||||
@@ -151,7 +168,9 @@
|
|||||||
<script src="../js/ko.js"></script>
|
<script src="../js/ko.js"></script>
|
||||||
<script src="../js/relativeTime.js"></script>
|
<script src="../js/relativeTime.js"></script>
|
||||||
<script src="../js/view-image.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>
|
<script src="../js/oper.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user