Add packaging workflow and browser dist

Add a GitHub Actions workflow (package-extensions.yml) to build and upload Chrome/Firefox packages (store and offline artifacts) on manual trigger or when pushing v* tags. Update README with packaging instructions. Reorganize extension sources into chrome/ and firefox/ directories, add Firefox-specific files (manifest, locales, assets, CSS, LICENSE), and bump Chrome manifest version to 2026.04.23. Also modify js/oper.js (moved to chrome/js) to improve proportional editor resizing: add drag-to-resize, scale clamping/persistence (localStorage + chrome.storage.sync), pointer event handlers, and max-scale computation.
This commit is contained in:
jonny
2026-04-22 18:53:42 +08:00
parent 15b8493a2b
commit d307741f1f
52 changed files with 5081 additions and 22 deletions
+62
View File
@@ -0,0 +1,62 @@
name: Package Extensions
on:
workflow_dispatch:
push:
tags:
- 'v*'
jobs:
package:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Read extension versions
id: versions
run: |
chrome_version=$(python - <<'PY'
import json
with open('chrome/manifest.json', 'r', encoding='utf-8') as fp:
print(json.load(fp)['version'])
PY
)
firefox_version=$(python - <<'PY'
import json
with open('firefox/manifest.json', 'r', encoding='utf-8') as fp:
print(json.load(fp)['version'])
PY
)
echo "chrome_version=$chrome_version" >> "$GITHUB_OUTPUT"
echo "firefox_version=$firefox_version" >> "$GITHUB_OUTPUT"
- name: Build package files
run: |
mkdir -p dist/store dist/offline
pushd chrome >/dev/null
zip -qr "../dist/store/memos-bber-chrome-webstore-${{ steps.versions.outputs.chrome_version }}.zip" .
zip -qr "../dist/offline/memos-bber-chrome-offline-${{ steps.versions.outputs.chrome_version }}.zip" .
popd >/dev/null
pushd firefox >/dev/null
zip -qr "../dist/store/memos-bber-firefox-amo-${{ steps.versions.outputs.firefox_version }}.xpi" .
zip -qr "../dist/offline/memos-bber-firefox-offline-${{ steps.versions.outputs.firefox_version }}.xpi" .
popd >/dev/null
- name: Upload store packages
uses: actions/upload-artifact@v4
with:
name: store-packages
path: dist/store/*
if-no-files-found: error
- name: Upload offline packages
uses: actions/upload-artifact@v4
with:
name: offline-packages
path: dist/offline/*
if-no-files-found: error
+12 -1
View File
@@ -1,9 +1,20 @@
## 说明
Chrome 应用商店:<https://chrome.google.com/webstore/detail/memos-bber/cbhjebjfccgchgbmfbobjmebjjckgofe/>
- Chrome 应用商店:https://chrome.google.com/webstore/detail/memos-bber/cbhjebjfccgchgbmfbobjmebjjckgofe/
- FireFox 应用商店: 等待上架
- edge 应用商店: 等待开发
一个通过浏览器插件发布 [Memos](https://usememos.com/) 的插件。基于 iSpeak-bber 修改,原作者为 [DreamyTZK](https://www.antmoe.com/)。
## 打包
仓库内置了 GitHub Actions 工作流 [package-extensions.yml](.github/workflows/package-extensions.yml),支持手动触发或在推送 `v*` 标签时自动打包。
- `store-packages`:商店上传用文件,包含 Chrome 的 zip 包和 Firefox 的 xpi 包。
- `offline-packages`:离线分发用文件,包含 Chrome 的 zip 包和 Firefox 的 xpi 包。
说明:Chrome 的离线包用于解压后在开发者模式中“加载已解压的扩展程序”;Firefox 的离线包为未签名 xpi,适合临时附加组件或自行签名后的分发场景。
## 更新日志
- 20260422 调整发送设置,支持仅发送附件
#### 20260421 更新匹配 0.27.x
View File

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File
View File
View File
View File
View File
+97 -19
View File
@@ -41,37 +41,115 @@ function initProportionalEditorResize() {
const storageKey = 'popupEditorScale'
const nonEditorHeight = Math.max(0, Math.ceil(document.body.scrollHeight - initialRect.height))
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
let currentScale = 1
let dragging = false
let dragStartX = 0
let dragStartY = 0
let dragStartScale = 1
const editorRect = editor.getBoundingClientRect()
const toolsRect = tools.getBoundingClientRect()
const toolsStyle = window.getComputedStyle(tools)
if (pendingScale != null) {
applyScale(pendingScale)
pendingScale = null
}
const clampScale = (scale) => {
if (!Number.isFinite(scale)) return 1
return Math.min(Math.max(scale, 1), maxScale)
}
// Persist current scale (best-effort).
const applyScale = (scale) => {
currentScale = clampScale(scale)
editor.style.width = `${Math.round(baseW * currentScale)}px`
editor.style.height = `${Math.round(baseH * currentScale)}px`
}
const persistScale = () => {
try {
const s = readCurrentScale()
if (typeof s === 'number' && Number.isFinite(s)) {
try {
if (window.localStorage) window.localStorage.setItem(storageKey, String(s))
} catch (_) {}
chrome.storage.sync.set({ [storageKey]: s })
if (window.localStorage) window.localStorage.setItem(storageKey, String(currentScale))
} catch (_) {}
try {
if (chrome.storage && chrome.storage.sync) {
chrome.storage.sync.set({ [storageKey]: currentScale })
}
} catch (_) {
// ignore
}
}
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 toolsRect = tools.getBoundingClientRect()
const toolsStyle = window.getComputedStyle(tools)
const toolsMarginTop = parseFloat(toolsStyle.marginTop || '0') || 0
const extraWidth = safety * 2
const extraHeight = nonEditorHeight + Math.ceil(toolsRect.height + toolsMarginTop) + safety
const widthScale = (viewportW - extraWidth) / baseW
const heightScale = (viewportH - extraHeight) / baseH
maxScale = Math.max(1, Math.min(widthScale, heightScale))
applyScale(currentScale)
}
const endDrag = () => {
if (!dragging) return
dragging = false
handle.classList.remove('dragging')
persistScale()
}
const onPointerMove = (ev) => {
if (!dragging) return
const dx = ev.clientX - dragStartX
const dy = ev.clientY - dragStartY
const widthScale = (baseW * dragStartScale + dx) / baseW
const heightScale = (baseH * dragStartScale + dy) / baseH
applyScale(Math.max(widthScale, heightScale))
}
const startDrag = (ev) => {
ev.preventDefault()
dragging = true
dragStartX = ev.clientX
dragStartY = ev.clientY
dragStartScale = currentScale
handle.classList.add('dragging')
if (typeof handle.setPointerCapture === 'function') {
try {
handle.setPointerCapture(ev.pointerId)
} catch (_) {
// Ignore capture failures.
}
}
}
computeMaxScale()
try {
const localValue = window.localStorage ? Number(window.localStorage.getItem(storageKey)) : NaN
if (Number.isFinite(localValue) && localValue >= 1) {
applyScale(localValue)
}
} catch (_) {
// ignore
}
try {
chrome.storage.sync.get({ [storageKey]: 1 }, (items) => {
const savedScale = Number(items && items[storageKey])
if (Number.isFinite(savedScale) && savedScale >= 1) {
applyScale(savedScale)
}
})
} catch (_) {
// ignore
}
handle.addEventListener('pointerdown', startDrag)
window.addEventListener('pointermove', onPointerMove)
handle.addEventListener('pointerup', endDrag)
handle.addEventListener('pointercancel', endDrag)
window.addEventListener('pointerup', endDrag)
window.addEventListener('resize', computeMaxScale)
} catch (_) {
// best-effort only
}
View File
+1 -2
View File
@@ -2,8 +2,7 @@
"manifest_version": 3,
"name": "__MSG_extName__",
"default_locale": "en",
"version": "2026.04.22",
"version_name": "Supports 0.15.0 - 0.27.x",
"version": "2026.04.23",
"action": {
"default_popup": "popup.html",
"default_icon": "assets/logo_24x24.png",
View File
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Charles Chin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+188
View File
@@ -0,0 +1,188 @@
{
"extName": {
"message": "Memos"
},
"actionTitle": {
"message": "Send Memos"
},
"extDescription": {
"message": "memos: A lightweight, self-hosted memo hub."
},
"sendTo": {
"message": "SendTo Memos \"%s\""
},
"sendLinkTo": {
"message": "Send link to Memos"
},
"sendImageTo": {
"message": "Send image to Memos"
},
"saveBtn":{
"message": "Save"
},
"supportedMemosVersion": {
"message": "Compatible with Memos v0.15.0 - 0.27.x"
},
"settingsConnectionTitle": {
"message": "Connection"
},
"settingsConnectionDesc": {
"message": "Configure the Memos site URL and access token."
},
"settingsPostingTitle": {
"message": "Posting"
},
"settingsPostingDesc": {
"message": "Default text for attachment-only sends"
},
"placeApiUrl":{
"message": "Memos site URL"
},
"placeApiTokens":{
"message": "Memos Access Tokens"
},
"placeContent":{
"message": "What's on your mind..."
},
"lockPrivate":{
"message": "Private"
},
"lockProtected":{
"message": "Protected"
},
"lockPublic":{
"message": "Public"
},
"submitBtn":{
"message": "Save"
},
"placeHideInput":{
"message": "Default 'Private' tag name"
},
"placeShowInput":{
"message": "Default 'Everyone can see' Tag name"
},
"placeAttachmentOnlyDefaultText": {
"message": "Default text for attachment-only sends (leave blank to use built-in text)"
},
"uploadedListTitle": {
"message": "Uploaded files, Drag to reorder"
},
"uploadedListEmpty": {
"message": "No uploaded files"
},
"tipReorder": {
"message": "Drag to reorder"
},
"tipDeleteAttachment": {
"message": "Delete"
},
"attachmentDeleteSuccess": {
"message": "Deleted"
},
"attachmentDeleteFailed": {
"message": "Delete failed 😭"
},
"picDrag":{
"message": "Drag upload the image"
},
"picCancelDrag":{
"message": "Cancel upload"
},
"picUploading":{
"message": "Upload the picture..."
},
"picSuccess":{
"message": "Upload completed"
},
"picFailed":{
"message": "Uploading failed"
},
"picPending":{
"message": "Image uploading is in progress"
},
"saveSuccess":{
"message": "Save Info Success!"
},
"searchNow":{
"message": "What are you looking for?"
},
"searchNone":{
"message": "Try another word!"
},
"archiveSuccess":{
"message": "Archive Success 😊"
},
"archiveFailed":{
"message": "Archive Failed 😭"
},
"getTabFailed":{
"message": "Get Tab Failed 😭"
},
"memoUploading":{
"message": "Sending"
},
"memoSuccess":{
"message": "Success! 😊"
},
"memoFailed":{
"message": "Failed! 😭"
},
"invalidToken":{
"message": "Invalid token or url 😭"
},
"tipOpenSite": {
"message": "Open Memos"
},
"tipSettings": {
"message": "Settings"
},
"tipTags": {
"message": "Insert tag"
},
"tipTodo": {
"message": "Insert todo"
},
"tipUpload": {
"message": "Upload file"
},
"tipLink": {
"message": "Insert current tab link"
},
"tipRandom": {
"message": "Random memo"
},
"tipSearch": {
"message": "Search"
},
"tipVisibility": {
"message": "Visibility"
},
"tipSend": {
"message": "Send (Ctrl/⌘+Enter)"
},
"tipLanguage": {
"message": "Language"
},
"langAuto": {
"message": "Auto"
},
"langEnglish": {
"message": "English"
},
"langChineseSimplified": {
"message": "简体中文"
},
"langJapanese": {
"message": "日本語"
},
"langKorean": {
"message": "한국어"
},
"tipFullscreen": {
"message": "Open fullscreen editor"
},
"tipResize": {
"message": "Drag to resize (min: default size)"
}
}
+188
View File
@@ -0,0 +1,188 @@
{
"extName": {
"message": "Memos"
},
"actionTitle": {
"message": "Memos に送信"
},
"extDescription": {
"message": "memos: 軽量なセルフホスト型メモハブ。"
},
"sendTo": {
"message": "Memos に \"%s\" を送信"
},
"sendLinkTo": {
"message": "リンクを Memos に送信"
},
"sendImageTo": {
"message": "画像を Memos に送信"
},
"saveBtn": {
"message": "保存"
},
"supportedMemosVersion": {
"message": "Memos v0.15.0 - 0.27.x に対応"
},
"settingsConnectionTitle": {
"message": "接続設定"
},
"settingsConnectionDesc": {
"message": "Memos のURLとアクセストークンを設定します。"
},
"settingsPostingTitle": {
"message": "投稿設定"
},
"settingsPostingDesc": {
"message": "添付ファイルのみ送信時の既定テキスト"
},
"placeApiUrl": {
"message": "Memos サイトURL"
},
"placeApiTokens": {
"message": "Memos アクセストークン"
},
"placeContent": {
"message": "今のメモは…"
},
"lockPrivate": {
"message": "非公開"
},
"lockProtected": {
"message": "保護"
},
"lockPublic": {
"message": "公開"
},
"submitBtn": {
"message": "送信"
},
"placeHideInput": {
"message": "既定の「非公開」タグ名"
},
"placeShowInput": {
"message": "既定の「全員に公開」タグ名"
},
"placeAttachmentOnlyDefaultText": {
"message": "添付ファイルのみ送信時の既定テキスト(空欄で内蔵文言を使用)"
},
"uploadedListTitle": {
"message": "アップロード済みファイル(ドラッグで並べ替え)"
},
"uploadedListEmpty": {
"message": "アップロード済みファイルはありません"
},
"tipReorder": {
"message": "ドラッグして並べ替え"
},
"tipDeleteAttachment": {
"message": "削除"
},
"attachmentDeleteSuccess": {
"message": "削除しました"
},
"attachmentDeleteFailed": {
"message": "削除に失敗しました 😭"
},
"picDrag": {
"message": "画像をここにドラッグしてアップロード"
},
"picCancelDrag": {
"message": "アップロードをキャンセル"
},
"picUploading": {
"message": "画像をアップロード中..."
},
"picSuccess": {
"message": "アップロード完了"
},
"picFailed": {
"message": "アップロード失敗"
},
"picPending": {
"message": "画像のアップロードが進行中です"
},
"saveSuccess": {
"message": "保存しました!"
},
"searchNow": {
"message": "何を探していますか?"
},
"searchNone": {
"message": "別のキーワードを試してください!"
},
"archiveSuccess": {
"message": "アーカイブ成功 😊"
},
"archiveFailed": {
"message": "アーカイブ失敗 😭"
},
"getTabFailed": {
"message": "タブの取得に失敗 😭"
},
"memoUploading": {
"message": "送信中"
},
"memoSuccess": {
"message": "成功!😊"
},
"memoFailed": {
"message": "失敗!😭"
},
"invalidToken": {
"message": "無効なトークンまたはURL 😭"
},
"tipOpenSite": {
"message": "Memos を開く"
},
"tipSettings": {
"message": "設定"
},
"tipTags": {
"message": "タグを挿入"
},
"tipTodo": {
"message": "ToDo を挿入"
},
"tipUpload": {
"message": "ファイルをアップロード"
},
"tipLink": {
"message": "現在のタブのリンクを挿入"
},
"tipRandom": {
"message": "ランダムメモ"
},
"tipSearch": {
"message": "検索"
},
"tipVisibility": {
"message": "公開範囲"
},
"tipSend": {
"message": "送信(Ctrl/⌘+Enter"
},
"tipLanguage": {
"message": "言語"
},
"langAuto": {
"message": "自動"
},
"langEnglish": {
"message": "English"
},
"langChineseSimplified": {
"message": "简体中文"
},
"langJapanese": {
"message": "日本語"
},
"langKorean": {
"message": "한국어"
},
"tipFullscreen": {
"message": "全画面で編集"
},
"tipResize": {
"message": "ドラッグで拡大/縮小(最小:初期サイズ)"
}
}
+188
View File
@@ -0,0 +1,188 @@
{
"extName": {
"message": "Memos"
},
"actionTitle": {
"message": "Memos 보내기"
},
"extDescription": {
"message": "memos: 가볍고 셀프호스팅 가능한 메모 허브."
},
"sendTo": {
"message": "Memos로 \"%s\" 보내기"
},
"sendLinkTo": {
"message": "링크를 Memos로 보내기"
},
"sendImageTo": {
"message": "이미지를 Memos로 보내기"
},
"saveBtn": {
"message": "저장"
},
"supportedMemosVersion": {
"message": "Memos v0.15.0 - 0.27.x 호환"
},
"settingsConnectionTitle": {
"message": "연결 설정"
},
"settingsConnectionDesc": {
"message": "Memos 사이트 URL과 액세스 토큰을 설정합니다."
},
"settingsPostingTitle": {
"message": "전송 설정"
},
"settingsPostingDesc": {
"message": "첨부만 전송할 때의 기본 텍스트"
},
"placeApiUrl": {
"message": "Memos 사이트 URL"
},
"placeApiTokens": {
"message": "Memos 액세스 토큰"
},
"placeContent": {
"message": "지금 떠오른 생각은..."
},
"lockPrivate": {
"message": "비공개"
},
"lockProtected": {
"message": "보호됨"
},
"lockPublic": {
"message": "공개"
},
"submitBtn": {
"message": "전송"
},
"placeHideInput": {
"message": "기본 '비공개' 태그 이름"
},
"placeShowInput": {
"message": "기본 '모두 공개' 태그 이름"
},
"placeAttachmentOnlyDefaultText": {
"message": "첨부만 전송할 때의 기본 텍스트(비워두면 내장 문구 사용)"
},
"uploadedListTitle": {
"message": "업로드된 파일(드래그로 순서 변경)"
},
"uploadedListEmpty": {
"message": "업로드된 파일이 없습니다"
},
"tipReorder": {
"message": "드래그하여 순서 변경"
},
"tipDeleteAttachment": {
"message": "삭제"
},
"attachmentDeleteSuccess": {
"message": "삭제됨"
},
"attachmentDeleteFailed": {
"message": "삭제 실패 😭"
},
"picDrag": {
"message": "이미지를 드래그하여 업로드"
},
"picCancelDrag": {
"message": "업로드 취소"
},
"picUploading": {
"message": "이미지 업로드 중..."
},
"picSuccess": {
"message": "업로드 완료"
},
"picFailed": {
"message": "업로드 실패"
},
"picPending": {
"message": "이미지 업로드가 진행 중입니다"
},
"saveSuccess": {
"message": "저장 성공!"
},
"searchNow": {
"message": "무엇을 찾고 있나요?"
},
"searchNone": {
"message": "다른 단어를 시도해 보세요!"
},
"archiveSuccess": {
"message": "보관 성공 😊"
},
"archiveFailed": {
"message": "보관 실패 😭"
},
"getTabFailed": {
"message": "탭 가져오기 실패 😭"
},
"memoUploading": {
"message": "전송 중"
},
"memoSuccess": {
"message": "성공! 😊"
},
"memoFailed": {
"message": "실패! 😭"
},
"invalidToken": {
"message": "유효하지 않은 토큰 또는 URL 😭"
},
"tipOpenSite": {
"message": "Memos 열기"
},
"tipSettings": {
"message": "설정"
},
"tipTags": {
"message": "태그 삽입"
},
"tipTodo": {
"message": "할 일 삽입"
},
"tipUpload": {
"message": "파일 업로드"
},
"tipLink": {
"message": "현재 탭 링크 삽입"
},
"tipRandom": {
"message": "랜덤 메모"
},
"tipSearch": {
"message": "검색"
},
"tipVisibility": {
"message": "공개 범위"
},
"tipSend": {
"message": "전송(Ctrl/⌘+Enter)"
},
"tipLanguage": {
"message": "언어"
},
"langAuto": {
"message": "자동"
},
"langEnglish": {
"message": "English"
},
"langChineseSimplified": {
"message": "简体中文"
},
"langJapanese": {
"message": "日本語"
},
"langKorean": {
"message": "한국어"
},
"tipFullscreen": {
"message": "전체화면 편집"
},
"tipResize": {
"message": "드래그로 확대/축소(최소: 기본 크기)"
}
}
+188
View File
@@ -0,0 +1,188 @@
{
"extName": {
"message": "Memos"
},
"actionTitle": {
"message": "发送 Memos"
},
"extDescription": {
"message": "一键发送灵感时刻,珍藏你的记忆"
},
"sendTo": {
"message": "发送至 Memos “%s”"
},
"sendLinkTo": {
"message": "发送链接至 Memos"
},
"sendImageTo": {
"message": "发送图片至 Memos"
},
"saveBtn":{
"message": "保存"
},
"supportedMemosVersion": {
"message": "兼容 Memos v0.15.0 - 0.27.x"
},
"settingsConnectionTitle": {
"message": "连接设置"
},
"settingsConnectionDesc": {
"message": "配置 Memos 服务地址和访问令牌。"
},
"settingsPostingTitle": {
"message": "发送设置"
},
"settingsPostingDesc": {
"message": "仅发送附件时的默认文本"
},
"placeApiUrl":{
"message": "请填入 Memos 主页网址"
},
"placeApiTokens":{
"message": "请填入 Memos Access Tokens"
},
"placeContent":{
"message": "现在的想法是..."
},
"lockPrivate":{
"message": "私有"
},
"lockProtected":{
"message": "登录可见"
},
"lockPublic":{
"message": "公开"
},
"submitBtn":{
"message": "记下"
},
"placeHideInput":{
"message": "默认“私有”标签名"
},
"placeShowInput":{
"message": "默认“公开”标签名"
},
"placeAttachmentOnlyDefaultText":{
"message": "仅发送附件时的默认文本(留空则使用内置文案)"
},
"picDrag":{
"message": "拖拽到窗口上传该图片"
},
"picCancelDrag":{
"message": "取消上传"
},
"picUploading":{
"message": "图片上传中……"
},
"picSuccess":{
"message": "上传完成"
},
"picFailed":{
"message": "上传图片失败"
},
"picPending":{
"message": "有图片等待上传"
},
"saveSuccess":{
"message": "保存信息成功"
},
"searchNow":{
"message": "想搜点啥?"
},
"searchNone":{
"message": "搜不到,换个词试试"
},
"archiveSuccess":{
"message": "归档成功!😊"
},
"archiveFailed":{
"message": "归档失败 😭"
},
"getTabFailed":{
"message": "获取标签失败 😭"
},
"memoUploading":{
"message": "发送中……"
},
"memoSuccess":{
"message": "发送成功!😊"
},
"memoFailed":{
"message": "发送失败 😭"
},
"invalidToken":{
"message": "无效的 token 或 url 😭"
},
"uploadedListTitle": {
"message": "已上传文件,可拖动排序"
},
"uploadedListEmpty": {
"message": "暂无已上传文件"
},
"tipReorder": {
"message": "拖动排序"
},
"tipDeleteAttachment": {
"message": "删除"
},
"attachmentDeleteSuccess": {
"message": "删除成功"
},
"attachmentDeleteFailed": {
"message": "删除失败 😭"
},
"tipOpenSite": {
"message": "打开 Memos"
},
"tipSettings": {
"message": "设置"
},
"tipTags": {
"message": "插入标签"
},
"tipTodo": {
"message": "插入待办"
},
"tipUpload": {
"message": "上传文件"
},
"tipLink": {
"message": "插入当前页面链接"
},
"tipRandom": {
"message": "随机一条"
},
"tipSearch": {
"message": "搜索"
},
"tipVisibility": {
"message": "可见性"
},
"tipSend": {
"message": "发送(Ctrl/⌘+Enter"
},
"tipLanguage": {
"message": "语言"
},
"langAuto": {
"message": "跟随浏览器"
},
"langEnglish": {
"message": "English"
},
"langChineseSimplified": {
"message": "简体中文"
},
"langJapanese": {
"message": "日本語"
},
"langKorean": {
"message": "한국어"
},
"tipFullscreen": {
"message": "全屏编辑"
},
"tipResize": {
"message": "拖拽缩放编辑框(最小为默认大小)"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

+658
View File
@@ -0,0 +1,658 @@
button, input, textarea {
font-family: inherit;
font-size: 100%;
font-weight: inherit;
line-height: inherit;
color: inherit;
margin: 0;
padding: 0;
border: none;
outline: none;
}
input:focus::-moz-placeholder ,.common-editor-inputer:focus::-moz-placeholder {
color: #d3d3d3
}
input::placeholder ,.common-editor-inputer::placeholder {
color: #999;
}
input:focus::placeholder ,.common-editor-inputer:focus::placeholder {
color: #d3d3d3
}
.body{
min-width:360px;
background-color: #f6f5f4;
padding:0 1rem 1rem;
font-family: eafont,PingFang SC,Hiragino Sans GB,Microsoft YaHei,STHeiti,WenQuanYi Micro Hei,Helvetica,Arial,sans-serif;
font-size: 16px;
font-size: 1rem;
line-height: 1.5;
position: relative;
}
a{color: #555;}
.title{
width: 100px;
cursor: pointer;
font-size: 1.125rem;
font-weight: 700;
line-height: 2.5rem;
color: rgb(55,65,81);
}
.memo-editor,.random-item{
border: 2px solid rgb(229,231,235);
border-radius: .5rem;
background-color: rgb(255,255,255);
margin-top:0.8rem;
padding: 0.6rem;
}
.memo-editor{
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{
border: 1px solid rgb(229,231,235);
color: #666;
padding: 1rem;
}
.random-time{font-size:13px;margin-bottom:6px;color: #999;font-style:italic;}
.random-time span{float: right;margin-top: -3px;cursor: pointer;}
.random-time span svg.icon{width:15px;height:15px;padding:4px 6px 0;opacity: 0.6;margin-left:4px;}
.random-time span svg.icon:hover{opacity: 1;}
.random-image{width:180px;height:180px;max-width:100%;object-fit: cover;border-radius: .25rem;margin:5px 5px 5px 0;}
.random-content{width:100%;
max-width:100%;
font-size: 1rem;
line-height: 1.5rem;
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%;
width: 100%;
resize: none;
overflow-y: auto;
overflow-x: hidden;
white-space: pre-wrap;
background-color: transparent;
font-size: 1rem;
min-height: 40px;
scrollbar-width: none;
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%;}
.settings-panel{
display: flex;
flex-direction: column;
gap: .75rem;
margin-top: .75rem;
}
.settings-section{
border: 1px solid rgb(229,231,235);
border-radius: .5rem;
background-color: rgb(255,255,255);
padding: .75rem;
display: flex;
flex-direction: column;
gap: .6rem;
}
.settings-section-title{
font-size: .9rem;
font-weight: 700;
color: rgb(55,65,81);
}
.settings-section-desc{
font-size: .75rem;
line-height: 1.35;
color: #7a7a7a;
}
.settings-input{
width: 100% !important;
box-sizing: border-box;
border: 1px solid rgb(229,231,235);
border-radius: .35rem;
background-color: #fafafa;
padding: .55rem .7rem;
}
.settings-input:focus{
border-color: rgb(22,163,74);
background-color: rgb(255,255,255);
}
.settings-textarea{
resize: vertical;
min-height: 4.5rem;
white-space: pre-wrap;
}
.settings-actions{
display: flex;
justify-content: flex-end;
}
#saveKey{margin:0;flex:1;}
.common-tools-wrapper {
position: relative;
display: flex;
width: 100%;
flex-direction: row;
align-items: center;
margin-top: 1rem;
justify-content: space-between;
}
.upload-list-wrapper{
margin-top: .5rem;
}
.upload-list-title{
font-size: .875rem;
color: #999;
margin-top: .5rem;
margin-bottom: .25rem;
}
.upload-list{
border: 1px solid rgb(229,231,235);
border-radius: .5rem;
background-color: rgb(255,255,255);
padding: .25rem;
}
.upload-empty{
padding: .5rem .75rem;
font-size: .875rem;
color: #999;
}
.upload-item{
display:flex;
align-items:center;
justify-content: space-between;
padding: .4rem .5rem;
border-radius: .25rem;
color:#666;
}
.upload-item + .upload-item{
border-top: 1px solid rgb(243,244,246);
}
.upload-item.drag-over{
background-color: rgb(243,244,246);
}
.upload-left{
display:flex;
align-items:center;
min-width: 0;
gap: .5rem;
}
.upload-drag{
cursor: grab;
opacity: .6;
user-select: none;
}
.upload-filename{
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: .875rem;
}
.upload-del{
cursor:pointer;
font-size: 1rem;
line-height: 1;
padding: .15rem .35rem;
border-radius: .25rem;
opacity: .6;
background-color: transparent;
}
.upload-del:hover{
opacity: 1;
background-color: rgb(243,244,246);
}
.common-tools-container {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
}
.confirm-btn {
display: inline-flex;
cursor: pointer;
flex-direction: row;
align-items: center;
justify-content: center;
background-color: rgb(22,163,74);
padding:0 1rem;
font-size: .875rem;
line-height: 2rem;
color: rgb(255,255,255);
box-shadow: 0 1px 3px 0
rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);
user-select: none;
border-radius: .25rem;
border-style: none;
opacity: .6;
}
.confirm-btn:hover {
opacity: 1;
}
.confirm-btn:disabled{
opacity: .6;
cursor: not-allowed;
}
.common-tools-container .mr-5{margin-right: .5rem;}
.common-tools-container svg.icon,#blog_info_edit svg.icon{width:24px;height:24px;opacity: 0.6;cursor: pointer;}
#newtodo svg.icon{padding-top: 2px;}
#tags svg.icon{padding: 2px;width:23px;height:23px;}
#random svg.icon{padding:2px;width:19px;height:19px;}
.common-tools-container svg.icon:hover{opacity: 1;}
#locked,#taglist,#visibilitylis,#randomlist{display: none;}
.tag-list,.visibility-list {
margin-top: .5rem;
max-height: 13rem;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
font-family: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;
}
.tag-list>.item-container,.visibility-list >.item-lock,.tag-list .hidetag{
display: inline-block;
background-color: #666;
cursor: pointer;
padding: .2rem .5rem;
border-radius: .25rem;
font-size: .875rem;
line-height: 1.25rem;
color: #fff;
margin:0 6px 6px 0;
}
.tag-list .hidetag{padding:0;float:right;background-color:#ddd;}
.tag-list .hidetag:hover{background-color:#666;}
.tag-hide{display: none;}
.tag-hide input.inputer{width:40%;font-size:11px;}
.visibility-list .item-lock.lock-now{
background-color:rgb(22,163,74);
}
#blog_info_edit{
position: absolute;
right: 1rem;
top: 0.5rem;
}
.lang-switcher{
position: absolute;
right: 3.5rem;
top: .55rem;
}
.lang-toggle{
border: none;
border-radius: 0;
background-color: transparent;
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;
line-height: 1.25rem;
cursor: pointer;
}
.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{
margin-left: 36%;
max-width: 640px;
position: fixed;
text-align: center;
top: 15px;
width: 58%;
z-index: 10001;
left: 50%;
margin-left: -320px;
}
.tip-info{
background: -webkit-gradient(linear,left top,right top,from(#9c51ff),to(#816bff));
background: -webkit-linear-gradient(90deg,#9c51ff,#816bff);
background: linear-gradient(90deg,#9c51ff,#816bff);
-moz-box-shadow: 3px 3px 20px #d7ceff38;
-webkit-box-shadow: 3px 3px 20px #d7ceff38;
box-shadow: 3px 3px 20px #d7ceff38;
color: #fff;
font-size: 12px;
padding: 8px 40px;
display: inline-block;
border-radius: 3px;
margin: 0;
line-height: 1;
font-weight: 300;
}
@-webkit-keyframes bounceIn {
0% {
opacity: 0;
-webkit-transform: scale(.3);
}
50% {
opacity: 1;
-webkit-transform: scale(1);
}
70% {
-webkit-transform: scale(.95);
}
100% {
-webkit-transform: scale(1);
}
}
@keyframes bounceIn {
0% {
opacity: 0;
transform: scale(.3);
}
50% {
opacity: 1;
transform: scale(1);
}
70% {
transform: scale(.95);
}
100% {
transform: scale(1);
}
}
.bounceIn {
-webkit-animation-name: bounceIn;
animation-name: bounceIn;
}
.animate {
-webkit-animation-duration: .3s;
animation-duration: .3s;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
}
.\!hidden{
display: none!important;
}
.selector-wrapper {
position: relative;
display: flex;
height: 2rem;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start
}
.selector-wrapper>.current-value-container {
display: flex;
height: 100%;
width: 100%;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
flex-direction: row;
align-items: center;
justify-content: space-between;
border-radius: .25rem;
border-width: 1px;
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
padding-left: .5rem;
padding-right: .25rem;
}
.selector-wrapper>.current-value-container>.value-text {
margin-right: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: .875rem
}
.selector-wrapper>.current-value-container>.value-text {
width: calc(100% - 20px)
}
.selector-wrapper>.current-value-container>.lock-text {
margin-right: .25rem;
display: flex;
width: 1rem;
flex-shrink: 0;
flex-direction: row;
align-items: center;
justify-content: center
}
.selector-wrapper>.current-value-container>.arrow-text {
display: flex;
width: 1rem;
flex-shrink: 0;
flex-direction: row;
align-items: center;
justify-content: center
}
.selector-wrapper>.current-value-container>.arrow-text>.icon-img {
height: auto;
width: 1rem;
opacity: .4
}
.selector-wrapper>.items-wrapper {
position: absolute;
bottom: 100%;
left: 0px;
z-index: 1;
margin-top: .25rem;
margin-left: -.5rem;
display: flex;
width: auto;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
overflow-y: auto;
border-radius: .375rem;
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
padding: .25rem;
-ms-overflow-style: none;
scrollbar-width: none
}
.selector-wrapper>.items-wrapper::-webkit-scrollbar {
display: none
}
.selector-wrapper>.items-wrapper {
min-width: calc(100% + 16px);
max-height: 256px;
box-shadow: 0 0 8px #0003
}
.selector-wrapper>.items-wrapper>.item-lock {
display: flex;
width: 100%;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
white-space: nowrap;
border-radius: .25rem;
padding-left: .75rem;
padding-right: .75rem;
font-size: .875rem;
line-height: 2rem
}
.selector-wrapper>.items-wrapper>.item-lock:hover {
--tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity))
}
.selector-wrapper>.items-wrapper>.item-lock.selected {
--tw-text-opacity: 1;
color: rgb(22 163 74 / var(--tw-text-opacity))
}
.selector-wrapper>.items-wrapper>.tip-text {
padding: .25rem .75rem;
font-size: .875rem;
--tw-text-opacity: 1;
color: rgb(75 85 99 / var(--tw-text-opacity))
}
.selector-wrapper>.selector-disabled {
cursor: not-allowed;
pointer-events: none;
--tw-bg-opacity: 1;
background-color: rgb(229 231 235 / var(--tw-bg-opacity));
--tw-text-opacity: 1;
color: rgb(156 163 175 / var(--tw-text-opacity))
}
+250
View File
@@ -0,0 +1,250 @@
const UI_LANGUAGE_STORAGE_KEY = 'uiLanguage'
const SUPPORTED_UI_LANGUAGES = new Set(['auto', 'en', 'zh_CN', 'ja', 'ko'])
function normalizeUiLanguage(value) {
const lang = String(value || 'auto')
return SUPPORTED_UI_LANGUAGES.has(lang) ? lang : 'auto'
}
function storageSyncGet(defaults) {
return new Promise((resolve) => {
chrome.storage.sync.get(defaults, (items) => resolve(items || {}))
})
}
function updateContextMenu(id, update) {
return new Promise((resolve) => {
try {
chrome.contextMenus.update(id, update, () => resolve())
} catch (_) {
resolve()
}
})
}
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 pageReadSelectionTextSource() {
return `(${pageReadSelectionText.toString()})()`
}
function getSelectionTextFromTab(tabId, fallbackText) {
return new Promise((resolve) => {
const fallback = typeof fallbackText === 'string' ? fallbackText : ''
if (!tabId) {
resolve(fallback)
return
}
if (chrome.scripting && typeof chrome.scripting.executeScript === 'function') {
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)
}
)
return
} catch (_) {
// Fallback below for Firefox MV2 background pages.
}
}
if (chrome.tabs && typeof chrome.tabs.executeScript === 'function') {
try {
chrome.tabs.executeScript(tabId, { code: pageReadSelectionTextSource() }, (results) => {
if (chrome.runtime.lastError) {
resolve(fallback)
return
}
const text = Array.isArray(results) && typeof results[0] === 'string' ? results[0] : ''
resolve(text || fallback)
})
return
} catch (_) {
// Ignore and fall back to the original selection text below.
}
}
resolve(fallback)
})
}
function getActionApi() {
if (chrome.action && typeof chrome.action.openPopup === 'function') return chrome.action
if (chrome.browserAction && typeof chrome.browserAction.openPopup === 'function') return chrome.browserAction
return null
}
function tryOpenActionPopup(tab) {
try {
const actionApi = getActionApi()
if (!actionApi) return
const windowId = tab && typeof tab.windowId === 'number' ? tab.windowId : undefined
const open = () => {
try {
if (typeof windowId === 'number') {
actionApi.openPopup({ windowId }, () => void chrome.runtime.lastError)
} else {
actionApi.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
async function loadLocaleMessages(locale) {
if (!locale || locale === 'auto') return null
try {
const url = chrome.runtime.getURL(`_locales/${locale}/messages.json`)
const resp = await fetch(url)
if (!resp.ok) return null
return await resp.json()
} catch (_) {
return null
}
}
async function getUiLanguage() {
const items = await storageSyncGet({ [UI_LANGUAGE_STORAGE_KEY]: 'auto' })
return normalizeUiLanguage(items[UI_LANGUAGE_STORAGE_KEY])
}
async function t(key) {
const lang = await getUiLanguage()
if (lang !== cachedUiLanguage) {
cachedUiLanguage = lang
cachedOverrideMessages = await loadLocaleMessages(lang)
}
const msg = cachedOverrideMessages && cachedOverrideMessages[key] && cachedOverrideMessages[key].message
if (typeof msg === 'string' && msg.length > 0) return msg
return chrome.i18n.getMessage(key) || ''
}
async function refreshContextMenus() {
await updateContextMenu('Memos-send-selection', { title: await t('sendTo') })
await updateContextMenu('Memos-send-link', { title: await t('sendLinkTo') })
await updateContextMenu('Memos-send-image', { title: await t('sendImageTo') })
}
chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.create({
type: 'normal',
title: chrome.i18n.getMessage('sendTo'),
id: 'Memos-send-selection',
contexts: ['selection']
})
chrome.contextMenus.create({
type: 'normal',
title: chrome.i18n.getMessage('sendLinkTo'),
id: 'Memos-send-link',
contexts: ['link', 'page']
})
chrome.contextMenus.create({
type: 'normal',
title: chrome.i18n.getMessage('sendImageTo'),
id: 'Memos-send-image',
contexts: ['image']
})
// Apply override titles if user selected a fixed language.
refreshContextMenus()
})
chrome.storage.onChanged.addListener((changes, areaName) => {
if (areaName !== 'sync') return
if (!changes[UI_LANGUAGE_STORAGE_KEY]) return
cachedUiLanguage = null
cachedOverrideMessages = null
refreshContextMenus()
})
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))
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(`![](${info.srcUrl})` + '\n')
}
})
+521
View File
@@ -0,0 +1,521 @@
(function (global) {
'use strict'
const FLAVOR_V020_V021 = 'v020-v021'
const KNOWN_FLAVORS = [FLAVOR_V020_V021, 'v023', 'modern']
function requestJson(options, success, fail) {
global.$
.ajax(options)
.done(function (data) {
if (success) success(data)
})
.fail(function (xhr) {
if (fail) fail(xhr)
})
}
function extractMemos(data) {
if (global.MemosApiModern && typeof global.MemosApiModern.extractMemosListFromResponse === 'function') {
return global.MemosApiModern.extractMemosListFromResponse(data)
}
return []
}
function getFlavor(info) {
if (!info) return 'legacy'
if (info.apiFlavor === 'modern' && global.MemosApiV023) return 'modern'
if (info.apiFlavor === 'v023' && global.MemosApiV023) return 'v023'
if ((info.apiFlavor === FLAVOR_V020_V021 || info.apiFlavor === 'v1') && global.MemosApiV020V021) {
return FLAVOR_V020_V021
}
return 'legacy'
}
function normalizeDetectedFlavor(flavor) {
const value = typeof flavor === 'string' ? flavor : ''
if (value === 'v020' || value === 'v021' || value === 'v1') return FLAVOR_V020_V021
return value
}
function looksLikeMemosListPayload(data) {
if (!data) return false
if (Array.isArray(data)) return true
if (Array.isArray(data.memos)) return true
if (data.data && Array.isArray(data.data.memos)) return true
if (Array.isArray(data.list)) return true
if (typeof data.error === 'string' || typeof data.message === 'string') return false
return false
}
function isNotFoundLikeProbeXhr(xhr) {
const status = xhr && xhr.status
return status === 404 || status === 405
}
function probeFlavor(apiUrl, apiTokens, callback) {
const headers = { Authorization: 'Bearer ' + apiTokens }
const modernQ =
'api/v1/memos?pageSize=1&filter=' +
encodeURIComponent('visibility in ["PUBLIC","PROTECTED"]')
const v023Q =
'api/v1/memos?pageSize=1&filter=' +
encodeURIComponent('visibilities == ["PUBLIC","PROTECTED"]')
const v020V021Q = 'api/v1/memo?limit=1&rowStatus=NORMAL'
function finish(flavor) {
const normalized = normalizeDetectedFlavor(flavor)
if (KNOWN_FLAVORS.indexOf(normalized) !== -1) {
if (callback) callback({ flavor: normalized })
return
}
if (callback) callback({ flavor: 'unknown' })
}
function probeV023() {
global.$
.ajax({
url: apiUrl + v023Q,
method: 'GET',
headers: headers,
dataType: 'json'
})
.done(function (data) {
if (looksLikeMemosListPayload(data)) finish('v023')
else finish('unknown')
})
.fail(function () {
finish('unknown')
})
}
global.$
.ajax({
url: apiUrl + modernQ,
method: 'GET',
headers: headers,
dataType: 'json'
})
.done(function (data) {
if (looksLikeMemosListPayload(data)) {
finish('modern')
return
}
probeV023()
})
.fail(function (xhr) {
if (xhr && xhr.status === 400) {
probeV023()
return
}
if (isNotFoundLikeProbeXhr(xhr)) {
global.$
.ajax({
url: apiUrl + v020V021Q,
method: 'GET',
headers: headers,
dataType: 'json'
})
.done(function (data) {
if (looksLikeMemosListPayload(data)) finish(FLAVOR_V020_V021)
else finish('unknown')
})
.fail(function () {
finish('unknown')
})
return
}
finish('unknown')
})
}
function keepLegacyVisibleMemos(list) {
const items = Array.isArray(list) ? list : []
return items.filter(function (memo) {
if (!memo) return false
const visibility = typeof memo.visibility === 'string' ? memo.visibility.toUpperCase() : ''
if (!visibility) return true
return visibility === 'PUBLIC' || visibility === 'PROTECTED'
})
}
function extractTagsFromGenericMemo(memo) {
if (!memo) return []
if (Array.isArray(memo.tags) && memo.tags.length > 0) return memo.tags
if (Array.isArray(memo.tagList) && memo.tagList.length > 0) return memo.tagList
if (memo.property && Array.isArray(memo.property.tags) && memo.property.tags.length > 0) {
return memo.property.tags
}
return []
}
function collectTags(info, memos) {
const items = Array.isArray(memos) ? memos : []
const out = items.flatMap(function (memo) {
if (!memo) return []
if (getFlavor(info) === 'v023' && global.MemosApiV023 && typeof global.MemosApiV023.extractTagsFromMemo === 'function') {
return global.MemosApiV023.extractTagsFromMemo(memo)
}
return extractTagsFromGenericMemo(memo)
})
return [...new Set(out.filter(Boolean))]
}
function buildUploadVisibility(editorContent, hideTag, showTag, memoLock) {
const content = typeof editorContent === 'string' ? editorContent : ''
const nowTag = content.match(/(#[^\s#]+)/)
let visibility = memoLock || ''
if (nowTag) {
if (nowTag[1] === showTag) visibility = 'PUBLIC'
else if (nowTag[1] === hideTag) visibility = 'PRIVATE'
}
return visibility
}
function buildModernFilter(parts) {
const p = parts || {}
const exprs = []
if (typeof p.contentSearch === 'string' && p.contentSearch.length > 0) {
exprs.push('content.contains(' + JSON.stringify(String(p.contentSearch)) + ')')
}
return exprs.join(' && ')
}
function normalizeUploadedItem(entity, fallbackFilename) {
if (!entity) return null
const inferredId = (function () {
const value = entity.id != null ? entity.id : entity.ID != null ? entity.ID : entity.Id
if (typeof value === 'number' && Number.isFinite(value)) return Math.floor(value)
if (typeof value === 'string' && value.trim() !== '' && !Number.isNaN(Number(value))) {
return Math.floor(Number(value))
}
return null
})()
const name = entity.name || (inferredId != null ? 'resources/' + String(inferredId) : '')
if (!name && inferredId == null) return null
return {
id: inferredId != null ? inferredId : entity.id,
name: name,
filename: entity.filename || fallbackFilename || name,
createTime: entity.createTime || entity.createdTs || entity.createdAt,
type: entity.type
}
}
function unwrapLegacyMemoEntity(data) {
if (!data) return data
if (data.memo) return data.memo
if (data.data && data.data.memo) return data.data.memo
return data
}
function normalizeLegacyResourceIdList(list) {
const items = Array.isArray(list) ? list : []
return items
.map(function (item) {
if (!item) return null
if (typeof item.id === 'number' && Number.isFinite(item.id)) return Math.floor(item.id)
if (typeof item.id === 'string' && item.id.trim() !== '' && !Number.isNaN(Number(item.id))) {
return Math.floor(Number(item.id))
}
const name = typeof item.name === 'string' ? item.name : ''
const tail = name ? name.split('/').pop() : ''
if (tail && !Number.isNaN(Number(tail))) return Math.floor(Number(tail))
return null
})
.filter(function (value) {
return value != null && Number.isFinite(value)
})
}
function resolve(info) {
const flavor = getFlavor(info)
function listTags(success, fail) {
if (flavor === FLAVOR_V020_V021 && global.MemosApiV020V021) {
global.MemosApiV020V021.getTagSuggestion(info, success, fail)
return
}
if (flavor === 'v023' && global.MemosApiV023) {
const filterExpr = global.MemosApiV023.buildFilter({
rowStatus: 'NORMAL',
creator: 'users/' + info.userid
})
global.MemosApiV023.listMemos(
info,
{ pageSize: 1000, filterExpr: filterExpr },
function (data) {
if (success) success(collectTags(info, extractMemos(data)))
},
fail
)
return
}
if (global.MemosApiModern) {
global.MemosApiModern.fetchMemosWithFallback(
info,
'?pageSize=1000',
function (data) {
if (success) success(collectTags(info, extractMemos(data)))
},
fail
)
}
}
function searchMemos(pattern, success, fail) {
const text = String(pattern || '')
const patternLiteral = JSON.stringify(text)
const legacyFilter = '?filter=' + encodeURIComponent('visibility in ["PUBLIC","PROTECTED"] && content.contains(' + patternLiteral + ')')
if (flavor === 'modern' && global.MemosApiV023) {
const filterExpr = buildModernFilter({ contentSearch: text })
global.MemosApiV023.listMemos(info, { pageSize: 1000, filterExpr: filterExpr }, function (data) {
if (success) success(extractMemos(data))
}, fail)
return
}
if (flavor === 'v023' && global.MemosApiV023) {
const filterExpr = global.MemosApiV023.buildFilter({
visibilities: ['PUBLIC', 'PROTECTED'],
contentSearch: text
})
global.MemosApiV023.listMemos(info, { pageSize: 1000, filterExpr: filterExpr }, function (data) {
if (success) success(keepLegacyVisibleMemos(extractMemos(data)))
}, fail)
return
}
if (flavor === FLAVOR_V020_V021 && global.MemosApiV020V021) {
global.MemosApiV020V021.listMemos(info, { limit: 1000, rowStatus: 'NORMAL', contentSearch: text }, function (data) {
if (success) success(keepLegacyVisibleMemos(extractMemos(data)))
}, fail)
return
}
if (global.MemosApiModern) {
global.MemosApiModern.fetchMemosWithFallback(info, legacyFilter, function (data) {
if (success) success(keepLegacyVisibleMemos(extractMemos(data)))
}, fail)
}
}
function listRandomMemos(success, fail) {
if (flavor === 'modern' && global.MemosApiV023) {
const filterExpr = global.MemosApiV023.buildFilter({})
global.MemosApiV023.listMemos(info, { pageSize: 1000, filterExpr: filterExpr }, function (data) {
if (success) success(extractMemos(data))
}, fail)
return
}
if (flavor === 'v023' && global.MemosApiV023) {
const filterExpr = global.MemosApiV023.buildFilter({ visibilities: ['PUBLIC', 'PROTECTED'] })
global.MemosApiV023.listMemos(info, { pageSize: 1000, filterExpr: filterExpr }, function (data) {
if (success) success(keepLegacyVisibleMemos(extractMemos(data)))
}, fail)
return
}
if (flavor === FLAVOR_V020_V021 && global.MemosApiV020V021) {
global.MemosApiV020V021.listMemos(info, { limit: 1000, rowStatus: 'NORMAL' }, function (data) {
if (success) success(keepLegacyVisibleMemos(extractMemos(data)))
}, fail)
return
}
if (global.MemosApiModern) {
const legacyFilter = '?filter=' + encodeURIComponent('visibility in ["PUBLIC","PROTECTED"]')
global.MemosApiModern.fetchMemosWithFallback(info, legacyFilter, function (data) {
if (success) success(keepLegacyVisibleMemos(extractMemos(data)))
}, fail)
}
}
function deleteResource(item, success, fail) {
const name = item && item.name ? item.name : ''
const rid = item && item.id != null ? item.id : ''
const inferredId = (function () {
if (rid != null && String(rid).trim() !== '' && !Number.isNaN(Number(rid))) return Math.floor(Number(rid))
const tail = String(name || '').split('/').pop()
if (tail && !Number.isNaN(Number(tail))) return Math.floor(Number(tail))
return null
})()
if (flavor === FLAVOR_V020_V021 && global.MemosApiV020V021 && typeof global.MemosApiV020V021.deleteResource === 'function' && inferredId != null) {
global.MemosApiV020V021.deleteResource(info, inferredId, success, fail)
return
}
requestJson({
url: info.apiUrl + 'api/v1/' + name,
type: 'DELETE',
headers: { Authorization: 'Bearer ' + info.apiTokens }
}, success, fail)
}
function uploadFile(file, options, success, fail) {
const oldName = String(file && file.name ? file.name : 'upload').split('.')
const fileExt = String(file && file.name ? file.name : '').split('.').pop()
const now = global.dayjs().format('YYYYMMDDHHmmss')
const nextName = oldName[0] + '_' + now + (fileExt ? '.' + fileExt : '')
if (flavor === FLAVOR_V020_V021 && global.MemosApiV020V021) {
global.MemosApiV020V021.uploadResourceBlob(
info,
file,
{ filename: nextName, type: file.type },
function (entity) {
if (success) success(normalizeUploadedItem(entity, nextName))
},
fail
)
return
}
const reader = new FileReader()
reader.onload = function (e) {
const base64String = e && e.target && e.target.result ? String(e.target.result).split(',')[1] : ''
const payload = {
content: base64String,
visibility: buildUploadVisibility(options && options.editorContent, options && options.hideTag, options && options.showTag, options && options.memoLock),
filename: nextName,
type: file.type
}
global.MemosApiModern.uploadAttachmentOrResource(
info,
payload,
function (resp) {
const entity = (resp && resp.resource) || resp
if (success) success(normalizeUploadedItem(entity, nextName))
},
fail
)
}
reader.onerror = fail
reader.readAsDataURL(file)
}
function archiveMemo(memo, success, fail) {
const memoId = memo && memo.id != null ? memo.id : ''
const memoName = memo && memo.name ? memo.name : ''
if (flavor === FLAVOR_V020_V021 && global.MemosApiV020V021 && memoId !== '') {
global.MemosApiV020V021.patchMemo(info, memoId, { rowStatus: 'ARCHIVED' }, success, fail)
return
}
requestJson({
url: info.apiUrl + 'api/v1/' + memoName,
type: 'PATCH',
data: JSON.stringify({ state: 'ARCHIVED' }),
contentType: 'application/json',
dataType: 'json',
headers: { Authorization: 'Bearer ' + info.apiTokens }
}, success, fail)
}
function getMemo(memoRef, success, fail) {
const url = flavor === FLAVOR_V020_V021
? info.apiUrl + 'api/v1/memo/' + memoRef
: info.apiUrl + 'api/v1/' + memoRef
requestJson({
url: url,
type: 'GET',
contentType: 'application/json',
dataType: 'json',
headers: { Authorization: 'Bearer ' + info.apiTokens }
}, function (data) {
if (success) success(flavor === FLAVOR_V020_V021 ? unwrapLegacyMemoEntity(data) : data)
}, fail)
}
function createMemo(params, success, fail) {
const payload = params || {}
if (flavor === FLAVOR_V020_V021 && global.MemosApiV020V021) {
global.MemosApiV020V021.createMemo(
info,
{
content: payload.content,
visibility: payload.visibility,
resourceIdList: normalizeLegacyResourceIdList(payload.resourceIdList)
},
success,
fail
)
return
}
requestJson({
url: info.apiUrl + 'api/v1/memos',
type: 'POST',
data: JSON.stringify({
content: payload.content,
visibility: payload.visibility
}),
contentType: 'application/json',
dataType: 'json',
headers: { Authorization: 'Bearer ' + info.apiTokens }
}, function (data) {
const createdName = data && data.name ? data.name : data && data.memo && data.memo.name ? data.memo.name : ''
const resources = Array.isArray(payload.resourceIdList) ? payload.resourceIdList : []
if (!createdName) {
if (success) success(data)
return
}
if (resources.length === 0) {
getMemo(createdName, success, fail)
return
}
global.MemosApiModern.patchMemoWithAttachmentsOrResources(
info,
createdName,
resources,
function () {
getMemo(createdName, success, fail)
},
function () {
getMemo(createdName, success, fail)
}
)
}, fail)
}
return {
flavor: flavor,
needsAuthenticatedImagePreview: function () {
return flavor === FLAVOR_V020_V021
},
listTags: listTags,
searchMemos: searchMemos,
listRandomMemos: listRandomMemos,
deleteResource: deleteResource,
uploadFile: uploadFile,
archiveMemo: archiveMemo,
getMemo: getMemo,
createMemo: createMemo
}
}
global.MemosApiAdapter = {
FLAVOR_V020_V021: FLAVOR_V020_V021,
KNOWN_FLAVORS: KNOWN_FLAVORS.slice(),
getFlavor: getFlavor,
normalizeDetectedFlavor: normalizeDetectedFlavor,
probeFlavor: probeFlavor,
resolve: resolve
}
})(window)
+512
View File
@@ -0,0 +1,512 @@
(function (global) {
'use strict'
function extractUserIdFromAuthResponse(response) {
if (!response) return null
const user = response.user || response
if (typeof user.id === 'number' && Number.isFinite(user.id)) return user.id
if (typeof user.id === 'string' && user.id.trim() !== '' && !Number.isNaN(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)
if (typeof name === 'string') {
const m = name.match(/\busers\/(\d+)\b/)
if (m) return Number(m[1])
const last = name.split('/').pop()
if (last) {
if (!Number.isNaN(Number(last))) return Number(last)
if (last.trim() !== '') return last.trim()
}
}
return null
}
function extractMemosListFromResponse(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 isNotFoundLikeXhr(jqXhr) {
const status = jqXhr && jqXhr.status
return status === 404 || status === 405
}
function authWithFallback(apiUrl, apiTokens, callback) {
const headers = { Authorization: 'Bearer ' + apiTokens }
// v0.26+: GET auth/me
// older: POST/GET auth/status
const tries = [
{ 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' }
]
function runAt(index) {
if (index >= tries.length) {
callback(null)
return
}
const t = tries[index]
global.$
.ajax({
async: true,
crossDomain: true,
url: apiUrl + t.path,
method: t.method,
headers: headers
})
.done(function (response) {
const userId = extractUserIdFromAuthResponse(response)
if (userId != null) callback({ userId: userId, uiPath: t.uiPath, raw: response })
else runAt(index + 1)
})
.fail(function () {
runAt(index + 1)
})
}
runAt(0)
}
function fetchMemosWithFallback(info, query, success, fail) {
const qs = query || ''
const headers = { Authorization: 'Bearer ' + info.apiTokens }
// v0.24: `GET /api/v1/memos` tends to behave like a public feed (private memos excluded).
// For an authenticated user, `GET /api/v1/users/{id}/memos` is the safe way to retrieve
// the full set (including private), which affects tag extraction.
// Newer versions may not expose the user-scoped endpoint, so we fallback by 404/405.
const urlUserScoped = info.userid
? info.apiUrl + 'api/v1/users/' + encodeURIComponent(String(info.userid)) + '/memos' + qs
: null
const urlGlobal = info.apiUrl + 'api/v1/memos' + qs
const urlPrimary = urlUserScoped || urlGlobal
const urlFallback = urlUserScoped ? urlGlobal : null
global.$
.ajax({
url: urlPrimary,
type: 'GET',
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
success(data)
})
.fail(function (xhr) {
const status = xhr && xhr.status
const canFallback = Boolean(urlFallback) && (isNotFoundLikeXhr(xhr) || status === 400)
if (!canFallback) {
if (fail) fail(xhr)
return
}
global.$
.ajax({
url: urlFallback,
type: 'GET',
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
success(data)
})
.fail(function (xhr2) {
if (fail) fail(xhr2)
})
})
}
function uploadAttachmentOrResource(info, payload, onSuccess, onFail) {
const headers = { Authorization: 'Bearer ' + info.apiTokens }
const urlAttachments = info.apiUrl + 'api/v1/attachments'
const urlResources = info.apiUrl + 'api/v1/resources'
function stripVisibility(p) {
if (!p || typeof p !== 'object') return p
if (!Object.prototype.hasOwnProperty.call(p, 'visibility')) return p
const copy = Object.assign({}, p)
delete copy.visibility
return copy
}
global.$
.ajax({
url: urlAttachments,
data: JSON.stringify(payload),
type: 'POST',
cache: false,
processData: false,
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'attachments')
})
.fail(function (xhr) {
if (xhr && xhr.status === 400) {
global.$
.ajax({
url: urlAttachments,
data: JSON.stringify(stripVisibility(payload)),
type: 'POST',
cache: false,
processData: false,
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'attachments')
})
.fail(function (xhrRetry) {
if (!isNotFoundLikeXhr(xhrRetry)) {
if (onFail) onFail(xhrRetry)
return
}
// fall through to resources below
xhr = xhrRetry
if (!isNotFoundLikeXhr(xhr)) {
if (onFail) onFail(xhr)
return
}
global.$
.ajax({
url: urlResources,
data: JSON.stringify(payload),
type: 'POST',
cache: false,
processData: false,
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'resources')
})
.fail(function (xhr2) {
if (xhr2 && xhr2.status === 400) {
global.$
.ajax({
url: urlResources,
data: JSON.stringify(stripVisibility(payload)),
type: 'POST',
cache: false,
processData: false,
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'resources')
})
.fail(function (xhr3) {
if (onFail) onFail(xhr3)
})
return
}
if (onFail) onFail(xhr2)
})
})
return
}
if (!isNotFoundLikeXhr(xhr)) {
if (onFail) onFail(xhr)
return
}
global.$
.ajax({
url: urlResources,
data: JSON.stringify(payload),
type: 'POST',
cache: false,
processData: false,
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'resources')
})
.fail(function (xhr2) {
if (xhr2 && xhr2.status === 400) {
global.$
.ajax({
url: urlResources,
data: JSON.stringify(stripVisibility(payload)),
type: 'POST',
cache: false,
processData: false,
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'resources')
})
.fail(function (xhr3) {
if (onFail) onFail(xhr3)
})
return
}
if (onFail) onFail(xhr2)
})
})
}
function patchMemoWithAttachmentsOrResources(info, memoName, list, onSuccess, onFail) {
const headers = { Authorization: 'Bearer ' + info.apiTokens }
const url = info.apiUrl + 'api/v1/' + memoName
const items = Array.isArray(list) ? list : []
const hasResourceNames = items.some(function (x) {
return x && typeof x.name === 'string' && x.name.indexOf('resources/') === 0
})
const hasAttachmentNames = items.some(function (x) {
return x && typeof x.name === 'string' && x.name.indexOf('attachments/') === 0
})
function doPatchAttachments() {
const attachments = items
.map(function (x) {
if (!x) return null
const n = x.name
if (!n) return null
if (hasAttachmentNames && typeof n === 'string' && n.indexOf('attachments/') !== 0) return null
return { name: n }
})
.filter(Boolean)
// Prefer the dedicated subresource endpoint when available.
global.$
.ajax({
url: url + '/attachments',
type: 'PATCH',
data: JSON.stringify({ name: memoName, attachments: attachments }),
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'attachments')
})
.fail(function (xhr0) {
// If the endpoint doesn't exist, try UpdateMemo-style patching.
if (isNotFoundLikeXhr(xhr0)) {
// continue
} else if (xhr0 && xhr0.status && xhr0.status !== 400) {
// continue; some gateways may reject body shape here.
}
// Some versions accept a loose patch, others require updateMask.
const attachmentsPayloadLoose = {
name: memoName,
attachments: attachments
}
global.$
.ajax({
url: url,
type: 'PATCH',
data: JSON.stringify(attachmentsPayloadLoose),
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'attachments')
})
.fail(function (xhr) {
// v0.25 requires update mask when updating attachments.
if (!isNotFoundLikeXhr(xhr) && xhr && xhr.status !== 400) {
if (onFail) onFail(xhr)
return
}
// If the server doesn't support attachments at all, fallback to resources flow.
if (isNotFoundLikeXhr(xhr)) {
doPatchResources()
return
}
const attachmentsPayloadV025 = {
name: memoName,
attachments: attachments
}
const updateUrl1 = url + (url.indexOf('?') >= 0 ? '&' : '?') + 'updateMask=attachments'
global.$
.ajax({
url: updateUrl1,
type: 'PATCH',
data: JSON.stringify(attachmentsPayloadV025),
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'attachments')
})
.fail(function (xhr2) {
if (isNotFoundLikeXhr(xhr2)) {
doPatchResources()
return
}
// Some grpc-gateway setups prefer updateMask.paths.
if (xhr2 && xhr2.status === 400) {
const updateUrl2 =
url + (url.indexOf('?') >= 0 ? '&' : '?') + 'updateMask.paths=attachments'
global.$
.ajax({
url: updateUrl2,
type: 'PATCH',
data: JSON.stringify(attachmentsPayloadV025),
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'attachments')
})
.fail(function (xhr3) {
if (isNotFoundLikeXhr(xhr3)) {
doPatchResources()
return
}
if (onFail) onFail(xhr3)
})
return
}
if (onFail) onFail(xhr2)
})
})
})
}
function doPatchResources() {
const resources = items
.map(function (x) {
if (!x) return null
const n = x.name
if (!n) return null
if (hasResourceNames && typeof n === 'string' && n.indexOf('resources/') !== 0) return null
return { name: n }
})
.filter(Boolean)
// Prefer the dedicated subresource endpoint when available.
global.$
.ajax({
url: url + '/resources',
type: 'PATCH',
data: JSON.stringify({ name: memoName, resources: resources }),
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'resources')
})
.fail(function (xhr0) {
if (!isNotFoundLikeXhr(xhr0) && xhr0 && xhr0.status && xhr0.status !== 400) {
// continue; try UpdateMemo flow below.
}
// Try a loose PATCH first (some versions accept this).
const resourcesPayloadLoose = { resources: resources }
global.$
.ajax({
url: url,
type: 'PATCH',
data: JSON.stringify(resourcesPayloadLoose),
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'resources')
})
.fail(function (xhr2) {
// v0.24 expects UpdateMemo with an update mask when modifying resources.
// The gateway commonly accepts `updateMask=resources` as a query param and a
// Memo body containing `name` + `resources`.
if (!isNotFoundLikeXhr(xhr2) && xhr2 && xhr2.status !== 400) {
if (onFail) onFail(xhr2)
return
}
const updateUrl = url + (url.indexOf('?') >= 0 ? '&' : '?') + 'updateMask=resources'
const resourcesPayloadV024 = {
name: memoName,
resources: resources
}
global.$
.ajax({
url: updateUrl,
type: 'PATCH',
data: JSON.stringify(resourcesPayloadV024),
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
onSuccess(data, 'resources')
})
.fail(function (xhr3) {
if (onFail) onFail(xhr3)
})
})
})
}
// If the list clearly contains v0.24-style resource names, go directly to the
// resource linking flow. If it contains attachment names, go attachment flow.
if (hasResourceNames && !hasAttachmentNames) {
doPatchResources()
return
}
if (hasAttachmentNames && !hasResourceNames) {
doPatchAttachments()
return
}
// Default to attachments first, then fallback to resources.
doPatchAttachments()
}
global.MemosApiModern = {
extractUserIdFromAuthResponse: extractUserIdFromAuthResponse,
extractMemosListFromResponse: extractMemosListFromResponse,
isNotFoundLikeXhr: isNotFoundLikeXhr,
authWithFallback: authWithFallback,
fetchMemosWithFallback: fetchMemosWithFallback,
uploadAttachmentOrResource: uploadAttachmentOrResource,
patchMemoWithAttachmentsOrResources: patchMemoWithAttachmentsOrResources
}
})(window)
+286
View File
@@ -0,0 +1,286 @@
(function (global) {
'use strict'
function isNotFoundLikeXhr(jqXhr) {
const status = jqXhr && jqXhr.status
return status === 404 || status === 405
}
function extractMemoListFromResponse(data) {
if (!data) return []
if (Array.isArray(data)) return data
if (Array.isArray(data.memos)) return data.memos
if (data.data && Array.isArray(data.data.memos)) return data.data.memos
if (Array.isArray(data.list)) return data.list
return []
}
function extractMemoEntityFromResponse(data) {
if (!data) return data
if (data.memo) return data.memo
if (data.data && data.data.memo) return data.data.memo
if (data.data && (data.data.id != null || data.data.name || data.data.content)) return data.data
return data
}
function extractResourceEntityFromResponse(data) {
if (!data) return data
if (data.resource) return data.resource
if (data.data && data.data.resource) return data.data.resource
if (data.data && (data.data.id != null || data.data.name || data.data.filename)) return data.data
return data
}
function requestGet(url, headers, success, fail) {
global.$
.ajax({
url: url,
type: 'GET',
contentType: 'application/json',
dataType: 'json',
headers: headers
})
.done(function (data) {
if (success) success(data)
})
.fail(function (xhr) {
if (fail) fail(xhr)
})
}
function requestPostJson(url, headers, body, success, fail) {
global.$
.ajax({
url: url,
type: 'POST',
contentType: 'application/json',
dataType: 'json',
data: body != null ? JSON.stringify(body) : null,
headers: headers
})
.done(function (data) {
if (success) success(data)
})
.fail(function (xhr) {
if (fail) fail(xhr)
})
}
function requestPatchJson(url, headers, body, success, fail) {
global.$
.ajax({
url: url,
type: 'PATCH',
contentType: 'application/json',
dataType: 'json',
data: body != null ? JSON.stringify(body) : null,
headers: headers
})
.done(function (data) {
if (success) success(data)
})
.fail(function (xhr) {
if (fail) fail(xhr)
})
}
// v1 memo list: GET /api/v1/memo
// Query params (v0.20/v0.21): limit/offset/rowStatus/content/tag (best-effort)
function listMemos(info, options, success, fail) {
const opt = options || {}
const headers = { Authorization: 'Bearer ' + info.apiTokens }
const limit = opt.limit && Number.isFinite(opt.limit) ? Math.max(1, Math.floor(opt.limit)) : 1000
const offset = opt.offset && Number.isFinite(opt.offset) ? Math.max(0, Math.floor(opt.offset)) : null
const rowStatus = typeof opt.rowStatus === 'string' && opt.rowStatus ? opt.rowStatus : 'NORMAL'
const content = typeof opt.contentSearch === 'string' ? opt.contentSearch : ''
const tag = typeof opt.tagSearch === 'string' ? opt.tagSearch : ''
let qs = '?limit=' + encodeURIComponent(String(limit))
if (offset != null) qs += '&offset=' + encodeURIComponent(String(offset))
if (rowStatus) qs += '&rowStatus=' + encodeURIComponent(String(rowStatus))
if (content) qs += '&content=' + encodeURIComponent(String(content))
if (tag) qs += '&tag=' + encodeURIComponent(String(tag).replace(/^#/, ''))
requestGet(
info.apiUrl + 'api/v1/memo' + qs,
headers,
function (data) {
if (success) success({ memos: extractMemoListFromResponse(data) })
},
function (xhr) {
// Some builds might expose plural `/api/v1/memos`; try as a last resort (still v1).
if (isNotFoundLikeXhr(xhr)) {
requestGet(
info.apiUrl + 'api/v1/memos' + qs,
headers,
function (data2) {
if (success) success({ memos: extractMemoListFromResponse(data2) })
},
fail
)
return
}
if (fail) fail(xhr)
}
)
}
function createMemo(info, body, success, fail) {
const headers = { Authorization: 'Bearer ' + info.apiTokens }
requestPostJson(
info.apiUrl + 'api/v1/memo',
headers,
body,
function (data) {
if (success) success(extractMemoEntityFromResponse(data))
},
function (xhr) {
// Last resort: plural route.
if (isNotFoundLikeXhr(xhr)) {
requestPostJson(
info.apiUrl + 'api/v1/memos',
headers,
body,
function (data2) {
if (success) success(extractMemoEntityFromResponse(data2))
},
fail
)
return
}
if (fail) fail(xhr)
}
)
}
function patchMemo(info, memoId, patch, success, fail) {
const headers = { Authorization: 'Bearer ' + info.apiTokens }
const id = memoId != null ? String(memoId) : ''
if (!id) {
if (fail) fail({ status: 400 })
return
}
requestPatchJson(
info.apiUrl + 'api/v1/memo/' + encodeURIComponent(id),
headers,
patch,
function (data) {
if (success) success(extractMemoEntityFromResponse(data))
},
fail
)
}
function getTagList(info, success, fail) {
const headers = { Authorization: 'Bearer ' + info.apiTokens }
requestGet(
info.apiUrl + 'api/v1/tag',
headers,
function (data) {
const list = Array.isArray(data) ? data : Array.isArray(data.tags) ? data.tags : []
const out = list
.map(function (t) {
if (!t) return ''
if (typeof t === 'string') return t
if (typeof t.name === 'string') return t.name
if (typeof t.tag === 'string') return t.tag
return ''
})
.map(function (s) {
return String(s).replace(/^#/, '').trim()
})
.filter(Boolean)
if (success) success(out)
},
fail
)
}
function getTagSuggestion(info, success, fail) {
const headers = { Authorization: 'Bearer ' + info.apiTokens }
requestGet(
info.apiUrl + 'api/v1/tag/suggestion',
headers,
function (data) {
const list = Array.isArray(data) ? data : []
const out = list
.map(function (s) {
return String(s).replace(/^#/, '').trim()
})
.filter(Boolean)
if (success) success(out)
},
function (xhr) {
// Some forks might only expose list.
if (isNotFoundLikeXhr(xhr)) {
getTagList(info, success, fail)
return
}
if (fail) fail(xhr)
}
)
}
function uploadResourceBlob(info, file, meta, success, fail) {
const headers = { Authorization: 'Bearer ' + info.apiTokens }
const url = info.apiUrl + 'api/v1/resource/blob'
const m = meta || {}
const filename = String(m.filename || (file && file.name) || 'upload')
const form = new FormData()
if (file) form.append('file', file, filename)
global.$
.ajax({
url: url,
type: 'POST',
data: form,
processData: false,
contentType: false,
dataType: 'json',
headers: headers
})
.done(function (data) {
if (success) success(extractResourceEntityFromResponse(data))
})
.fail(function (xhr) {
if (fail) fail(xhr)
})
}
function deleteResource(info, resourceId, success, fail) {
const headers = { Authorization: 'Bearer ' + info.apiTokens }
const id = resourceId != null ? String(resourceId) : ''
if (!id) {
if (fail) fail({ status: 400 })
return
}
global.$
.ajax({
url: info.apiUrl + 'api/v1/resource/' + encodeURIComponent(id),
type: 'DELETE',
headers: headers
})
.done(function (data) {
if (success) success(data)
})
.fail(function (xhr) {
if (fail) fail(xhr)
})
}
global.MemosApiV020V021 = {
listMemos: listMemos,
createMemo: createMemo,
patchMemo: patchMemo,
getTagList: getTagList,
getTagSuggestion: getTagSuggestion,
uploadResourceBlob: uploadResourceBlob,
deleteResource: deleteResource
}
})(window)
+119
View File
@@ -0,0 +1,119 @@
(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)
})
}
global.MemosApiV023 = {
buildFilter: buildFilter,
listMemos: listMemos,
extractTagsFromMemo: extractTagsFromMemo
}
})(window)
+1
View File
File diff suppressed because one or more lines are too long
+210
View File
@@ -0,0 +1,210 @@
const UI_LANGUAGE_STORAGE_KEY = 'uiLanguage'
const SUPPORTED_UI_LANGUAGES = new Set(['auto', 'en', 'zh_CN', 'ja', 'ko'])
function normalizeUiLanguage(value) {
const lang = String(value || 'auto')
return SUPPORTED_UI_LANGUAGES.has(lang) ? lang : 'auto'
}
function storageSyncGet(defaults) {
return new Promise((resolve) => {
chrome.storage.sync.get(defaults, (items) => resolve(items || {}))
})
}
function storageSyncSet(items) {
return new Promise((resolve) => {
chrome.storage.sync.set(items, () => resolve())
})
}
async function loadLocaleMessages(locale) {
if (!locale || locale === 'auto') return null
try {
const url = chrome.runtime.getURL(`_locales/${locale}/messages.json`)
const resp = await fetch(url)
if (!resp.ok) return null
return await resp.json()
} catch (_) {
return null
}
}
function formatSubstitutions(message, substitutions) {
if (!message) return ''
if (substitutions == null) return message
const subs = Array.isArray(substitutions) ? substitutions : [substitutions]
let out = message
for (let i = 0; i < subs.length; i++) {
const v = String(subs[i])
out = out.replaceAll(`$${i + 1}`, v)
out = out.replace('%s', v)
}
return out
}
let currentUiLanguage = 'auto'
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) {
const msg = overrideMessages && overrideMessages[key] && overrideMessages[key].message
if (typeof msg === 'string' && msg.length > 0) {
return formatSubstitutions(msg, substitutions)
}
const chromeMsg = chrome.i18n.getMessage(key, substitutions) || ''
return formatSubstitutions(chromeMsg, substitutions)
}
function setText(id, messageKey) {
const el = document.getElementById(id)
if (el) el.textContent = t(messageKey)
}
function setPlaceholder(id, messageKey) {
const el = document.getElementById(id)
if (el) el.placeholder = t(messageKey)
}
function setTitle(id, messageKey) {
const el = document.getElementById(id)
if (el) el.title = t(messageKey)
}
function applyStaticI18n() {
setText('saveSettings', 'saveBtn')
setText('saveTag', 'saveBtn')
setText('supportedMemosVersion', 'supportedMemosVersion')
setText('settingsConnectionTitle', 'settingsConnectionTitle')
setText('settingsConnectionDesc', 'settingsConnectionDesc')
setText('settingsPostingTitle', 'settingsPostingTitle')
setText('settingsPostingDesc', 'settingsPostingDesc')
setPlaceholder('apiUrl', 'placeApiUrl')
setPlaceholder('apiTokens', 'placeApiTokens')
setPlaceholder('content', 'placeContent')
setText('lockPrivate', 'lockPrivate')
setText('lockProtected', 'lockProtected')
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')
setPlaceholder('attachmentOnlyDefaultText', 'placeAttachmentOnlyDefaultText')
setText('uploadlist-title', 'uploadedListTitle')
// Language switcher
setText('langOptionAuto', 'langAuto')
setText('langOptionEn', 'langEnglish')
setText('langOptionZhCN', 'langChineseSimplified')
setText('langOptionJa', 'langJapanese')
setText('langOptionKo', 'langKorean')
setTitle('langToggle', 'tipLanguage')
const langToggle = document.getElementById('langToggle')
if (langToggle) langToggle.setAttribute('aria-label', t('tipLanguage'))
// Native hover tooltips (title)
setTitle('opensite', 'tipOpenSite')
setTitle('blog_info_edit', 'tipSettings')
setTitle('tags', 'tipTags')
setTitle('newtodo', 'tipTodo')
setTitle('upres', 'tipUpload')
setTitle('getlink', 'tipLink')
setTitle('random', 'tipRandom')
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 } = {}) {
const lang = normalizeUiLanguage(nextLang)
currentUiLanguage = lang
overrideMessages = await loadLocaleMessages(lang)
applyStaticI18n()
syncLanguageToggleText(lang)
syncLanguageMenuState(lang)
if (persist) await storageSyncSet({ [UI_LANGUAGE_STORAGE_KEY]: lang })
window.dispatchEvent(new CustomEvent('i18n:changed', { detail: { lang } }))
}
async function initLanguageSwitcher() {
const switcher = document.getElementById('lang_switcher')
const toggle = document.getElementById('langToggle')
const langItems = document.querySelectorAll('.lang-menu-item')
if (toggle) {
toggle.addEventListener('click', (event) => {
event.stopPropagation()
const isOpen = toggle.getAttribute('aria-expanded') === 'true'
setLanguageMenuOpen(!isOpen)
})
}
langItems.forEach((item) => {
item.addEventListener('click', async (event) => {
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 })
setLanguageMenuOpen(false)
}
window.t = t
window.setUiLanguage = setUiLanguage
window.getUiLanguage = () => currentUiLanguage
applyStaticI18n()
window.i18nReady = initLanguageSwitcher()
+1
View File
@@ -0,0 +1 @@
!function(e,_){"object"==typeof exports&&"undefined"!=typeof module?module.exports=_(require("dayjs")):"function"==typeof define&&define.amd?define(["dayjs"],_):(e="undefined"!=typeof globalThis?globalThis:e||self).dayjs_locale_ja=_(e.dayjs)}(this,(function(e){"use strict";function _(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var t=_(e),a={name:"ja",relativeTime:{future:"%s後",past:"%s前",s:"数秒",m:"1分",mm:"%d分",h:"1時間",hh:"%d時間",d:"1日",dd:"%d日",M:"1か月",MM:"%dか月",y:"1年",yy:"%d年"}};return t.default.locale(a,null,!0),a}));
+2
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
!function(e,_){"object"==typeof exports&&"undefined"!=typeof module?module.exports=_(require("dayjs")):"function"==typeof define&&define.amd?define(["dayjs"],_):(e="undefined"!=typeof globalThis?globalThis:e||self).dayjs_locale_ko=_(e.dayjs)}(this,(function(e){"use strict";function _(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var t=_(e),a={name:"ko",relativeTime:{future:"%s 후",past:"%s 전",s:"몇 초",m:"1분",mm:"%d분",h:"1시간",hh:"%d시간",d:"하루",dd:"%d일",M:"1개월",MM:"%d개월",y:"1년",yy:"%d년"}};return t.default.locale(a,null,!0),a}));
+65
View File
@@ -0,0 +1,65 @@
/**
* 消息提示组件
*
* 1.调用
* 字符串类型参数 $.message('成功');
* 对象型参数$.message({});
*
* 2.参数详解
* message:' 操作成功', //提示信息
time:'2000', //显示时间(默认:2s
type:'success', //显示类型,包括4种:success.error,info,warning
showClose:false, //显示关闭按钮(默认:否)
autoClose:true, //是否自动关闭(默认:是)
*
* type:success,error,info,warning
*/
$.extend({
message: function(options) {
var defaults={
message:' 操作成功',
time:'2000',
autoClose: true,
onClose:function(){}
};
if(typeof options === 'string'){
defaults.message=options;
}
if(typeof options === 'object'){
defaults=$.extend({},defaults,options);
}
//message模版
var template='<div class="tip animate bounceIn">\n' +
' <p class="tip-info">'+defaults.message+'</p>\n' +
'</div>';
var _this=this;
var $body=$('body');
var $message=$(template);
var timer;
//移除所有并插入该消息
$('.tip').remove();
$body.append($message);
//居中
$message.css({
'margin-left':'-'+$message.width()/2+'px'
});
//自动关闭
if (defaults.autoClose){
timer=setTimeout(function(){
closeFn();
},defaults.time);
}
//关闭
var closeFn = function(){
$message.addClass('hide');
$message.remove();
defaults.onClose(defaults);
clearTimeout(timer);
};
}
});
+1259
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
!function(r,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(r="undefined"!=typeof globalThis?globalThis:r||self).dayjs_plugin_relativeTime=e()}(this,(function(){"use strict";return function(r,e,t){r=r||{};var n=e.prototype,o={future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"};function i(r,e,t,o){return n.fromToBase(r,e,t,o)}t.en.relativeTime=o,n.fromToBase=function(e,n,i,d,u){for(var f,a,s,l=i.$locale().relativeTime||o,h=r.thresholds||[{l:"s",r:44,d:"second"},{l:"m",r:89},{l:"mm",r:44,d:"minute"},{l:"h",r:89},{l:"hh",r:21,d:"hour"},{l:"d",r:35},{l:"dd",r:25,d:"day"},{l:"M",r:45},{l:"MM",r:10,d:"month"},{l:"y",r:17},{l:"yy",d:"year"}],m=h.length,c=0;c<m;c+=1){var y=h[c];y.d&&(f=d?t(e).diff(i,y.d,!0):i.diff(e,y.d,!0));var p=(r.rounding||Math.round)(Math.abs(f));if(s=f>0,p<=y.r||!y.r){p<=1&&c>0&&(y=h[c-1]);var v=l[y.l];u&&(p=u(""+p)),a="string"==typeof v?v.replace("%d",p):v(p,n,y.l,s);break}}if(n)return a;var M=s?l.future:l.past;return"function"==typeof M?M(a):M.replace("%s",a)},n.to=function(r,e){return i(r,e,this,!0)},n.from=function(r,e){return i(r,e,this)};var d=function(r){return r.$u?t.utc():t()};n.toNow=function(r){return this.to(d(this),r)},n.fromNow=function(r){return this.from(d(this),r)}}}));
+12
View File
@@ -0,0 +1,12 @@
/**
* ViewImage.min.js 2.0.2
* MIT License - http://www.opensource.org/licenses/mit-license.php
* https://tokinx.github.io/ViewImage/
*/
var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.createTemplateTagFirstArg=function(b){return b.raw=b};$jscomp.createTemplateTagFirstArgWithRaw=function(b,a){b.raw=a;return b};$jscomp.arrayIteratorImpl=function(b){var a=0;return function(){return a<b.length?{done:!1,value:b[a++]}:{done:!0}}};$jscomp.arrayIterator=function(b){return{next:$jscomp.arrayIteratorImpl(b)}};$jscomp.makeIterator=function(b){var a="undefined"!=typeof Symbol&&Symbol.iterator&&b[Symbol.iterator];return a?a.call(b):$jscomp.arrayIterator(b)};
$jscomp.arrayFromIterator=function(b){for(var a,d=[];!(a=b.next()).done;)d.push(a.value);return d};$jscomp.arrayFromIterable=function(b){return b instanceof Array?b:$jscomp.arrayFromIterator($jscomp.makeIterator(b))};
(function(){window.ViewImage=new function(){var b=this;this.target="[view-image] img";this.listener=function(a){if(!(a.ctrlKey||a.metaKey||a.shiftKey||a.altKey)){var d=String(b.target.split(",").map(function(g){return g.trim()+":not([no-view])"})),c=a.target.closest(d);if(c){var e=c.closest("[view-image]")||document.body;d=[].concat($jscomp.arrayFromIterable(e.querySelectorAll(d))).map(function(g){return g.href||g.src});b.display(d,c.href||c.src);a.stopPropagation();a.preventDefault()}}};this.init=
function(a){a&&(b.target=a);["removeEventListener","addEventListener"].forEach(function(d){document[d]("click",b.listener,!1)})};this.display=function(a,d){var c=a.indexOf(d),e=(new DOMParser).parseFromString('\n <div class="view-image">\n <style>.view-image{position:fixed;inset:0;z-index:500;padding:1rem;display:flex;flex-direction:column;animation:view-image-in 300ms;backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px)}.view-image__out{animation:view-image-out 300ms}@keyframes view-image-in{0%{opacity:0}}@keyframes view-image-out{100%{opacity:0}}.view-image-btn{width:32px;height:32px;display:flex;justify-content:center;align-items:center;cursor:pointer;border-radius:3px;background-color:rgba(255,255,255,0.2)}.view-image-btn:hover{background-color:rgba(255,255,255,0.5)}.view-image-close__full{position:absolute;inset:0;background-color:rgba(48,55,66,0.3);z-index:unset;cursor:zoom-out;margin:0}.view-image-container{height:0;flex:1;display:flex;align-items:center;justify-content:center;}.view-image-lead{display:contents}.view-image-lead img{position:relative;z-index:1;max-width:100%;max-height:100%;object-fit:contain;border-radius:3px}.view-image-lead__in img{animation:view-image-lead-in 300ms}.view-image-lead__out img{animation:view-image-lead-out 300ms forwards}@keyframes view-image-lead-in{0%{opacity:0;transform:translateY(-20px)}}@keyframes view-image-lead-out{100%{opacity:0;transform:translateY(20px)}}[class*=__out] ~ .view-image-loading{display:block}.view-image-loading{position:absolute;inset:50%;width:8rem;height:2rem;color:#aab2bd;overflow:hidden;text-align:center;margin:-1rem -4rem;z-index:1;display:none}.view-image-loading::after{content:"";position:absolute;inset:50% 0;width:100%;height:3px;background:rgba(255,255,255,0.5);transform:translateX(-100%) translateY(-50%);animation:view-image-loading 800ms -100ms ease-in-out infinite}@keyframes view-image-loading{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}}.view-image-tools{position:relative;display:flex;justify-content:space-between;align-content:center;color:#fff;max-width:600px;position: absolute; bottom: 5%; left: 1rem; right: 1rem; backdrop-filter: blur(10px);margin:0 auto;padding:10px;border-radius:5px;background:rgba(0,0,0,0.1);margin-bottom:constant(safe-area-inset-bottom);margin-bottom:env(safe-area-inset-bottom);z-index:1}.view-image-tools__count{width:60px;display:flex;align-items:center;justify-content:center}.view-image-tools__flip{display:flex;gap:10px}.view-image-tools [class*=-close]{margin:0 10px}</style>\n <div class="view-image-container">\n <div class="view-image-lead"></div>\n <div class="view-image-loading"></div>\n <div class="view-image-close view-image-close__full"></div>\n </div>\n <div class="view-image-tools">\n <div class="view-image-tools__count">\n <span><b class="view-image-index">'+
(c+1)+"</b>/"+a.length+'</span>\n </div>\n <div class="view-image-tools__flip">\n <div class="view-image-btn view-image-tools__flip-prev">\n <svg width="20" height="20" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="48" height="48" fill="white" fill-opacity="0.01"/><path d="M31 36L19 24L31 12" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/></svg>\n </div>\n <div class="view-image-btn view-image-tools__flip-next">\n <svg width="20" height="20" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="48" height="48" fill="white" fill-opacity="0.01"/><path d="M19 12L31 24L19 36" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/></svg>\n </div>\n </div>\n <div class="view-image-btn view-image-close">\n <svg width="16" height="16" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"><rect width="48" height="48" fill="white" fill-opacity="0.01"/><path d="M8 8L40 40" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/><path d="M8 40L40 8" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/></svg>\n </div>\n </div>\n </div>\n ',
"text/html").body.firstChild,g=function(f){var h={Escape:"close",ArrowLeft:"tools__flip-prev",ArrowRight:"tools__flip-next"};h[f.key]&&e.querySelector(".view-image-"+h[f.key]).click()},l=function(f){var h=new Image,k=e.querySelector(".view-image-lead");k.className="view-image-lead view-image-lead__out";setTimeout(function(){k.innerHTML="";h.onload=function(){setTimeout(function(){k.innerHTML='<img src="'+h.src+'" alt="ViewImage" no-view/>';k.className="view-image-lead view-image-lead__in"},100)};
h.src=f},300)};document.body.appendChild(e);l(d);window.addEventListener("keydown",g);e.onclick=function(f){f.target.closest(".view-image-close")?(window.removeEventListener("keydown",g),e.onclick=null,e.classList.add("view-image__out"),setTimeout(function(){return e.remove()},290)):f.target.closest(".view-image-tools__flip")&&(c=f.target.closest(".view-image-tools__flip-prev")?0===c?a.length-1:c-1:c===a.length-1?0:c+1,l(a[c]),e.querySelector(".view-image-index").innerHTML=c+1)}}}})();
+1
View File
@@ -0,0 +1 @@
!function(e,_){"object"==typeof exports&&"undefined"!=typeof module?module.exports=_(require("dayjs")):"function"==typeof define&&define.amd?define(["dayjs"],_):(e="undefined"!=typeof globalThis?globalThis:e||self).dayjs_locale_zh_cn=_(e.dayjs)}(this,(function(e){"use strict";function _(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var t=_(e),d={name:"zh-cn",weekdays:"星期日_星期一_星期二_星期三_星期四_星期五_星期六".split("_"),weekdaysShort:"周日_周一_周二_周三_周四_周五_周六".split("_"),weekdaysMin:"日_一_二_三_四_五_六".split("_"),months:"一月_二月_三月_四月_五月_六月_七月_八月_九月_十月_十一月_十二月".split("_"),monthsShort:"1月_2月_3月_4月_5月_6月_7月_8月_9月_10月_11月_12月".split("_"),ordinal:function(e,_){return"W"===_?e+"周":e+"日"},weekStart:1,yearStart:4,formats:{LT:"HH:mm",LTS:"HH:mm:ss",L:"YYYY/MM/DD",LL:"YYYY年M月D日",LLL:"YYYY年M月D日Ah点mm分",LLLL:"YYYY年M月D日ddddAh点mm分",l:"YYYY/M/D",ll:"YYYY年M月D日",lll:"YYYY年M月D日 HH:mm",llll:"YYYY年M月D日dddd HH:mm"},relativeTime:{future:"%s内",past:"%s前",s:"几秒",m:"1 分钟",mm:"%d 分钟",h:"1 小时",hh:"%d 小时",d:"1 天",dd:"%d 天",M:"1 个月",MM:"%d 个月",y:"1 年",yy:"%d 年"},meridiem:function(e,_){var t=100*e+_;return t<600?"凌晨":t<900?"早上":t<1100?"上午":t<1300?"中午":t<1800?"下午":"晚上"}};return t.default.locale(d,null,!0),d}));
+43
View File
@@ -0,0 +1,43 @@
{
"manifest_version": 2,
"name": "__MSG_extName__",
"default_locale": "en",
"version": "2026.4.23",
"browser_action": {
"default_popup": "popup.html",
"default_icon": "assets/logo_24x24.png",
"default_title": "__MSG_actionTitle__"
},
"description": "__MSG_extDescription__",
"homepage_url": "https://github.com/Jonnyan404/memos-bber",
"browser_specific_settings": {
"gecko": {
"id": "memos-bber@jonnyan404.github.io"
}
},
"icons": {
"128": "assets/logo.png",
"16": "assets/logo.png",
"48": "assets/logo.png"
},
"background": {
"scripts": ["js/background.js"]
},
"permissions": [
"tabs",
"storage",
"activeTab",
"contextMenus",
"http://*/*",
"https://*/*"
],
"commands": {
"open-extension": {
"description": "Open my extension",
"suggested_key": {
"default": "Ctrl+Shift+F",
"mac": "MacCtrl+Shift+F"
}
}
}
}
+194
View File
@@ -0,0 +1,194 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>MEMOS</title>
<link rel="stylesheet" href="../css/main.css" />
</head>
<body class="body">
<div class="title" id="opensite">MEMOS</div>
<div id="lang_switcher" class="lang-switcher">
<button
id="langToggle"
class="lang-toggle"
type="button"
aria-haspopup="true"
aria-expanded="false"
>
<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 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="M510 366a146 146 0 1 0 1 292 146 146 0 0 0-1-292m87 146a87 87 0 1 1-173-1 87 87 0 0 1 173 1"/>
</svg></div>
<div id="blog_info" class="settings-panel">
<div class="settings-section">
<div id="settingsConnectionTitle" class="settings-section-title"></div>
<div class="settings-section-desc" id="settingsConnectionDesc"></div>
<input
id="apiUrl"
class="inputer settings-input"
name="apiUrl"
type="text"
value=""
maxlength="245"
placeholder=""
required
/>
<input
id="apiTokens"
class="inputer settings-input"
name="apiTokens"
type="text"
value=""
maxlength="245"
placeholder=""
required
/>
<div id="supportedMemosVersion" class="upload-list-title"></div>
</div>
<div class="settings-section">
<div id="settingsPostingTitle" class="settings-section-title"></div>
<div class="settings-section-desc" id="settingsPostingDesc"></div>
<textarea
id="attachmentOnlyDefaultText"
class="inputer settings-input settings-textarea"
name="attachmentOnlyDefaultText"
rows="2"
maxlength="500"
placeholder=""
></textarea>
</div>
<div class="settings-actions">
<span id="saveSettings" class="action-btn confirm-btn"></span>
</div>
</div>
<div class="memo-editor">
<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"
name="text"
id="content"
placeholder=""
required=""
></textarea>
<div id="editor-resize-handle" aria-label="Resize"></div>
</div>
<div class="common-tools-wrapper">
<div class="common-tools-container">
<div id="tags" class="mr-5">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024">
<path fill="#666" d="M171 341h682q43 0 43 43t-43 43H171q-43 0-43-43t43-43Z"/>
<path fill="#666" d="M423 85h4a39 39 0 0 1 38 43l-77 772a43 43 0 0 1-43 39h-4a39 39 0 0 1-38-43l77-772a43 43 0 0 1 43-39zm256 0h4a39 39 0 0 1 38 43l-77 772a43 43 0 0 1-43 39h-4a39 39 0 0 1-38-43l77-772a43 43 0 0 1 43-39z"/>
<path fill="#666" d="M171 597h682q43 0 43 43t-43 43H171q-43 0-43-43t43-43Z"/>
</svg>
</div>
<div id="newtodo" class="mr-5">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024">
<path fill="#666" d="M407 365a41 41 0 0 0-59 0 41 41 0 0 0 0 60l149 149c9 8 19 13 30 13s21-5 30-13l341-341c17-18 17-43 0-60s-43-17-60 0L527 484 407 365z"/>
<path fill="#666" d="M868 416c-23 0-45 19-45 45v277c0 2 0 7-2 9 0 2-2 4-4 6s-4 4-6 4l-9 2H247c-2 0-6 0-8-2-2 0-4-2-6-4-3-2-5-4-5-6l-2-9V183l2-8c0-2 2-4 5-6 2-3 4-5 6-5l8-2h278c23 0 45-19 45-45s-20-44-45-44H247c-14 0-27 2-42 8a144 144 0 0 0-55 60c-7 13-9 28-9 42v555c0 15 2 28 8 43a122 122 0 0 0 58 58c13 6 28 8 43 8h554a108 108 0 0 0 77-32c11-11 17-21 24-34 6-13 8-28 8-43V461c-2-26-21-45-45-45z"/>
</svg>
</div>
<div id="upres" class="mr-5">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024">
<path fill="#555" d="M752 80H272c-70 0-128 58-128 128v608c0 70 58 128 128 128h354c33 0 65-13 91-37l126-126c24-24 37-56 37-91V208c0-70-58-128-128-128zM208 816V208c0-35 29-64 64-64h480c35 0 64 29 64 64v464h-96c-70 0-128 58-128 128v80H272c-35 0-64-29-64-64zm462 45c-4 5-9 8-14 11v-72c0-35 29-64 64-64h75L670 861z"/>
<path fill="#555" d="M368 352h288c18 0 32-14 32-32s-14-32-32-32H368c-18 0-32 14-32 32s14 32 32 32zm128 256H368c-18 0-32 14-32 32s14 32 32 32h128c18 0 32-14 32-32s-14-32-32-32zm-128-96h288c18 0 32-14 32-32s-14-32-32-32H368c-18 0-32 14-32 32s14 32 32 32z"/>
</svg>
</div>
<div id="getlink" class="mr-5">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024">
<path fill="#666" d="m600 697-1 1-94 76a198 198 0 0 1-280-30c-69-85-56-211 30-280l99-81-46-57-99 81a273 273 0 0 0 143 483 279 279 0 0 0 29 1c63 0 122-21 171-61l95-76-46-56-1-1zm256-464a273 273 0 0 0-383-40l-91 73 47 58 90-74a199 199 0 1 1 250 310l-96 77-1 1 46 57 97-78c56-46 92-111 99-184 9-72-12-143-58-200z"/>
<path fill="#666" d="m388 668 306-255 1-1-48-56-305 255h-2z"/>
</svg>
</div>
<div id="random" class="mr-5">
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path fill="#666" d="M988.492 718.906L864.168 595.6c-15.686-15.556-41.012-15.454-56.568.232-15.556 15.686-15.452 41.012.232 56.568L922.368 766h-48.812c-115.514 0-222.1-49.978-292.428-137.122-13.874-17.194-39.058-19.88-56.248-6.006-17.192 13.874-19.88 39.056-6.006 56.248C604.464 785.176 733.74 846 873.556 846h44.78L807.832 955.6c-15.684 15.556-15.79 40.882-.232 56.568A39.88 39.88 0 0 0 836.002 1024c10.18 0 20.368-3.864 28.166-11.6l124.324-123.306C1011.39 866.384 1024 836.162 1024 804s-12.61-62.382-35.508-85.094z"/><path fill="#666" d="M988.492 134.906L864.168 11.6c-15.686-15.556-41.012-15.454-56.568.232-15.556 15.686-15.452 41.012.232 56.568L918.336 178h-44.78c-163.332 0-314.542 86.102-394.626 224.702l-16.952 29.342-27.352-47.342C354.544 246.102 203.332 160 40 160c-22.092 0-40 17.908-40 40s17.908 40 40 40c134.852 0 259.522 70.782 325.356 184.724L415.78 512l-50.426 87.276C299.522 713.22 174.852 784 40 784c-22.092 0-40 17.908-40 40s17.908 40 40 40c163.332 0 314.542-86.102 394.626-224.702l61.64-106.684c.224-.374.442-.752.654-1.134l51.28-88.756C614.034 328.782 738.704 258 873.556 258h48.812L807.832 371.6c-15.684 15.556-15.79 40.882-.232 56.568A39.88 39.88 0 0 0 836.002 440c10.18 0 20.368-3.864 28.166-11.6l124.324-123.306C1011.39 282.384 1024 252.162 1024 220s-12.61-62.382-35.508-85.094z"/></svg>
</div>
<div id="search" class="mr-5">
<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M689.067 631.467L889.6 832c38.4 38.4-19.2 96-57.6 57.6L631.467 689.067C576 731.733 505.6 757.333 430.933 757.333 249.6 757.333 102.4 610.133 102.4 428.8s147.2-326.4 328.533-326.4 328.534 147.2 328.534 328.533c-2.134 74.667-27.734 145.067-70.4 200.534zm-258.134 44.8c136.534 0 245.334-110.934 245.334-245.334S565.333 183.467 430.933 183.467 183.467 294.4 183.467 430.933 294.4 676.267 430.933 676.267z" fill="#666"/></svg>
</div>
<div class="selector-wrapper visibility-selector ">
<div id="lock" class="current-value-container active false">
<span id="lock-now" class="value-text"></span><span class="arrow-text"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon-img"><polyline points="6 9 12 15 18 9"></polyline></svg></span>
</div>
<div id="lock-wrapper" class="items-wrapper !hidden">
<div id="lockPrivate" class="item-lock" data-type="PRIVATE"></div>
<div id="lockProtected" class="item-lock" data-type="PROTECTED"></div>
<div id="lockPublic" class="item-lock" data-type="PUBLIC"></div>
</div>
</div>
</div>
<div class="btns-container" type="submit" name="submit" id="submit">
<button id="content_submit_text" class="action-btn confirm-btn"><img class="icon-img" src="../assets/logo_24x24.png"></button>
</div>
</div>
<div class="upload-list-wrapper">
<div id="uploadlist-title" class="upload-list-title"></div>
<div id="uploadlist" class="upload-list"></div>
</div>
<div class="tag-list" id="taglist"></div>
<div class="tag-hide" id="taghide">
<input
id="hideInput"
class="inputer"
name="hideInput"
type="text"
value=""
maxlength="50"
placeholder=""
/>
<input
id="showInput"
class="inputer"
name="showInput"
type="text"
value=""
maxlength="50"
placeholder=""
/>
<span id="saveTag" class="action-btn confirm-btn"></span>
</div>
<div class="" id="randomlist"></div>
<input type="file" id="inFile" style="display:none;">
<script src="../js/i18n.js"></script>
<script src="../js/jquery.min.js"></script>
<script src="../js/message.js"></script>
<script src="../js/dayjs.min.js"></script>
<script src="../js/zh-cn.js"></script>
<script src="../js/ja.js"></script>
<script src="../js/ko.js"></script>
<script src="../js/relativeTime.js"></script>
<script src="../js/view-image.js"></script>
<script src="../js/compat/memosApi.modern.js"></script>
<script src="../js/compat/memosApi.v020-v021.js"></script>
<script src="../js/compat/memosApi.v023.js"></script>
<script src="../js/compat/memosApi.adapter.js"></script>
<script src="../js/oper.js"></script>
</body>
</html>