19 Commits

Author SHA1 Message Date
jonny 71c8ed04df Update package-extensions.yml 2026-04-22 19:17:18 +08:00
jonny 8f4b64a13f Firefox: add icons, rewrite viewer, update manifest
Add Firefox icon assets and update manifest to use the new icon files and opt-out data_collection_permissions. Replace minified view-image.js with a modern, readable implementation (style injection, accessible controls, keyboard navigation, and better DOM handling). Simplify background popup logic to call chrome.browserAction.openPopup directly. Remove the packaging section from README to clean up docs.
2026-04-22 19:15:49 +08:00
jonny cf75f087a7 Publish extension packages to releases
Rewrite package-extensions workflow to publish built extension artifacts directly to GitHub Releases instead of uploading Actions artifacts. Changes include: enable Node 24 for JS actions, add workflow_dispatch input (release_tag), resolve release tag at runtime, upgrade actions/checkout to v5 and set contents permission to write, produce dist/release/*.zip/.xpi, remove artifact uploads, create/reuse release and upload assets with gh, and write a workflow summary. README updated to document the new release-based publishing flow and Node/checkout changes.
2026-04-22 19:04:57 +08:00
jonny d307741f1f 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.
2026-04-22 18:53:42 +08:00
jonny 15b8493a2b Add tutorial section to README
Add a "教程" section to README describing how to use the search: enter keywords in the text box and click the search button. Note that random and search features for private memos are supported in v0.24+ (other versions do not support this). Provides brief usage steps to help users.
2026-04-22 17:01:59 +08:00
jonny 3968b6896c Add Memos API adapter and settings UI
Introduce a Memos API adapter to unify compatibility across API flavors (v020-v021, v023, modern) and probe/detect server flavor. Add js/compat/memosApi.adapter.js and rename existing compat modules (memosApi.v024.js -> memosApi.modern.js, memosApi.v1.js -> memosApi.v020-v021.js) and trim probe from v023. Add settings UI support: new i18n keys in en/ja/ko/zh_CN, settings panel styles in css/main.css, new settings input binding and save flow in js (attachment-only default text persisted). Refactor oper.js to use the adapter for uploads, deletes, tag listing, search and image preview auth handling; add helper functions for attachment-only default text and settings payload. Update i18n bindings, popup, manifest and README changelog accordingly.
closed #5
2026-04-22 16:13:18 +08:00
jonny 6c4801cb16 Update README.md 2026-04-21 15:44:42 +08:00
jonny 944be49edb Support Memos v0.27 and improve API parsing
Bump supported Memos version to v0.27 across locales, README changelog, and manifest (version and version_name). Improve memos API compatibility: prefer and return trimmed username when present, handle non-numeric trailing name segments safely, and encode userid in the user-scoped memos URL to avoid issues with special characters.
2026-04-21 15:26:22 +08:00
jonny 25fbd486bf Bump extension version to 2026.04.11
Update manifest.json version from "2026.03.25" to "2026.04.11" to reflect the new release. No other changes to manifest fields or supported version range.
2026-04-11 10:44:40 +08:00
jonny 15d99cb51f Make editor container and input flex-aware
Enable flex layout for the editor wrapper and make .common-editor-inputer a flexible child. Added display:flex and flex-direction:column to the container rule, replaced height:auto with flex: 1 1 auto and added min-height:0 on .common-editor-inputer to allow proper shrinking/growth and avoid overflow issues in flex layouts. (css/main.css)
2026-04-11 10:43:47 +08:00
jonny ed5fc86e39 Replace language select with toggle menu
Replace the old <select> language picker with a custom toggle button and dropdown menu. Updates include new popup HTML (langToggle button, langMenu with menuitemradio buttons), new CSS for .lang-toggle/.lang-menu/.lang-menu-item (styling, hover/active states, shadow, rounded corners), and JS to manage label sync, menu state, click handlers, outside-click and Escape to close, and applying the selected UI language. Also update references in i18n code (setTitle target and language init flow) and persist language selection. Bump manifest version from 2026.03.24 to 2026.03.25.
2026-03-23 11:29:49 +08:00
jonny 0f468d49aa Update Chinese strings and bump version
Optimize Chinese display text and bump extension version. Add a README changelog entry for "优化中文显示效果"; simplify several zh_CN locale strings (lock labels shortened to "私有", "登录可见", "公开" and default tag placeholders updated). Also increment manifest version to 2026.03.24.
2026-03-23 10:52:59 +08:00
jonny f3e55ec53e Update version number to 2026.03.23 2026-03-22 23:27:14 +08:00
jonny f05581f88a Adjust body min-width
Reduced minimum width for the body class and removed media query styles for mobile responsiveness.
2026-03-22 23:25:50 +08:00
jonny d237a7f1c6 Bump manifest version and add mobile portrait note
Update manifest.json version to 2026.03.22 and add a README changelog entry (20260322) noting adaptation for mobile portrait windows.
2026-03-22 20:48:23 +08:00
jonny 13cc7659ea Add responsive styles for small portrait screens
Introduce a media query for max-width:480px, portrait, touch-only devices to improve layout on small screens. Adjustments include removing body min-width, enabling wrapping and spacing for .common-tools-wrapper, forcing .common-tools-container and .btns-container to full width with flexible sizing, adding bottom margin for .mr-5 elements, and increasing minimum dimensions for #content_submit_text to improve tap targets and prevent overflow.
2026-03-22 20:45:15 +08:00
jonny e9730b5839 Lower minimum supported Memos to v0.15.0
Update compatibility range to include Memos v0.15.0 across README and locale files (en, ja, ko, zh_CN). Bump manifest version/date to 2026.03.12 and update version_name to reflect the new supported range. This extends backward compatibility to older Memos releases.
2026-03-12 18:35:06 +08:00
jonny f2f1ff2c10 Bump manifest version and update changelog
Update manifest.json to version 2026.03.11 and set version_name to "Supports 0.18.0 - 0.26.x". Also add a README changelog entry for 2026-03-10 noting remembered drag window size and removal of drag window animation. This prepares a small release update and documents the recent UI tweak.
2026-03-09 20:07:57 +08:00
jonny 5fac00b5ce - feat:记忆拖拽窗口大小和优化动画效果
Add persistent, robust scaling for the proportional editor and remove CSS transitions that interfered with instant restores. Introduces a storageKey, parseScale, applyScaleInstant and logic to restore scale synchronously from localStorage (for immediate UX) and asynchronously from chrome.storage.sync (best-effort), keeping localStorage in sync. Ensures pending RAF updates are flushed before persisting and guards against missing APIs or invalid values. Also removes transition properties in main.css so scale restores remain immediate and predictable.
2026-03-09 20:00:42 +08:00
55 changed files with 6263 additions and 664 deletions
+80
View File
@@ -0,0 +1,80 @@
name: Package Extensions
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
on:
workflow_dispatch:
inputs:
release_tag:
description: Release tag used for direct asset upload, for example v2026.04.23
required: true
default: 'v2026.04.23'
push:
tags:
- 'v*'
jobs:
package:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Resolve release tag
id: release
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
tag='${{ github.event.inputs.release_tag }}'
else
tag='${{ github.ref_name }}'
fi
echo "tag=$tag" >> "$GITHUB_OUTPUT"
- 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/release
pushd chrome >/dev/null
zip -qr "../dist/release/memos-bber-chrome-${{ steps.versions.outputs.chrome_version }}.zip" .
popd >/dev/null
pushd firefox >/dev/null
zip -qr "../dist/release/memos-bber-firefox-${{ steps.versions.outputs.firefox_version }}.xpi" .
popd >/dev/null
- name: Publish release assets
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
tag='${{ steps.release.outputs.tag }}'
gh release view "$tag" >/dev/null 2>&1 || \
gh release create "$tag" --title "$tag" --notes "Automated extension packages"
gh release upload "$tag" dist/release/* --clobber
- name: Write workflow summary
run: |
tag='${{ steps.release.outputs.tag }}'
echo "## Release assets" >> "$GITHUB_STEP_SUMMARY"
echo "- Chrome: https://github.com/${{ github.repository }}/releases/download/$tag/memos-bber-chrome-${{ steps.versions.outputs.chrome_version }}.zip" >> "$GITHUB_STEP_SUMMARY"
echo "- Firefox: https://github.com/${{ github.repository }}/releases/download/$tag/memos-bber-firefox-${{ steps.versions.outputs.firefox_version }}.xpi" >> "$GITHUB_STEP_SUMMARY"
+17 -2
View File
@@ -1,17 +1,32 @@
## 说明
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/)。
## 更新日志
- 20260422 调整发送设置,支持仅发送附件
#### 20260421 更新匹配 0.27.x
- 20260325 优化语言按钮样式
- 20260323 优化中文显示效果
- 20260322 适配移动端竖屏窗口
- 20260310 记忆拖拽窗口大小,移除拖拽窗口动画
- 20260309 右键发送选中文本保持原格式,增加全屏和窗口放大功能
### 20260308 向前兼容到0.18.0,可能再往前也行,只测试到0.18.0
#### 20260308 向前兼容到0.15.0,可能再往前也行,只测试到0.15.0
- 20260307 增加语言切换按钮以及韩语和日语支持,
- 2026年03月06日 右键菜单发送选中文本附带原文链接
- 2026年03月05日 向前兼容到0.24.0,可能再往前也行,因为只测试了0.24.0和0.25.0以及当前最新版本,如有更早版本需求,可issue反馈
- 2026年02月22日 由于原作者基本放弃更新,现接手维护,不兼容更新,匹配 v0.26.1 ,欢迎各位大佬PR
## 教程
- 在文本框输入想搜索的关键字,点击搜索按钮
- 随机和搜索功能在`0.24`以上版本支持私有权限的 memo,其它版本不支持
<details>
<summary>点击展开/折叠内容</summary>
2024.07.21 不兼容更新,已匹配 v0.22.3
View File
@@ -21,7 +21,19 @@
"message": "Save"
},
"supportedMemosVersion": {
"message": "Compatible with Memos v0.18.0 - 0.26.x"
"message": "Compatible with Memos v0.15.0 - 0.27.x"
},
"settingsConnectionTitle": {
"message": "Connection"
},
"settingsConnectionDesc": {
"message": "Configure the Memos site URL and access token."
},
"settingsPostingTitle": {
"message": "Posting"
},
"settingsPostingDesc": {
"message": "Default text for attachment-only sends"
},
"placeApiUrl":{
"message": "Memos site URL"
@@ -50,6 +62,9 @@
"placeShowInput":{
"message": "Default 'Everyone can see' Tag name"
},
"placeAttachmentOnlyDefaultText": {
"message": "Default text for attachment-only sends (leave blank to use built-in text)"
},
"uploadedListTitle": {
"message": "Uploaded files, Drag to reorder"
},
@@ -21,7 +21,19 @@
"message": "保存"
},
"supportedMemosVersion": {
"message": "Memos v0.18.0 - 0.26.x に対応"
"message": "Memos v0.15.0 - 0.27.x に対応"
},
"settingsConnectionTitle": {
"message": "接続設定"
},
"settingsConnectionDesc": {
"message": "Memos のURLとアクセストークンを設定します。"
},
"settingsPostingTitle": {
"message": "投稿設定"
},
"settingsPostingDesc": {
"message": "添付ファイルのみ送信時の既定テキスト"
},
"placeApiUrl": {
"message": "Memos サイトURL"
@@ -50,6 +62,9 @@
"placeShowInput": {
"message": "既定の「全員に公開」タグ名"
},
"placeAttachmentOnlyDefaultText": {
"message": "添付ファイルのみ送信時の既定テキスト(空欄で内蔵文言を使用)"
},
"uploadedListTitle": {
"message": "アップロード済みファイル(ドラッグで並べ替え)"
},
@@ -21,7 +21,19 @@
"message": "저장"
},
"supportedMemosVersion": {
"message": "Memos v0.18.0 - 0.26.x 호환"
"message": "Memos v0.15.0 - 0.27.x 호환"
},
"settingsConnectionTitle": {
"message": "연결 설정"
},
"settingsConnectionDesc": {
"message": "Memos 사이트 URL과 액세스 토큰을 설정합니다."
},
"settingsPostingTitle": {
"message": "전송 설정"
},
"settingsPostingDesc": {
"message": "첨부만 전송할 때의 기본 텍스트"
},
"placeApiUrl": {
"message": "Memos 사이트 URL"
@@ -50,6 +62,9 @@
"placeShowInput": {
"message": "기본 '모두 공개' 태그 이름"
},
"placeAttachmentOnlyDefaultText": {
"message": "첨부만 전송할 때의 기본 텍스트(비워두면 내장 문구 사용)"
},
"uploadedListTitle": {
"message": "업로드된 파일(드래그로 순서 변경)"
},
@@ -21,7 +21,19 @@
"message": "保存"
},
"supportedMemosVersion": {
"message": "兼容 Memos v0.18.0 - 0.26.x"
"message": "兼容 Memos v0.15.0 - 0.27.x"
},
"settingsConnectionTitle": {
"message": "连接设置"
},
"settingsConnectionDesc": {
"message": "配置 Memos 服务地址和访问令牌。"
},
"settingsPostingTitle": {
"message": "发送设置"
},
"settingsPostingDesc": {
"message": "仅发送附件时的默认文本"
},
"placeApiUrl":{
"message": "请填入 Memos 主页网址"
@@ -33,22 +45,25 @@
"message": "现在的想法是..."
},
"lockPrivate":{
"message": "仅自己可见"
"message": "私有"
},
"lockProtected":{
"message": "登录用户可见"
"message": "登录可见"
},
"lockPublic":{
"message": "所有人可见"
"message": "公开"
},
"submitBtn":{
"message": "记下"
},
"placeHideInput":{
"message": "默认“仅自己可见”标签名"
"message": "默认“私有”标签名"
},
"placeShowInput":{
"message": "默认“所有人可见”标签名"
"message": "默认“公开”标签名"
},
"placeAttachmentOnlyDefaultText":{
"message": "仅发送附件时的默认文本(留空则使用内置文案)"
},
"picDrag":{
"message": "拖拽到窗口上传该图片"

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

+127 -10
View File
@@ -20,7 +20,7 @@ input:focus::placeholder ,.common-editor-inputer:focus::placeholder {
}
.body{
min-width:460px;
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;
@@ -29,6 +29,7 @@ input:focus::placeholder ,.common-editor-inputer:focus::placeholder {
line-height: 1.5;
position: relative;
}
a{color: #555;}
.title{
width: 100px;
@@ -44,9 +45,6 @@ a{color: #555;}
background-color: rgb(255,255,255);
margin-top:0.8rem;
padding: 0.6rem;
transition-property: all;
transition-timing-function: cubic-bezier(.4,0,.2,1);
transition-duration: .15s;
}
.memo-editor{
position: relative;
@@ -54,6 +52,8 @@ a{color: #555;}
overflow: visible;
box-sizing: border-box;
contain: layout;
display: flex;
flex-direction: column;
}
.memo-editor-header{
@@ -152,7 +152,8 @@ a{color: #555;}
.common-editor-inputer{
padding-right: 1.5rem;
height: auto;
flex: 1 1 auto;
min-height: 0;
display: block;
box-sizing: border-box;
}
@@ -169,6 +170,60 @@ a{color: #555;}
}
input.inputer{border-bottom: 1px solid #ccc;width:75%;}
.settings-panel{
display: flex;
flex-direction: column;
gap: .75rem;
margin-top: .75rem;
}
.settings-section{
border: 1px solid rgb(229,231,235);
border-radius: .5rem;
background-color: rgb(255,255,255);
padding: .75rem;
display: flex;
flex-direction: column;
gap: .6rem;
}
.settings-section-title{
font-size: .9rem;
font-weight: 700;
color: rgb(55,65,81);
}
.settings-section-desc{
font-size: .75rem;
line-height: 1.35;
color: #7a7a7a;
}
.settings-input{
width: 100% !important;
box-sizing: border-box;
border: 1px solid rgb(229,231,235);
border-radius: .35rem;
background-color: #fafafa;
padding: .55rem .7rem;
}
.settings-input:focus{
border-color: rgb(22,163,74);
background-color: rgb(255,255,255);
}
.settings-textarea{
resize: vertical;
min-height: 4.5rem;
white-space: pre-wrap;
}
.settings-actions{
display: flex;
justify-content: flex-end;
}
#saveKey{margin:0;flex:1;}
.common-tools-wrapper {
@@ -324,17 +379,79 @@ input.inputer{border-bottom: 1px solid #ccc;width:75%;}
top: .55rem;
}
.lang-select{
border: 1px solid rgb(229,231,235);
border-radius: .25rem;
background-color: rgb(255,255,255);
.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;
padding: .15rem .35rem;
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%;
+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)
@@ -11,12 +11,19 @@
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 && !Number.isNaN(Number(last))) return Number(last)
if (last) {
if (!Number.isNaN(Number(last))) return Number(last)
if (last.trim() !== '') return last.trim()
}
}
return null
@@ -88,7 +95,7 @@
// 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/' + info.userid + '/memos' + qs
? info.apiUrl + 'api/v1/users/' + encodeURIComponent(String(info.userid)) + '/memos' + qs
: null
const urlGlobal = info.apiUrl + 'api/v1/memos' + qs
@@ -493,7 +500,7 @@
doPatchAttachments()
}
global.MemosApi = {
global.MemosApiModern = {
extractUserIdFromAuthResponse: extractUserIdFromAuthResponse,
extractMemosListFromResponse: extractMemosListFromResponse,
isNotFoundLikeXhr: isNotFoundLikeXhr,
@@ -274,7 +274,7 @@
})
}
global.MemosApiV1 = {
global.MemosApiV020V021 = {
listMemos: listMemos,
createMemo: createMemo,
patchMemo: patchMemo,
@@ -111,112 +111,9 @@
})
}
function probeApiFlavor(apiUrl, apiTokens, callback) {
const headers = { Authorization: 'Bearer ' + apiTokens }
function looksLikeMemosListPayload(data) {
if (!data) return false
if (Array.isArray(data)) return true
if (Array.isArray(data.memos)) return true
if (data.data && Array.isArray(data.data.memos)) return true
if (Array.isArray(data.list)) return true
// Common JSON error shapes should not be treated as success.
if (typeof data.error === 'string' || typeof data.message === 'string') return false
return false
}
function isNotFoundLike(xhr) {
const status = xhr && xhr.status
return status === 404 || status === 405
}
// Modern-style filter probe.
const modernQ =
'api/v1/memos?pageSize=1&filter=' +
encodeURIComponent('visibility in ["PUBLIC","PROTECTED"]')
// v0.23-style filter probe.
const v023Q =
'api/v1/memos?pageSize=1&filter=' +
encodeURIComponent('visibilities == ["PUBLIC","PROTECTED"]')
// v0.20/v0.21 unified API v1 probe.
const v1Q = 'api/v1/memo?limit=1&rowStatus=NORMAL'
global.$
.ajax({
url: apiUrl + modernQ,
method: 'GET',
headers: headers,
dataType: 'json'
})
.done(function (data) {
if (looksLikeMemosListPayload(data)) {
callback({ flavor: 'modern' })
return
}
// Treat unexpected success payload as unknown and continue probing.
global.$
.ajax({
url: apiUrl + v023Q,
method: 'GET',
headers: headers,
dataType: 'json'
})
.done(function (data2) {
if (looksLikeMemosListPayload(data2)) callback({ flavor: 'v023' })
else callback({ flavor: 'unknown' })
})
.fail(function () {
callback({ flavor: 'unknown' })
})
})
.fail(function (xhr) {
if (xhr && xhr.status === 400) {
global.$
.ajax({
url: apiUrl + v023Q,
method: 'GET',
headers: headers,
dataType: 'json'
})
.done(function (data2) {
if (looksLikeMemosListPayload(data2)) callback({ flavor: 'v023' })
else callback({ flavor: 'unknown' })
})
.fail(function () {
callback({ flavor: 'unknown' })
})
return
}
// If /api/v1/memos is missing, check /api/v1/memo (v0.20/v0.21 unified).
if (isNotFoundLike(xhr)) {
global.$
.ajax({
url: apiUrl + v1Q,
method: 'GET',
headers: headers,
dataType: 'json'
})
.done(function (data2) {
if (looksLikeMemosListPayload(data2)) callback({ flavor: 'v1' })
else callback({ flavor: 'unknown' })
})
.fail(function () {
callback({ flavor: 'unknown' })
})
return
}
callback({ flavor: 'unknown' })
})
}
global.MemosApiV023 = {
buildFilter: buildFilter,
listMemos: listMemos,
extractTagsFromMemo: extractTagsFromMemo,
probeApiFlavor: probeApiFlavor
extractTagsFromMemo: extractTagsFromMemo
}
})(window)
View File
+70 -12
View File
@@ -47,6 +47,36 @@ function formatSubstitutions(message, substitutions) {
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) {
@@ -72,10 +102,14 @@ function setTitle(id, messageKey) {
}
function applyStaticI18n() {
setText('saveKey', 'saveBtn')
setText('saveSettings', 'saveBtn')
setText('saveTag', 'saveBtn')
setText('supportedMemosVersion', 'supportedMemosVersion')
setText('settingsConnectionTitle', 'settingsConnectionTitle')
setText('settingsConnectionDesc', 'settingsConnectionDesc')
setText('settingsPostingTitle', 'settingsPostingTitle')
setText('settingsPostingDesc', 'settingsPostingDesc')
setPlaceholder('apiUrl', 'placeApiUrl')
setPlaceholder('apiTokens', 'placeApiTokens')
@@ -91,6 +125,7 @@ function applyStaticI18n() {
setPlaceholder('hideInput', 'placeHideInput')
setPlaceholder('showInput', 'placeShowInput')
setPlaceholder('attachmentOnlyDefaultText', 'placeAttachmentOnlyDefaultText')
setText('uploadlist-title', 'uploadedListTitle')
@@ -100,7 +135,9 @@ function applyStaticI18n() {
setText('langOptionZhCN', 'langChineseSimplified')
setText('langOptionJa', 'langJapanese')
setText('langOptionKo', 'langKorean')
setTitle('langSelect', 'tipLanguage')
setTitle('langToggle', 'tipLanguage')
const langToggle = document.getElementById('langToggle')
if (langToggle) langToggle.setAttribute('aria-label', t('tipLanguage'))
// Native hover tooltips (title)
setTitle('opensite', 'tipOpenSite')
@@ -122,26 +159,47 @@ async function setUiLanguage(nextLang, { persist = true } = {}) {
currentUiLanguage = lang
overrideMessages = await loadLocaleMessages(lang)
applyStaticI18n()
const select = document.getElementById('langSelect')
if (select && select.value !== lang) select.value = lang
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 select = document.getElementById('langSelect')
if (select) {
select.addEventListener('change', async () => {
await setUiLanguage(select.value)
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)
})
}
const items = await storageSyncGet({ [UI_LANGUAGE_STORAGE_KEY]: 'auto' })
const stored = normalizeUiLanguage(items[UI_LANGUAGE_STORAGE_KEY])
if (select) select.value = stored
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
View File
View File
View File
+220 -467
View File
@@ -39,93 +39,117 @@ function initProportionalEditorResize() {
editor.style.minWidth = `${baseW}px`
editor.style.minHeight = `${baseH}px`
const storageKey = 'popupEditorScale'
const nonEditorHeight = Math.max(0, Math.ceil(document.body.scrollHeight - initialRect.height))
let maxScale = 1
let currentScale = 1
let dragging = false
let dragStartX = 0
let dragStartY = 0
let dragStartScale = 1
const clampScale = (scale) => {
if (!Number.isFinite(scale)) return 1
return Math.min(Math.max(scale, 1), maxScale)
}
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 {
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 editorRect = editor.getBoundingClientRect()
const toolsRect = tools.getBoundingClientRect()
const toolsStyle = window.getComputedStyle(tools)
const gap = parseFloat(toolsStyle.marginTop || '0') || 0
const availW = Math.max(0, viewportW - safety - editorRect.left)
const availH = Math.max(0, viewportH - safety - toolsRect.height - editorRect.top - gap)
const scaleW = baseW > 0 ? availW / baseW : 1
const scaleH = baseH > 0 ? availH / baseH : 1
maxScale = Math.max(1, Math.min(scaleW, scaleH))
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)
}
computeMaxScale()
window.addEventListener('resize', computeMaxScale)
let dragging = false
let startX = 0
let startY = 0
let startScale = 1
let rafId = 0
let pendingScale = null
const readCurrentScale = () => {
const w = parseFloat(editor.style.width || '')
const h = parseFloat(editor.style.height || '')
const sw = baseW > 0 && Number.isFinite(w) ? w / baseW : 1
const sh = baseH > 0 && Number.isFinite(h) ? h / baseH : 1
return Math.max(1, sw, sh)
}
const applyScale = (scale) => {
const s = Math.max(1, Math.min(maxScale, scale))
editor.style.width = `${Math.round(baseW * s)}px`
editor.style.height = `${Math.round(baseH * s)}px`
}
const scheduleApply = () => {
if (rafId) return
rafId = window.requestAnimationFrame(() => {
rafId = 0
if (pendingScale == null) return
const s = pendingScale
pendingScale = null
applyScale(s)
})
}
handle.addEventListener('pointerdown', (ev) => {
dragging = true
startX = ev.clientX
startY = ev.clientY
startScale = readCurrentScale()
computeMaxScale()
try { handle.setPointerCapture(ev.pointerId) } catch (_) {}
ev.preventDefault()
})
handle.addEventListener('pointermove', (ev) => {
if (!dragging) return
const dx = ev.clientX - startX
const dy = ev.clientY - startY
// Proportional scale based on diagonal length for smoother, more linear feel.
const diag0 = Math.hypot(baseW, baseH)
const targetW = baseW * startScale + dx
const targetH = baseH * startScale + dy
const diag1 = Math.hypot(targetW, targetH)
const next = diag0 > 0 ? diag1 / diag0 : startScale
pendingScale = next
scheduleApply()
})
const endDrag = () => {
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
}
@@ -199,6 +223,23 @@ window.addEventListener('i18n:changed', (ev) => {
let relistNow = []
const API_FLAVOR_V020_V021 = 'v020-v021'
const DEFAULT_ATTACHMENT_ONLY_TEXT = '#附件 此为默认填充,如需自定义,请在发送附件前填写你的文本内容或者设置项里自定义.'
function getAttachmentOnlyDefaultText(customText) {
const value = typeof customText === 'string' ? customText.trim() : ''
return value || DEFAULT_ATTACHMENT_ONLY_TEXT
}
function resolveSendContent(rawContent, resources, customText) {
const value = typeof rawContent === 'string' ? rawContent : ''
if (value.trim() !== '') return value
const items = Array.isArray(resources) ? resources : []
if (items.length === 0) return ''
return getAttachmentOnlyDefaultText(customText)
}
function get_info(callback) {
chrome.storage.sync.get(
{
@@ -212,7 +253,8 @@ function get_info(callback) {
open_content: '',
userid: '',
memoUiPath: 'memos',
resourceIdList: []
resourceIdList: [],
attachmentOnlyDefaultText: ''
},
function (items) {
var flag = false
@@ -234,18 +276,18 @@ function get_info(callback) {
returnObject.userid = items.userid
returnObject.memoUiPath = items.memoUiPath
returnObject.resourceIdList = items.resourceIdList
returnObject.attachmentOnlyDefaultText = items.attachmentOnlyDefaultText
if (callback) callback(returnObject)
}
)
}
function isV023Flavor(info) {
return info && info.apiFlavor === 'v023' && window.MemosApiV023
}
function isV1Flavor(info) {
return info && info.apiFlavor === 'v1' && window.MemosApiV1
function getApiAdapter(info) {
if (window.MemosApiAdapter && typeof window.MemosApiAdapter.resolve === 'function') {
return window.MemosApiAdapter.resolve(info)
}
return null
}
function getMemoUid(memo) {
@@ -273,6 +315,7 @@ get_info(function (info) {
$('#apiTokens').val(info.apiTokens)
$('#hideInput').val(info.hidetag)
$('#showInput').val(info.showtag)
$('#attachmentOnlyDefaultText').val(info.attachmentOnlyDefaultText)
if (info.open_action === 'upload_image') {
//打开的时候就是上传图片
uploadImage(info.open_content)
@@ -488,10 +531,11 @@ function memoFromNow(memo) {
}
function hydrateV1PreviewImages(info) {
if (!isV1Flavor(info)) return
if (!info || !info.apiUrl || !info.apiTokens) return
const adapter = getApiAdapter(info)
if (!adapter || !adapter.needsAuthenticatedImagePreview()) return
if (!info || !info.apiUrl) return
const token = String(info.apiTokens)
const token = info && info.apiTokens != null ? String(info.apiTokens).trim() : ''
let root = String(info.apiUrl)
let apiOrigin = ''
try {
@@ -575,9 +619,10 @@ function hydrateV1PreviewImages(info) {
fetch(abs, {
method: 'GET',
headers: {
credentials: 'include',
headers: token ? {
Authorization: 'Bearer ' + token
}
} : {}
})
.then(function (res) {
if (!res || !res.ok) throw new Error('HTTP ' + (res ? res.status : '0'))
@@ -591,9 +636,9 @@ function hydrateV1PreviewImages(info) {
img.src = objectUrl
})
.catch(function () {
// Don't break previews for modern versions where plain <img src> may already work.
// Fall back to the original URL so the browser can still try cookie-based auth.
if (hasAuthAttr) {
try { img.removeAttribute('src') } catch (_) {}
try { img.setAttribute('src', abs) } catch (_) {}
}
})
})
@@ -718,32 +763,9 @@ $(document).on('click', '.upload-del', function () {
return
}
const inferredId = (function () {
if (rid != null && String(rid).trim() !== '' && !Number.isNaN(Number(rid))) return Math.floor(Number(rid))
const tail = String(name).split('/').pop()
if (tail && !Number.isNaN(Number(tail))) return Math.floor(Number(tail))
return null
})()
const doDelete = isV1Flavor(info) && window.MemosApiV1 && typeof window.MemosApiV1.deleteResource === 'function' && inferredId != null
? function (onOk, onFail) {
window.MemosApiV1.deleteResource(info, inferredId, onOk, onFail)
}
: function (onOk, onFail) {
$.ajax({
url: info.apiUrl + 'api/v1/' + name,
type: 'DELETE',
headers: { Authorization: 'Bearer ' + info.apiTokens },
success: function (data) {
onOk(data)
},
error: function (xhr) {
onFail(xhr)
}
})
}
doDelete(
const adapter = getApiAdapter(info)
adapter.deleteResource(
{ name: name, id: rid },
function () {
const next = (Array.isArray(relistNow) ? relistNow : []).filter(function (x) {
return x && x.name !== name
@@ -759,193 +781,101 @@ $(document).on('click', '.upload-del', function () {
)
})
})
function uploadImage(file) {
$.message({
message: msg('picUploading'),
autoClose: false
});
get_info(function (info) {
if (isV1Flavor(info)) {
uploadImageNowV1(file)
return
}
const reader = new FileReader();
reader.onload = function(e) {
const base64String = e.target.result.split(',')[1];
uploadImageNow(base64String, file);
};
reader.onerror = function(error) {
console.error('Error reading file:', error);
};
reader.readAsDataURL(file);
})
};
function uploadImageNowV1(file) {
get_info(function (info) {
if (!info.status) {
$.message({ message: msg('placeApiUrl') })
return
}
let old_name = file.name.split('.')
let file_ext = file.name.split('.').pop()
let now = dayjs().format('YYYYMMDDHHmmss')
let new_name = old_name[0] + '_' + now + '.' + file_ext
window.MemosApiV1.uploadResourceBlob(
info,
const adapter = getApiAdapter(info)
adapter.uploadFile(
file,
{ filename: new_name, type: file.type },
{
editorContent: $("textarea[name=text]").val(),
hideTag: info.hidetag,
showTag: info.showtag,
memoLock: info.memo_lock
},
function (entity) {
const inferredId = (function () {
if (!entity) return null
const v = entity.id != null ? entity.id : entity.ID != null ? entity.ID : entity.Id
if (typeof v === 'number' && Number.isFinite(v)) return Math.floor(v)
if (typeof v === 'string' && v.trim() !== '' && !Number.isNaN(Number(v))) return Math.floor(Number(v))
return null
})()
// v0.18: resource entity has no `name`, only `id/filename/type/...`.
// Treat having an id as a successful upload.
if (entity && (entity.name || inferredId != null)) {
const name = entity.name || (inferredId != null ? 'resources/' + String(inferredId) : '')
relistNow.push({
id: inferredId != null ? inferredId : entity.id,
name: name,
filename: entity.filename || new_name,
createTime: entity.createTime || entity.createdTs || entity.createdAt,
type: entity.type
})
if (entity) {
relistNow.push(entity)
chrome.storage.sync.set({ open_action: '', open_content: '', resourceIdList: relistNow }, function () {
$.message({ message: msg('picSuccess') })
renderUploadList(relistNow)
})
return
}
chrome.storage.sync.set({ open_action: '', open_content: '' }, function () {
$.message({ message: msg('picFailed') })
})
},
function () {
chrome.storage.sync.set({ open_action: '', open_content: '' }, function () {
$.message({ message: msg('picFailed') })
})
}
)
})
};
function buildCustomSettingsPayload() {
return {
attachmentOnlyDefaultText: $('#attachmentOnlyDefaultText').val()
}
}
function uploadImageNow(base64String, file) {
get_info(function(info) {
if (info.status) {
let old_name = file.name.split('.');
let file_ext = file.name.split('.').pop();
let now = dayjs().format('YYYYMMDDHHmmss');
let new_name = old_name[0] + '_' + now + '.' + file_ext;
var hideTag = info.hidetag
var showTag = info.showtag
var nowTag = $("textarea[name=text]").val().match(/(#[^\s#]+)/)
var sendvisi = info.memo_lock || ''
if(nowTag){
if(nowTag[1] == showTag){
sendvisi = 'PUBLIC'
}else if(nowTag[1] == hideTag){
sendvisi = 'PRIVATE'
function saveSettingsPanel() {
var apiUrl = $('#apiUrl').val()
if (apiUrl.length > 0 && !apiUrl.endsWith('/')) {
apiUrl += '/'
}
}
const data = {
content: base64String,
visibility: sendvisi,
filename: new_name,
type: file.type
};
window.MemosApi.uploadAttachmentOrResource(
info,
data,
function (resp) {
const entity = (resp && resp.resource) || resp
if (entity && entity.name) {
relistNow.push({
name: entity.name,
filename: entity.filename || new_name,
createTime: entity.createTime,
type: entity.type
var apiTokens = $('#apiTokens').val()
var customSettings = buildCustomSettingsPayload()
if (!apiUrl && !apiTokens) {
chrome.storage.sync.set(customSettings, function () {
$.message({ message: msg('saveSuccess') })
$('#blog_info').slideUp(200)
})
chrome.storage.sync.set(
{
open_action: '',
open_content: '',
resourceIdList: relistNow
},
function () {
$.message({ message: msg('picSuccess') })
}
)
return
}
chrome.storage.sync.set(
{
open_action: '',
open_content: '',
resourceIdList: []
},
function () {
$.message({ message: msg('picFailed') })
}
)
},
function () {
$.message({ message: msg('picFailed') })
}
)
}else {
$.message({
message: msg('placeApiUrl')
})
}
});
}
$('#saveKey').click(function () {
var apiUrl = $('#apiUrl').val()
if (apiUrl.length > 0 && !apiUrl.endsWith('/')) {
apiUrl += '/';
}
var apiTokens = $('#apiTokens').val()
window.MemosApi.authWithFallback(apiUrl, apiTokens, function (auth) {
window.MemosApiModern.authWithFallback(apiUrl, apiTokens, function (auth) {
if (!auth || auth.userId == null) {
$.message({ message: msg('invalidToken') })
return
}
chrome.storage.sync.set(
{
Object.assign({}, customSettings, {
apiUrl: apiUrl,
apiTokens: apiTokens,
userid: auth.userId,
memoUiPath: auth.uiPath || 'memos',
apiFlavor: ''
},
}),
function () {
$.message({ message: msg('saveSuccess') })
$('#blog_info').hide()
// Auto-detect API flavor once; keep default behavior when unknown.
if (window.MemosApiV023 && typeof window.MemosApiV023.probeApiFlavor === 'function') {
window.MemosApiV023.probeApiFlavor(apiUrl, apiTokens, function (res) {
if (window.MemosApiAdapter && typeof window.MemosApiAdapter.probeFlavor === 'function') {
window.MemosApiAdapter.probeFlavor(apiUrl, apiTokens, function (res) {
const flavor = res && res.flavor ? res.flavor : ''
const normalized = flavor === 'v020' || flavor === 'v021' ? 'v1' : flavor
if (normalized === 'v1' || normalized === 'v023' || normalized === 'modern') {
chrome.storage.sync.set({ apiFlavor: normalized })
if (window.MemosApiAdapter.KNOWN_FLAVORS.indexOf(flavor) !== -1) {
chrome.storage.sync.set({ apiFlavor: flavor })
}
})
}
}
)
})
});
}
$('#saveSettings').click(function () {
saveSettingsPanel()
})
$('#opensite').click(function () {
get_info(function (info) {
@@ -957,9 +887,8 @@ $('#opensite').click(function () {
$('#tags').click(function () {
get_info(function (info) {
if (info.apiUrl) {
var parent = `users/${info.userid}`;
// 从最近的1000条memo中获取tags,因此不保证获取能全部的
var tagDom = "";
const adapter = getApiAdapter(info)
const renderTags = function (tags) {
const uniTags = [...new Set((Array.isArray(tags) ? tags : []).filter(Boolean))]
@@ -970,62 +899,9 @@ $('#tags').click(function () {
$("#taglist").html(tagDom).slideToggle(500)
}
const onTagsData = function (data) {
const memos = window.MemosApi.extractMemosListFromResponse(data)
const allTags = memos.flatMap(function (memo) {
if (!memo) return []
// v0.23 response may include `tags: []` while actual tags live in `memo.property.tags`.
// So when v0.23 flavor is detected, always use the compat extractor first.
if (isV023Flavor(info)) return window.MemosApiV023.extractTagsFromMemo(memo)
if (Array.isArray(memo.tags) && memo.tags.length > 0) return memo.tags
if (Array.isArray(memo.tagList) && memo.tagList.length > 0) return memo.tagList
if (memo.property && Array.isArray(memo.property.tags) && memo.property.tags.length > 0) {
return memo.property.tags
}
return []
adapter.listTags(renderTags, function () {
$.message({ message: msg('placeApiUrl') })
})
const uniTags = [...new Set(allTags.filter(Boolean))]
renderTags(uniTags)
}
if (isV1Flavor(info)) {
window.MemosApiV1.getTagSuggestion(
info,
function (tags) {
renderTags(Array.isArray(tags) ? tags : [])
},
function () {
$.message({ message: msg('placeApiUrl') })
}
)
} else if (isV023Flavor(info)) {
const filterExpr = window.MemosApiV023.buildFilter({
rowStatus: 'NORMAL',
creator: 'users/' + info.userid
})
window.MemosApiV023.listMemos(
info,
{
pageSize: 1000,
filterExpr: filterExpr
},
onTagsData,
function () {
$.message({ message: msg('placeApiUrl') })
}
)
} else {
window.MemosApi.fetchMemosWithFallback(
info,
'?pageSize=1000',
onTagsData,
function () {
$.message({ message: msg('placeApiUrl') })
}
)
}
} else {
$.message({
message: msg('placeApiUrl')
@@ -1036,10 +912,10 @@ $('#tags').click(function () {
$(document).on("click","#hideTag",function () {
$('#taghide').slideToggle(500)
$('#hideInput').trigger('focus')
})
$('#saveTag').click(function () {
// 保存数据
chrome.storage.sync.set(
{
hidetag: $('#hideInput').val(),
@@ -1071,32 +947,15 @@ $(document).on("click",".item-lock",function () {
$('#search').click(function () {
get_info(function (info) {
const pattern = $("textarea[name=text]").val()
var parent = `users/${info.userid}`;
const patternLiteral = JSON.stringify(String(pattern || ''))
var filter = "?filter=" + encodeURIComponent(`visibility in ["PUBLIC","PROTECTED"] && content.contains(${patternLiteral})`);
if (info.status) {
$("#randomlist").html('').hide()
var searchDom = ""
if(pattern){
const runSearch = isV023Flavor(info)
? function (onOk, onFail) {
const filterExpr = window.MemosApiV023.buildFilter({
visibilities: ['PUBLIC', 'PROTECTED'],
contentSearch: String(pattern)
})
window.MemosApiV023.listMemos(info, { pageSize: 1000, filterExpr: filterExpr }, onOk, onFail)
}
: isV1Flavor(info)
? function (onOk, onFail) {
window.MemosApiV1.listMemos(info, { limit: 1000, rowStatus: 'NORMAL', contentSearch: String(pattern) }, onOk, onFail)
}
: function (onOk, onFail) {
window.MemosApi.fetchMemosWithFallback(info, filter, onOk, onFail)
}
const adapter = getApiAdapter(info)
runSearch(
function (data) {
let searchData = window.MemosApi.extractMemosListFromResponse(data)
adapter.searchMemos(
pattern,
function (searchData) {
if(searchData.length == 0){
$.message({
message: msg('searchNone')
@@ -1121,7 +980,7 @@ $('#search').click(function () {
continue
}
if(restype == 'image'){
if (isV1Flavor(info)) {
if (adapter.needsAuthenticatedImagePreview()) {
searchDom += '<img class="random-image" data-auth-src="'+resLink+'"/>'
} else {
searchDom += '<img class="random-image" src="'+resLink+'"/>'
@@ -1158,26 +1017,12 @@ $('#search').click(function () {
$('#random').click(function () {
get_info(function (info) {
var parent = `users/${info.userid}`;
if (info.status) {
$("#randomlist").html('').hide()
const runRandom = isV023Flavor(info)
? function (onOk, onFail) {
const filterExpr = window.MemosApiV023.buildFilter({ visibilities: ['PUBLIC', 'PROTECTED'] })
window.MemosApiV023.listMemos(info, { pageSize: 1000, filterExpr: filterExpr }, onOk, onFail)
}
: isV1Flavor(info)
? function (onOk, onFail) {
window.MemosApiV1.listMemos(info, { limit: 1000, rowStatus: 'NORMAL' }, onOk, onFail)
}
: function (onOk, onFail) {
const filter = "?filter=" + encodeURIComponent(`visibility in ["PUBLIC","PROTECTED"]`);
window.MemosApi.fetchMemosWithFallback(info, filter, onOk, onFail)
}
const adapter = getApiAdapter(info)
runRandom(
function (data) {
const memos = window.MemosApi.extractMemosListFromResponse(data)
adapter.listRandomMemos(
function (memos) {
let randomNum = Math.floor(Math.random() * (memos.length));
var randomData = memos[randomNum]
randDom(randomData)
@@ -1196,6 +1041,7 @@ $('#random').click(function () {
function randDom(randomData){
get_info(function (info) {
const adapter = getApiAdapter(info)
var memosID = getMemoUid(randomData)
var timeText = memoFromNow(randomData)
var randomDom = '<div class="random-item"><div class="random-time"><span id="random-link" data-uid="'+memosID+'"><svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M864 640a32 32 0 0 1 64 0v224.096A63.936 63.936 0 0 1 864.096 928H159.904A63.936 63.936 0 0 1 96 864.096V159.904C96 124.608 124.64 96 159.904 96H384a32 32 0 0 1 0 64H192.064A31.904 31.904 0 0 0 160 192.064v639.872A31.904 31.904 0 0 0 192.064 864h639.872A31.904 31.904 0 0 0 864 831.936V640zm-485.184 52.48a31.84 31.84 0 0 1-45.12-.128 31.808 31.808 0 0 1-.128-45.12L815.04 166.048l-176.128.736a31.392 31.392 0 0 1-31.584-31.744 32.32 32.32 0 0 1 31.84-32l255.232-1.056a31.36 31.36 0 0 1 31.584 31.584L924.928 388.8a32.32 32.32 0 0 1-32 31.84 31.392 31.392 0 0 1-31.712-31.584l.736-179.392L378.816 692.48z" fill="#666" data-spm-anchor-id="a313x.7781069.0.i12" class="selected"/></svg></span><span id="random-delete" data-uid="'+memosID+'" data-name="'+randomData.name+'" data-id="'+(randomData && randomData.id != null ? randomData.id : '')+'"><svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M224 322.6h576c16.6 0 30-13.4 30-30s-13.4-30-30-30H224c-16.6 0-30 13.4-30 30 0 16.5 13.5 30 30 30zm66.1-144.2h443.8c16.6 0 30-13.4 30-30s-13.4-30-30-30H290.1c-16.6 0-30 13.4-30 30s13.4 30 30 30zm339.5 435.5H394.4c-16.6 0-30 13.4-30 30s13.4 30 30 30h235.2c16.6 0 30-13.4 30-30s-13.4-30-30-30z" fill="#666"/><path d="M850.3 403.9H173.7c-33 0-60 27-60 60v360c0 33 27 60 60 60h676.6c33 0 60-27 60-60v-360c0-33-27-60-60-60zm-.1 419.8l-.1.1H173.9l-.1-.1V464l.1-.1h676.2l.1.1v359.7z" fill="#666"/></svg></span>'+timeText+'</div><div class="random-content">'+(randomData && randomData.content ? randomData.content : '').replace(/!\[.*?\]\((.*?)\)/g,' <img class="random-image" src="$1"/> ').replace(/\[(.*?)\]\((.*?)\)/g,' <a href="$2" target="_blank">$1</a> ')+'</div>'
@@ -1214,7 +1060,7 @@ function randDom(randomData){
continue
}
if(restype == 'image'){
if (isV1Flavor(info)) {
if (adapter.needsAuthenticatedImagePreview()) {
randomDom += '<img class="random-image" data-auth-src="'+resLink+'"/>'
} else {
randomDom += '<img class="random-image" src="'+resLink+'"/>'
@@ -1242,16 +1088,12 @@ $(document).on("click","#random-link",function () {
$(document).on("click","#random-delete",function () {
get_info(function (info) {
// var memoUid = $("#random-delete").data('uid');
var memosName = $("#random-delete").data('name');
var memoId = $("#random-delete").data('id');
// v0.20/v0.21: archive memo via API v1 PATCH /api/v1/memo/:id
if (isV1Flavor(info) && memoId) {
window.MemosApiV1.patchMemo(
info,
memoId,
{ rowStatus: 'ARCHIVED' },
const adapter = getApiAdapter(info)
adapter.archiveMemo(
{ name: memosName, id: memoId },
function () {
$("#randomlist").html('').hide()
$.message({ message: msg('archiveSuccess') })
@@ -1260,31 +1102,6 @@ get_info(function (info) {
$.message({ message: msg('archiveFailed') })
}
)
return
}
var deleteUrl = info.apiUrl+'api/v1/'+memosName
$.ajax({
url:deleteUrl,
type:"PATCH",
data:JSON.stringify({
// 'uid': memoUid,
'state': "ARCHIVED"
}),
contentType:"application/json",
dataType:"json",
headers : {'Authorization':'Bearer ' + info.apiTokens},
success: function(result){
$("#randomlist").html('').hide()
$.message({
message: msg('archiveSuccess')
})
},error:function(err){//清空open_action(打开时候进行的操作),同时清空open_content
$.message({
message: msg('archiveFailed')
})
}
})
})
})
@@ -1345,8 +1162,13 @@ $('#blog_info_edit').click(function () {
$('#content_submit_text').click(function () {
var contentVal = $("textarea[name=text]").val()
if(contentVal){
sendText()
var contentToSend = resolveSendContent(
contentVal,
relistNow,
$('#attachmentOnlyDefaultText').val()
)
if(contentToSend){
sendText(contentToSend)
}else{
$.message({
message: msg('placeContent')
@@ -1358,16 +1180,9 @@ function getOne(memosId){
get_info(function (info) {
if (info.apiUrl) {
$("#randomlist").html('').hide()
var getUrl = isV1Flavor(info) ? info.apiUrl+'api/v1/memo/'+memosId : info.apiUrl+'api/v1/'+memosId
$.ajax({
url:getUrl,
type:"GET",
contentType:"application/json",
dataType:"json",
headers : {'Authorization':'Bearer ' + info.apiTokens},
success: function(data){
randDom(data)
}
const adapter = getApiAdapter(info)
adapter.getMemo(memosId, function (memoEntity) {
randDom(memoEntity)
})
} else {
$.message({
@@ -1377,17 +1192,27 @@ function getOne(memosId){
})
}
function sendText() {
function sendText(preparedContent) {
get_info(function (info) {
if (info.status) {
$.message({
message: msg('memoUploading')
})
//$("#content_submit_text").attr('disabled','disabled');
let content = $("textarea[name=text]").val()
let content = resolveSendContent(
typeof preparedContent === 'string' ? preparedContent : $("textarea[name=text]").val(),
info.resourceIdList,
info.attachmentOnlyDefaultText
)
if (!content) {
$.message({
message: msg('placeContent')
})
return
}
var hideTag = info.hidetag
var showTag = info.showtag
var nowTag = $("textarea[name=text]").val().match(/(#[^\s#]+)/)
var nowTag = content.match(/(#[^\s#]+)/)
var sendvisi = info.memo_lock || ''
if(nowTag){
if(nowTag[1] == showTag){
@@ -1397,32 +1222,12 @@ function sendText() {
}
}
// Memos v0.20/v0.21: use /api/v1/memo and bind resources by numeric IDs.
if (isV1Flavor(info)) {
const items = Array.isArray(info.resourceIdList) ? info.resourceIdList : []
const resourceIdList = items
.map(function (r) {
if (!r) return null
if (typeof r.id === 'number' && Number.isFinite(r.id)) return Math.floor(r.id)
if (typeof r.id === 'string' && r.id.trim() !== '' && !Number.isNaN(Number(r.id))) {
return Math.floor(Number(r.id))
}
// Some versions store name as resources/{id}.
const n = typeof r.name === 'string' ? r.name : ''
const tail = n ? n.split('/').pop() : ''
if (tail && !Number.isNaN(Number(tail))) return Math.floor(Number(tail))
return null
})
.filter(function (x) {
return x != null && Number.isFinite(x)
})
window.MemosApiV1.createMemo(
info,
const adapter = getApiAdapter(info)
adapter.createMemo(
{
content: content,
visibility: sendvisi,
resourceIdList: resourceIdList
resourceIdList: info.resourceIdList
},
function (data) {
chrome.storage.sync.set(
@@ -1438,65 +1243,13 @@ function sendText() {
},
function () {
chrome.storage.sync.set(
{ open_action: '', open_content: '' },
{ open_action: '', open_content: '', resourceIdList: [] },
function () {
$.message({ message: msg('memoFailed') })
}
)
}
)
return
}
$.ajax({
url:info.apiUrl+'api/v1/memos',
type:"POST",
data:JSON.stringify({
'content': content,
'visibility': sendvisi
}),
contentType:"application/json",
dataType:"json",
headers : {'Authorization':'Bearer ' + info.apiTokens},
success: function(data){
if(info.resourceIdList.length > 0 ){
//匹配图片
window.MemosApi.patchMemoWithAttachmentsOrResources(
info,
data.name,
info.resourceIdList,
function () {
getOne(data.name)
},
function () {
getOne(data.name)
}
)
}else{
getOne(data.name)
}
chrome.storage.sync.set(
{ open_action: '', open_content: '',resourceIdList:[]},
function () {
$.message({
message: msg('memoSuccess')
})
//$("#content_submit_text").removeAttr('disabled');
$("textarea[name=text]").val('')
relistNow = []
renderUploadList(relistNow)
}
)
},error:function(err){//清空open_action(打开时候进行的操作),同时清空open_content
chrome.storage.sync.set(
{ open_action: '', open_content: '',resourceIdList:[] },
function () {
$.message({
message: msg('memoFailed')
})
}
)},
})
} else {
$.message({
message: msg('placeApiUrl')
View File
+1 -2
View File
@@ -2,8 +2,7 @@
"manifest_version": 3,
"name": "__MSG_extName__",
"default_locale": "en",
"version": "2026.03.10",
"version_name": "Supports 0.18.0 to the latest version",
"version": "2026.04.23",
"action": {
"default_popup": "popup.html",
"default_icon": "assets/logo_24x24.png",
+41 -14
View File
@@ -13,22 +13,34 @@
<body class="body">
<div class="title" id="opensite">MEMOS</div>
<div id="lang_switcher" class="lang-switcher">
<select id="langSelect" class="lang-select" aria-label="Language" title="">
<option id="langOptionAuto" value="auto"></option>
<option id="langOptionEn" value="en"></option>
<option id="langOptionZhCN" value="zh_CN"></option>
<option id="langOptionJa" value="ja"></option>
<option id="langOptionKo" value="ko"></option>
</select>
<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="">
<div id="blog_info" class="settings-panel">
<div class="settings-section">
<div id="settingsConnectionTitle" class="settings-section-title"></div>
<div class="settings-section-desc" id="settingsConnectionDesc"></div>
<input
id="apiUrl"
class="inputer"
class="inputer settings-input"
name="apiUrl"
type="text"
value=""
@@ -38,7 +50,7 @@
/>
<input
id="apiTokens"
class="inputer"
class="inputer settings-input"
name="apiTokens"
type="text"
value=""
@@ -46,9 +58,24 @@
placeholder=""
required
/>
<span id="saveKey" class="action-btn confirm-btn"></span>
<div id="supportedMemosVersion" class="upload-list-title"></div>
</div>
<div class="settings-section">
<div id="settingsPostingTitle" class="settings-section-title"></div>
<div class="settings-section-desc" id="settingsPostingDesc"></div>
<textarea
id="attachmentOnlyDefaultText"
class="inputer settings-input settings-textarea"
name="attachmentOnlyDefaultText"
rows="2"
maxlength="500"
placeholder=""
></textarea>
</div>
<div class="settings-actions">
<span id="saveSettings" class="action-btn confirm-btn"></span>
</div>
</div>
<div class="memo-editor">
<div class="memo-editor-header">
@@ -145,7 +172,6 @@
/>
<span id="saveTag" class="action-btn confirm-btn"></span>
</div>
<div class="" id="randomlist"></div>
<input type="file" id="inFile" style="display:none;">
@@ -159,9 +185,10 @@
<script src="../js/ko.js"></script>
<script src="../js/relativeTime.js"></script>
<script src="../js/view-image.js"></script>
<script src="../js/compat/memosApi.v024.js"></script>
<script src="../js/compat/memosApi.v1.js"></script>
<script src="../js/compat/memosApi.modern.js"></script>
<script src="../js/compat/memosApi.v020-v021.js"></script>
<script src="../js/compat/memosApi.v023.js"></script>
<script src="../js/compat/memosApi.adapter.js"></script>
<script src="../js/oper.js"></script>
</body>
</html>
+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: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 844 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

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))
}
+243
View File
@@ -0,0 +1,243 @@
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 tryOpenActionPopup(tab) {
try {
if (!chrome.browserAction || typeof chrome.browserAction.openPopup !== 'function') return
const windowId = tab && typeof tab.windowId === 'number' ? tab.windowId : undefined
const open = () => {
try {
if (typeof windowId === 'number') {
chrome.browserAction.openPopup({ windowId }, () => void chrome.runtime.lastError)
} else {
chrome.browserAction.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)}}}));
+172
View File
@@ -0,0 +1,172 @@
;(function () {
const STYLE_ID = 'view-image-style'
const STYLE_TEXT = `
.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);color:#fff;font-size:20px;line-height:1}
.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);cursor:zoom-out;margin:0}
.view-image-container{height:0;flex:1;display:flex;align-items:center;justify-content:center}
.view-image-lead{position:relative;z-index:1;display:flex;align-items:center;justify-content:center;max-width:100%;max-height:100%}
.view-image-lead img{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:absolute;bottom:5%;left:1rem;right:1rem;display:flex;justify-content:space-between;align-content:center;color:#fff;max-width:600px;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}
`
function ensureStyle() {
if (document.getElementById(STYLE_ID)) return
const style = document.createElement('style')
style.id = STYLE_ID
style.textContent = STYLE_TEXT
document.head.appendChild(style)
}
function createButton(className, label, ariaLabel) {
const button = document.createElement('button')
button.type = 'button'
button.className = className
button.setAttribute('aria-label', ariaLabel)
button.textContent = label
return button
}
window.ViewImage = new (function () {
const api = this
api.target = '[view-image] img'
api.listener = function (event) {
if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return
const selector = String(
api.target
.split(',')
.map(function (item) {
return item.trim() + ':not([no-view])'
})
.join(',')
)
const current = event.target.closest(selector)
if (!current) return
const root = current.closest('[view-image]') || document.body
const sources = Array.from(root.querySelectorAll(selector)).map(function (item) {
return item.href || item.src
})
api.display(sources, current.href || current.src)
event.stopPropagation()
event.preventDefault()
}
api.init = function (target) {
if (target) api.target = target
document.removeEventListener('click', api.listener, false)
document.addEventListener('click', api.listener, false)
}
api.display = function (sources, currentSrc) {
ensureStyle()
let currentIndex = Math.max(0, sources.indexOf(currentSrc))
const overlay = document.createElement('div')
overlay.className = 'view-image'
const container = document.createElement('div')
container.className = 'view-image-container'
const lead = document.createElement('div')
lead.className = 'view-image-lead'
const loading = document.createElement('div')
loading.className = 'view-image-loading'
const backdrop = document.createElement('div')
backdrop.className = 'view-image-close view-image-close__full'
container.appendChild(lead)
container.appendChild(loading)
container.appendChild(backdrop)
const tools = document.createElement('div')
tools.className = 'view-image-tools'
const count = document.createElement('div')
count.className = 'view-image-tools__count'
const countText = document.createElement('span')
count.appendChild(countText)
const flips = document.createElement('div')
flips.className = 'view-image-tools__flip'
const prev = createButton('view-image-btn view-image-tools__flip-prev', '', 'Previous image')
const next = createButton('view-image-btn view-image-tools__flip-next', '', 'Next image')
flips.appendChild(prev)
flips.appendChild(next)
const close = createButton('view-image-btn view-image-close', '×', 'Close image viewer')
tools.appendChild(count)
tools.appendChild(flips)
tools.appendChild(close)
overlay.appendChild(container)
overlay.appendChild(tools)
function render() {
countText.textContent = `${currentIndex + 1}/${sources.length}`
lead.className = 'view-image-lead view-image-lead__out'
window.setTimeout(function () {
const image = document.createElement('img')
image.alt = 'ViewImage'
image.setAttribute('no-view', '')
image.addEventListener('load', function () {
window.setTimeout(function () {
lead.replaceChildren(image)
lead.className = 'view-image-lead view-image-lead__in'
}, 100)
})
image.src = sources[currentIndex]
}, 300)
}
function closeViewer() {
window.removeEventListener('keydown', onKeyDown)
overlay.classList.add('view-image__out')
window.setTimeout(function () {
overlay.remove()
}, 290)
}
function onKeyDown(event) {
if (event.key === 'Escape') closeViewer()
if (event.key === 'ArrowLeft') prev.click()
if (event.key === 'ArrowRight') next.click()
}
prev.addEventListener('click', function () {
currentIndex = currentIndex === 0 ? sources.length - 1 : currentIndex - 1
render()
})
next.addEventListener('click', function () {
currentIndex = currentIndex === sources.length - 1 ? 0 : currentIndex + 1
render()
})
backdrop.addEventListener('click', closeViewer)
close.addEventListener('click', closeViewer)
document.body.appendChild(overlay)
window.addEventListener('keydown', onKeyDown)
render()
}
})()
})()
+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}));
+46
View File
@@ -0,0 +1,46 @@
{
"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",
"data_collection_permissions": {
"required": false
}
}
},
"icons": {
"128": "assets/logo-128.png",
"16": "assets/logo-16.png",
"48": "assets/logo-48.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>