From 88c92652c3bea5fc39b42d2e9055b1bfadd4eb13 Mon Sep 17 00:00:00 2001
From: jonny
Date: Thu, 23 Apr 2026 10:46:27 +0800
Subject: [PATCH] Add Edge extension and update release workflow
Add a complete Edge extension under edge/ (manifest, popup, CSS, JS, locales, assets, LICENSE) and a change.log. Update README with Edge/mobile notes and simplify content. Update GitHub Actions workflow to read edge/manifest.json version, package Edge builds, and include Edge asset in the release summary (also bump default release_tag). Minor update to firefox/js/oper.js and include .DS_Store change.
---
.DS_Store | Bin 6148 -> 6148 bytes
.github/workflows/package-extensions.yml | 14 +-
README.md | 85 +-
change.log | 71 ++
edge/LICENSE | 21 +
edge/_locales/en/messages.json | 188 ++++
edge/_locales/ja/messages.json | 188 ++++
edge/_locales/ko/messages.json | 188 ++++
edge/_locales/zh_CN/messages.json | 188 ++++
edge/assets/logo.png | Bin 0 -> 6158 bytes
edge/assets/logo_24x24.png | Bin 0 -> 1228 bytes
edge/css/main.css | 658 +++++++++++
edge/js/background.js | 218 ++++
edge/js/compat/memosApi.adapter.js | 521 +++++++++
edge/js/compat/memosApi.modern.js | 512 +++++++++
edge/js/compat/memosApi.v020-v021.js | 286 +++++
edge/js/compat/memosApi.v023.js | 119 ++
edge/js/dayjs.min.js | 1 +
edge/js/i18n.js | 210 ++++
edge/js/ja.js | 1 +
edge/js/jquery.min.js | 2 +
edge/js/ko.js | 1 +
edge/js/message.js | 65 ++
edge/js/oper.js | 1259 ++++++++++++++++++++++
edge/js/relativeTime.js | 1 +
edge/js/view-image.js | 12 +
edge/js/zh-cn.js | 1 +
edge/manifest.json | 39 +
edge/popup.html | 194 ++++
firefox/js/oper.js | 31 +-
30 files changed, 4995 insertions(+), 79 deletions(-)
create mode 100644 change.log
create mode 100644 edge/LICENSE
create mode 100644 edge/_locales/en/messages.json
create mode 100644 edge/_locales/ja/messages.json
create mode 100644 edge/_locales/ko/messages.json
create mode 100644 edge/_locales/zh_CN/messages.json
create mode 100644 edge/assets/logo.png
create mode 100644 edge/assets/logo_24x24.png
create mode 100644 edge/css/main.css
create mode 100644 edge/js/background.js
create mode 100644 edge/js/compat/memosApi.adapter.js
create mode 100644 edge/js/compat/memosApi.modern.js
create mode 100644 edge/js/compat/memosApi.v020-v021.js
create mode 100644 edge/js/compat/memosApi.v023.js
create mode 100644 edge/js/dayjs.min.js
create mode 100644 edge/js/i18n.js
create mode 100644 edge/js/ja.js
create mode 100644 edge/js/jquery.min.js
create mode 100644 edge/js/ko.js
create mode 100644 edge/js/message.js
create mode 100644 edge/js/oper.js
create mode 100644 edge/js/relativeTime.js
create mode 100644 edge/js/view-image.js
create mode 100644 edge/js/zh-cn.js
create mode 100644 edge/manifest.json
create mode 100644 edge/popup.html
diff --git a/.DS_Store b/.DS_Store
index aa13abef4c3e53901ca9fd42820dca98fc8d7ca7..4a054aa27617d4356e8a1fcfcafe9b3438895a07 100644
GIT binary patch
delta 18
acmZoMXfc?uY~#i-_K6Mro4GlD@&f=#kOxcv
delta 20
ccmZoMXfc?ujFEBU#xVAY4P2YqIsWnk08YaPNdN!<
diff --git a/.github/workflows/package-extensions.yml b/.github/workflows/package-extensions.yml
index 37d4e46..9993fb0 100644
--- a/.github/workflows/package-extensions.yml
+++ b/.github/workflows/package-extensions.yml
@@ -9,7 +9,7 @@ on:
release_tag:
description: Release tag used for direct asset upload, for example v2026.04.23
required: true
- default: 'v2026.04.23'
+ default: 'v2026.04.24'
push:
tags:
- 'v*'
@@ -48,8 +48,15 @@ jobs:
print(json.load(fp)['version'])
PY
)
+ edge_version=$(python - <<'PY'
+ import json
+ with open('edge/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"
+ echo "edge_version=$edge_version" >> "$GITHUB_OUTPUT"
- name: Build package files
run: |
@@ -59,6 +66,10 @@ jobs:
zip -qr "../dist/release/memos-bber-chrome-${{ steps.versions.outputs.chrome_version }}.zip" .
popd >/dev/null
+ pushd edge >/dev/null
+ zip -qr "../dist/release/memos-bber-edge-${{ steps.versions.outputs.edge_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
@@ -77,4 +88,5 @@ jobs:
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 "- Edge: https://github.com/${{ github.repository }}/releases/download/$tag/memos-bber-edge-${{ steps.versions.outputs.edge_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"
\ No newline at end of file
diff --git a/README.md b/README.md
index 5e61e85..b646dc3 100644
--- a/README.md
+++ b/README.md
@@ -1,84 +1,19 @@
-## 说明
+一款通过浏览器插件发布 [Memos](https://usememos.com/) 的插件。基于 iSpeak-bber 修改,原作者为 [DreamyTZK](https://www.antmoe.com/)。
+
+## 在线商店安装
- Chrome 应用商店:https://chrome.google.com/webstore/detail/memos-bber/cbhjebjfccgchgbmfbobjmebjjckgofe/
- (审核中)FireFox 应用商店: https://addons.mozilla.org/zh-CN/firefox/addon/memos-bber/
-- edge 应用商店: 等待开发
+- Edge: 等待上架
-一个通过浏览器插件发布 [Memos](https://usememos.com/) 的插件。基于 iSpeak-bber 修改,原作者为 [DreamyTZK](https://www.antmoe.com/)。
+## 移动端
+Android
+- 可直接用 chrome 扩展: https://github.com/uazo/cromite/releases
+- firefox 手机版
+- edge 手机版
-## 更新日志
-- 20260422 调整发送设置,支持仅发送附件
-#### 20260421 更新匹配 0.27.x
-- 20260325 优化语言按钮样式
-- 20260323 优化中文显示效果
-- 20260322 适配移动端竖屏窗口
-- 20260310 记忆拖拽窗口大小,移除拖拽窗口动画
-- 20260309 右键发送选中文本保持原格式,增加全屏和窗口放大功能
-#### 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,其它版本不支持
-
-
-
-点击展开/折叠内容
-2024.07.21 不兼容更新,已匹配 v0.22.3
-
-2024.06.15 感谢好心人 [PR#44](https://github.com/lmm214/memos-bber/pull/44)
-
-2024.05.20 更新匹配至 v0.22
-
-2023.09.19 不兼容更新匹配 Memos v0.15 的 `Access tokens` 模式。
-
-
-
-2023.07.16 支持 Memos v0.14.0 `api/v1`,同时兼容之前的 api。
-
-2023.04.29 右键菜单的一系列改进,感谢 @EZForever 的 PR [#17](https://github.com/lmm214/memos-bber/pull/17)
-
-2023.04.09 匹配 v0.12.0 附件链接由 filename 改为 publicId 。
-
-2023.03.25 右键菜单发送文本改为“追加模式”(不刷新已打开页面、不刷新已打开页面、不刷新已打开页面时);新增多语言支持(en、zh-cn)。
-
-2023.03.19 上传图片重命名精确的秒;打开插件时 focus 输入框(配合右键发送文本到扩展,设置快捷键打开扩展,按下 Ctrl+Enter 记下)。
-
-2023.03.10 修复发布后调用最新一条 Memos。
-
-2023.03.09 新增右键发送文本至 Memos 输入框。
-
-
-
-2023.03.05 新增指定标签“仅自己可见”或“所有人可见”;图片上传文件名添加时间戳。
-
-2023.02.26 更改 Memos 可见范围按钮样式。支持 Ctrl/Meta + Enter 记下。点击标题跳转主站。
-
-2023.02.25 修复 v0.11.0 下随机按钮失效。(api amount 失效导致,换用 stats 获取总条数)
-
-
-
-2023.02.07 新增发布后显示最新一条 Memo ,具体一条新增归档按钮。
-
-2023.02.06 新增搜索按钮;新增图片灯箱。
-
-
-
-2023.02.05 随机 Memos 支持指定标签 (算是彩蛋:标签列表点开,输入框内有且只有1个标签时,点击随机按钮)
-
-2023.02.04 新增随机 Memos 按钮,随时唤醒记忆。
-
-2022.11.15 新增插入文件图片按钮,尝试修复首次安装需要点一下小锁。
-
-2022.11.13 新增插入 todo 按钮。
-
-2022.11.8 支持拖拽上传附件(一个个传)。
-
-2022.10.24 添加 visiblity 发送设置。
-
-
\ No newline at end of file
diff --git a/change.log b/change.log
new file mode 100644
index 0000000..139d785
--- /dev/null
+++ b/change.log
@@ -0,0 +1,71 @@
+## 更新日志
+- 20260423 优化 firefox 抖动问题和支持手机版,支持 edge 浏览器扩展
+- 20260422 调整发送设置,支持仅发送附件
+#### 20260421 更新匹配 0.27.x
+- 20260325 优化语言按钮样式
+- 20260323 优化中文显示效果
+- 20260322 适配移动端竖屏窗口
+- 20260310 记忆拖拽窗口大小,移除拖拽窗口动画
+- 20260309 右键发送选中文本保持原格式,增加全屏和窗口放大功能
+#### 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
+
+
+
+点击展开/折叠内容
+2024.07.21 不兼容更新,已匹配 v0.22.3
+
+2024.06.15 感谢好心人 [PR#44](https://github.com/lmm214/memos-bber/pull/44)
+
+2024.05.20 更新匹配至 v0.22
+
+2023.09.19 不兼容更新匹配 Memos v0.15 的 `Access tokens` 模式。
+
+
+
+2023.07.16 支持 Memos v0.14.0 `api/v1`,同时兼容之前的 api。
+
+2023.04.29 右键菜单的一系列改进,感谢 @EZForever 的 PR [#17](https://github.com/lmm214/memos-bber/pull/17)
+
+2023.04.09 匹配 v0.12.0 附件链接由 filename 改为 publicId 。
+
+2023.03.25 右键菜单发送文本改为“追加模式”(不刷新已打开页面、不刷新已打开页面、不刷新已打开页面时);新增多语言支持(en、zh-cn)。
+
+2023.03.19 上传图片重命名精确的秒;打开插件时 focus 输入框(配合右键发送文本到扩展,设置快捷键打开扩展,按下 Ctrl+Enter 记下)。
+
+2023.03.10 修复发布后调用最新一条 Memos。
+
+2023.03.09 新增右键发送文本至 Memos 输入框。
+
+
+
+2023.03.05 新增指定标签“仅自己可见”或“所有人可见”;图片上传文件名添加时间戳。
+
+2023.02.26 更改 Memos 可见范围按钮样式。支持 Ctrl/Meta + Enter 记下。点击标题跳转主站。
+
+2023.02.25 修复 v0.11.0 下随机按钮失效。(api amount 失效导致,换用 stats 获取总条数)
+
+
+
+2023.02.07 新增发布后显示最新一条 Memo ,具体一条新增归档按钮。
+
+2023.02.06 新增搜索按钮;新增图片灯箱。
+
+
+
+2023.02.05 随机 Memos 支持指定标签 (算是彩蛋:标签列表点开,输入框内有且只有1个标签时,点击随机按钮)
+
+2023.02.04 新增随机 Memos 按钮,随时唤醒记忆。
+
+2022.11.15 新增插入文件图片按钮,尝试修复首次安装需要点一下小锁。
+
+2022.11.13 新增插入 todo 按钮。
+
+2022.11.8 支持拖拽上传附件(一个个传)。
+
+2022.10.24 添加 visiblity 发送设置。
+
+
\ No newline at end of file
diff --git a/edge/LICENSE b/edge/LICENSE
new file mode 100644
index 0000000..32fea1a
--- /dev/null
+++ b/edge/LICENSE
@@ -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.
diff --git a/edge/_locales/en/messages.json b/edge/_locales/en/messages.json
new file mode 100644
index 0000000..7404ca3
--- /dev/null
+++ b/edge/_locales/en/messages.json
@@ -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)"
+ }
+}
\ No newline at end of file
diff --git a/edge/_locales/ja/messages.json b/edge/_locales/ja/messages.json
new file mode 100644
index 0000000..d86008b
--- /dev/null
+++ b/edge/_locales/ja/messages.json
@@ -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": "ドラッグで拡大/縮小(最小:初期サイズ)"
+ }
+}
\ No newline at end of file
diff --git a/edge/_locales/ko/messages.json b/edge/_locales/ko/messages.json
new file mode 100644
index 0000000..8e20cac
--- /dev/null
+++ b/edge/_locales/ko/messages.json
@@ -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": "드래그로 확대/축소(최소: 기본 크기)"
+ }
+}
\ No newline at end of file
diff --git a/edge/_locales/zh_CN/messages.json b/edge/_locales/zh_CN/messages.json
new file mode 100644
index 0000000..97974b8
--- /dev/null
+++ b/edge/_locales/zh_CN/messages.json
@@ -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": "拖拽缩放编辑框(最小为默认大小)"
+ }
+}
\ No newline at end of file
diff --git a/edge/assets/logo.png b/edge/assets/logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..4b880cdf676e174d8e135654e54cb38a5a34f0a7
GIT binary patch
literal 6158
zcmd6rF$sY0TC1t=~QV(gLE@Q!qJEV3P?IY`YVia
z28`|Jbv^&YbFOpl`#Sf_&v|zeO^kFXp{!5<06?jyt7-Pn5&tHF4D?TF;PTS{4D6!z
zPz?ZRNF&ERCH}Vq{mpdL0iQoNQmFNC$ib$xrg?={ZHea
zt9q0QWsDA;rGPe^pxLPP5rwkB5<=0rrPpYHDiUV~L}$
zQi+DV>DHnGckk@AnB-@r6xfL-o8GOlg^7uZ9yH#UlX-A9_2wm>uE{(#%LS1bQEp6??Z|eJvvYAGl5$a5sbeR_~o-dV%?|jl2
zP1TTxc*I##6`;;{D|UpxA^sUQ4k137&kop#6p;|
zA+=D$UyPK%1IiO*e;Qac7DRl6a2K}%@4`5M)unulIJbF7p;89
z^{m)-fy0kXCyneSCRBUX3=0`#dzG*Z9jXL1iauZRgIdNaE2^Pa(Dz2vp$g<{h0vXH
z+Raj`2qp5Bd`PM`b)X!y+l!*biRz03)khnut8USgdbafV!iX$61(bz_#p#(7F%TdmB&4pcMi^7X
zwZnONdE{hdva_=nOYeF*nt7Yb6MFgown^yd>h=VZA2;wCnV1SvfN~t!toccFd5Krv
z(KLiG##*ytk{IXTGnTtRv{=A{nS4F5bl3^Ebks@V*>
zthi@wNSo?7Eo6dqK9}Hwe+Rw}Ff%d-;BoW}P#lm5#DmAKX%v~KaMA}+Vuw|IPR
z<#%ida?es)c<1WrZnRfesjBS6E$=|-rpcf#q0j}%_<
z-o@VF_UBv(^&~Jp5qv~Q*yfG`SH8Ri6n#CsCzE
zJW?g)JJ7sHNm4449R^dp7YLSPjsSixM7DAx!K&TA@qy1eC}k!0u-%TBEQCTBk7E=C
zH*n}uV;p;W^WCpMNJJL7Jp>GPQ(UD670Lc&O;^NAC(s6tDH%}nN#b}WQxyUD6VAw4
z7V*(|-k#fOB(gFsCQ#l{2%gdc{36N`a29VUA2OiE${ejl$$K@D*22a
zhGs?FbL_}N*er{$@VobSf?O0;_LK@EKLXbxNUk~*_XH@83)RBcywIOt{CrIF2MJI?
zBB%<9L76tdur)UXm-E8qm5oMaR0BzHF2eWE#pNYwjtYya0RRxb3;6E)^wR134Hkz>
z2}r?p_vP6oj@
zU~?k6h*tb~&+b3tyE35jXj?S}ekCQT`==v>SMtW=WG@tGh`jFIX1BUib5VI%!{oZ~
zxMdv|&H+z=@XB;wxboz!x6Q>#(>9Obwp?jx#=@Nz?=+kwMc@&4JGg@lr?!fZaL|45
z`@{TM#IflC1WX+aAZm`dau`KbiX#?r4we=z^u_kC4d5m_V+-)k`frs3Lf~>-ZAk-bK7kny`01bkIC6vfss^QUXTFLli38QkKf5dw{d0jj
zzWoelZdtl8ggYp0p;GAhUdQy__mywN9zi+mM5
zEbVrf^~Vi;i(^hp((s~2dp;U6`lk^reosz4o`&%pMR-ij9nh4>yhWi_e)yohtyiZf
zBg9qAuXn2C)dOYu4KswsUp7U}j>^R=lAPa-IM%9I^fGQ-*pSb7(xJczYmD!+W4!9=
z=4$rvdLNU{rP{P>DQBh$VzNm8DhM_~k?$CZ^~$KneIl#aj@U^3Uxs>0jVDvAZMOj
zcygB}$4S@~(@$fI3Y_aGxprO|aX_amv~ytE`!tfA*6zYe!K7RiG+hl!~nT3Y7jW
zbF5*y(70c#+4F9D@^RBSK^yPe*VC6s!@z-)@ov?-gQW=Uo#(JRObK0D!Mg%wcE2Jj
zU_J@O6{aKwLQbynM(BAJM)Th6fkvS+4Nkg<1B!%VBT0u_%CAOy(D-czp(14GySqJ{-QIStk&?pPY@;0FGEKulvLx
zZb(`WO6kL7#YEH*#~bp#%|YHqMzcE40a+2p5k6KJ&I${4RZY!gb)GD>C@W!xHsNkE
z;`H>8m)Y=@`Ys6h6J`pF*ywO3ei4#BCNUZ94Vj$F=JW6r+Jw8w*GKxxF2x4V4{F58
zOya67q~Hq-hJva@nKZL+Q_JA%T@3XeIY%nRB!yjeB%t2c#D0aBu~##?>x6?S<5t4(}uka+)C01*U1Bn9VK3eAobpp!#@Wx}L>^=YG&MMV#LG|W4Y
zh^<^o5^Wz7kVSOetMZM!>D{8UQU3^nIAS|z)o>$iTqk%U4QmS0*9UH>W)Aj|{TjNE^WiVCD)D(jWdlh<{0QT19@hr)(zsPvNx`v(F()OwJ(guC{!=AaUGSbkYN`zJhvLLd|>|Q_&&&GV`rqT0|wI
zvUHWMj!x{5l)C+E^N6JR>-DRV7hT{Xe*7NFm~?SnflPA@%1vz&2M4U7Y1zn0*=aEP
zL|C(Xmp>$zh!vY3&!<90%!j_l$)2m`1P@J%X_rH7X>d?-J?V}Fd14?lmC>m
z>9y&Lig8&!^fn`Ux+rk!h{OrOmXZ3g2D}*Ps~6N_@cYC!N}GYo4TjEapPL*Ch^e!P
zZ7`95lu)Pw6jkqFqN3GCNC^$->z&+g5m7nr}5q&VK^rX5D<`-{p*g@&5eQaB@(
zaH}4hg3mvcO}wdf(U5MpYt?Su6QQ5RSNz`2$YvH)LY*u0)jd44~
zlZ0XgND}$JH3#x|{UJL09ur*X1q~mc+^Kmo2}Fz@mmPTd^(iT(fSI}ZU5ewBFf_E=
z!4=k!>nMHSgyeqTFLGvaL7GyE$SYy)m4!WBRmF#Y#AD;c{I)6QUmu%erS`@>52;W$
zfXx>%k&J1NUOo~Z6K^s24~|J{U>$#wPD%F~1r2D*@1t4Sa^`>}7+VVF&{ms;6xs`z
z(kitYyWBe*lURMZj4=tM=W!6&coMIvoywSorY6mQPzJ`N7MkW!GQpCOyo;E<4k%jQ
zOxWZp+ZJD<&jQNO1;-z3#Qe*dyYi3rtC54#`FUFfp@K0c^%OQv4tUxasR}sZyxmpi
z<48#gcn_gbSY3IBsU^FeL+X_{-=m@a@3=Elr$Yi%;6u&xBmqqp}zLK#mHu*)RTCEAZ
zkRFo;T@7m3PkGrU!a78pf00=2#x!17bO|V%GC;%Il+2pee2lYCU8ju`erii?E@oCt
zboP1n_4S1q7@SHzZ_2rr@FcEmPKFM&N`0kQp@
zOrO<&E1QAw&{*nV3wyrbM?601OnH}E0UX=`8`*iAe=`4ADM*?Jh$(~6faoJ
zPg(S!Jw}HUdfu)xKT+s?hj3X}k4zSZ@w;ZXPsr&rq*?}gBhH>J9=$*s=qKH@Q+?g9
zu9BEI`a;xCG8bkY&`8!qFS`dK>>?
zS~pXx!O-5_YF_Kg+jgx(+ldLRhx!jKoqjr0vYh@DMPkm=ZQ)q!w&Ur3-(}ialH5hO
zWHt#nr7zW5Oy1(ibT!@o=tV_aBtiz)2?<;)GePC(lqal#obO*g(Md_ZeC$(tT6j1z
zO&sgyZe!}aVN}vs)lgZ|^`$mNC~^+VG49w;)V$#DPw{2fs90Q;_=g>)ZbKN6DoVZZ+ORt#N1!Ee1e~
z{g?ULGa4*YuYVhNc+$}V;dA6G>KNO5itW);ARLSvi){@IkB^c)F^tVyurTa?cfoKV
zQnPI;!;6yuBWrBihLcu5$q5Tbn~;i+-}!6t$-c9*Q{dO3*OW)Z!V(|sogd79aKUp(
zuk*Htsq%B~mQJq$D*CaApy!BO{eaasvAUowg`AXKDgzT*J#O;1Z-tTy9{zpbDYqdY
zNiUbe2FOX9aMxJ3?<8%rvz2H08gThq0*XmPr3EpFuk}5><=iIhSUdwft^OdcQsjbe_(zYJk
z<}ZyO=L)pzZA08Hj$BK@c`Xb_#E)M8Sh8vQK5uERTGcRZz45B~eByiC%sIaU*O7a}
zQH6m
zHVRaI7_czGnB-S@RaI;2+Xs5+)tc1a!#Fk_?g^BTT?RCNrTNRLANne#8r3uZ>*d2Q
zRSG2~rTvzjDxb67p6m-95mnsap1u_9WH|TYTB5@4N54T+eP&}DDVUt%s9?2g8Yz1)
zH#nvkSK9l5?~%~NN7pap~J{lF)5qkPlfYgxDa!aGtHS{OBiFvm*Av
z*SbDrm{}RY^+SH)ktp5oee)hW0viK2`6*IXCTeGJmQjw@81Zw|jH@FNG>t&u-%AgBiAnGSN1(*m$rBm!>
z%5cpE%-X9EAGWzqRxB%>4mNv!kqJIhJHCCY^I)ldt@l->D5Z27SBL?WY7Uj`0Cb=v0*A(Z!O;qL4z)
zYp=k-FC#B%!`bu7-5iI(uKBfQ_2&p@9@a6G
z#G=&YBS>L^=Ej@j@Z1?X3h@iypwTLSd7j5I%~J4XEvex3ms
literal 0
HcmV?d00001
diff --git a/edge/assets/logo_24x24.png b/edge/assets/logo_24x24.png
new file mode 100644
index 0000000000000000000000000000000000000000..e3fa21b055d6f9c41e9d2e3ee8999ee3eab17ca1
GIT binary patch
literal 1228
zcmV;-1T*`IP)Px%Oi)ZzMNDaNC@28MfF{m6JEmC>00030z$%GQBLDwP
z|F|;%000dQ4fD4m>ZcONd+T^
zv>*S@L%??lgFzeiye|97O!m7l|Itp=$E^R%KKa5x|IbG4tswcqFu!Uld^Qm8t|$G<
zJ(g7-@4r7LCMI?>4~&eA^SMQaP$+XZ6s1=ejz|#{6cma;5t^Esh(;2-X%5wh2=TNn
zW@csp007d23iP!mxn~jl$wc0i2lc!<>#8^RyF>K2CHBET8Wj<>V-b8X3#(lrczAd+
zE-qvn3@alU7!L&UwI~WEGNf7~__`9HR2H964vmeCTU%L{bxr^P0QSN{mQD)*01BaK
zBgDklfOTd1#XI)GK>Ei-`Nc;1%vSQtUzKGNU06){zcl2a5C7L#{LD<8eMqZj4FCWD
z^}<5^(OLJxI{(v9{mMP~zAXRNU;n@({>C-`)m;Dp0Q}Zx|KVr-$vgklQ~uOg|H&)+
z!7hS04yjuV^|m1Y)?@y=B8fo_?yVXB++6|#2>;q=|JYXZ#!vs{V*kZ1|I$vqZwaMX
z3sX~5n@bOoLJy=*5S~;G^tdL!XAA$_Isd~X`?eYX(K_6Y6aU9Dv}Ft_DJjcx5JpBu
z(~J!a0095lYOq=qi#rZIGB5xR9RJ*7=etGt#XeO-I*msu@2LQcSODm)FaOp%NFo58
zZZiMjUi{Hj|I9$tk`t?SJnE(b|J-W-+))`PGXMZAYfAvGh8ya~Y?bO4T>t<8gmh9)
zQvd=50RsUA1pxvB1pfX0{{H_b{iA2=HvS6y`~G?RjHmqx{zLxH`waSG`7;nZ;wTmq
zu59RhgFS5c`*GFS`@i97_k%Mc_m%bV{Z*1e{EoZhm{%#p{YcBo(;PCezKU>
z{r&m+{c~FW{rgnN`~Uy{|NsC0|NsC0|NsB~{vnXS(f|Mfj!8s8R4C75U_b(_3_6gD7{5VO`MH`
zfxR#%adk8UCkwK2P6meDwEn~UtK)$@CQg_*2LppjQSJ8h%z}*eARAqveomN#d~K~_
zQ(pJMb;=7G#YDj_0jlO?2<Q(n_SbwOmr>3pE
zURu4PUCcqj4QL`mOqVp@w!SsXYuc)+8cOQqcv>rFJAovU`{i~C?5!zps4pognONF1
zQ_6jwJV+vSUvYWQ4prsK%DTGJy2>V=PN_x@kmV^`1gdrlcJeRfZ!H4?9tmFFIW8ax
zh0R%l*)p-s{@#4N!om{#*7nYGMM04ewQ5s-$I9@Qg)^qL$nZ^?Jazi4`L3YA4(^VV
zT@kj}%hAfTqghkmSk}_q1mtr`&p-~&5GEgSNg*R0aUl(O80wj!#taubJ3A93;uyG?
q*%{ecSlC&BVl0T5WyF#$@Bjb+4P*^qC.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))
+}
diff --git a/edge/js/background.js b/edge/js/background.js
new file mode 100644
index 0000000..6b1628c
--- /dev/null
+++ b/edge/js/background.js
@@ -0,0 +1,218 @@
+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 getSelectionTextFromTab(tabId, fallbackText) {
+ return new Promise((resolve) => {
+ const fallback = typeof fallbackText === 'string' ? fallbackText : ''
+ if (!tabId || !chrome.scripting || typeof chrome.scripting.executeScript !== 'function') {
+ resolve(fallback)
+ return
+ }
+
+ try {
+ chrome.scripting.executeScript(
+ {
+ target: { tabId },
+ func: pageReadSelectionText
+ },
+ (results) => {
+ if (chrome.runtime.lastError) {
+ resolve(fallback)
+ return
+ }
+ const first = Array.isArray(results) ? results[0] : null
+ const text = first && typeof first.result === 'string' ? first.result : ''
+ resolve(text || fallback)
+ }
+ )
+ } catch (_) {
+ resolve(fallback)
+ }
+ })
+}
+
+function tryOpenActionPopup(tab) {
+ try {
+ if (!chrome.action || typeof chrome.action.openPopup !== 'function') return
+ const windowId = tab && typeof tab.windowId === 'number' ? tab.windowId : undefined
+
+ const open = () => {
+ try {
+ if (typeof windowId === 'number') {
+ chrome.action.openPopup({ windowId }, () => void chrome.runtime.lastError)
+ } else {
+ chrome.action.openPopup({}, () => void chrome.runtime.lastError)
+ }
+ } catch (_) {
+ // best-effort only
+ }
+ }
+
+ // Avoid: "Cannot show popup for an inactive window".
+ if (typeof windowId === 'number' && chrome.windows && typeof chrome.windows.update === 'function') {
+ chrome.windows.update(windowId, { focused: true }, () => {
+ void chrome.runtime.lastError
+ open()
+ })
+ return
+ }
+
+ open()
+ } catch (_) {
+ // best-effort only
+ }
+}
+
+let cachedUiLanguage = null
+let cachedOverrideMessages = null
+
+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(`` + '\n')
+ }
+})
\ No newline at end of file
diff --git a/edge/js/compat/memosApi.adapter.js b/edge/js/compat/memosApi.adapter.js
new file mode 100644
index 0000000..d3efb23
--- /dev/null
+++ b/edge/js/compat/memosApi.adapter.js
@@ -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)
\ No newline at end of file
diff --git a/edge/js/compat/memosApi.modern.js b/edge/js/compat/memosApi.modern.js
new file mode 100644
index 0000000..4449eed
--- /dev/null
+++ b/edge/js/compat/memosApi.modern.js
@@ -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)
diff --git a/edge/js/compat/memosApi.v020-v021.js b/edge/js/compat/memosApi.v020-v021.js
new file mode 100644
index 0000000..bb874fd
--- /dev/null
+++ b/edge/js/compat/memosApi.v020-v021.js
@@ -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)
diff --git a/edge/js/compat/memosApi.v023.js b/edge/js/compat/memosApi.v023.js
new file mode 100644
index 0000000..c438e55
--- /dev/null
+++ b/edge/js/compat/memosApi.v023.js
@@ -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)
diff --git a/edge/js/dayjs.min.js b/edge/js/dayjs.min.js
new file mode 100644
index 0000000..ba16e65
--- /dev/null
+++ b/edge/js/dayjs.min.js
@@ -0,0 +1 @@
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).dayjs=e()}(this,(function(){"use strict";var t=1e3,e=6e4,n=36e5,r="millisecond",i="second",s="minute",u="hour",a="day",o="week",f="month",h="quarter",c="year",d="date",l="Invalid Date",$=/^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/,y=/\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,M={name:"en",weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),ordinal:function(t){var e=["th","st","nd","rd"],n=t%100;return"["+t+(e[(n-20)%10]||e[n]||e[0])+"]"}},m=function(t,e,n){var r=String(t);return!r||r.length>=e?t:""+Array(e+1-r.length).join(n)+t},v={s:m,z:function(t){var e=-t.utcOffset(),n=Math.abs(e),r=Math.floor(n/60),i=n%60;return(e<=0?"+":"-")+m(r,2,"0")+":"+m(i,2,"0")},m:function t(e,n){if(e.date()1)return t(u[0])}else{var a=e.name;D[a]=e,i=a}return!r&&i&&(g=i),i||!r&&g},w=function(t,e){if(p(t))return t.clone();var n="object"==typeof e?e:{};return n.date=t,n.args=arguments,new _(n)},O=v;O.l=S,O.i=p,O.w=function(t,e){return w(t,{locale:e.$L,utc:e.$u,x:e.$x,$offset:e.$offset})};var _=function(){function M(t){this.$L=S(t.locale,null,!0),this.parse(t)}var m=M.prototype;return m.parse=function(t){this.$d=function(t){var e=t.date,n=t.utc;if(null===e)return new Date(NaN);if(O.u(e))return new Date;if(e instanceof Date)return new Date(e);if("string"==typeof e&&!/Z$/i.test(e)){var r=e.match($);if(r){var i=r[2]-1||0,s=(r[7]||"0").substring(0,3);return n?new Date(Date.UTC(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,s)):new Date(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,s)}}return new Date(e)}(t),this.$x=t.x||{},this.init()},m.init=function(){var t=this.$d;this.$y=t.getFullYear(),this.$M=t.getMonth(),this.$D=t.getDate(),this.$W=t.getDay(),this.$H=t.getHours(),this.$m=t.getMinutes(),this.$s=t.getSeconds(),this.$ms=t.getMilliseconds()},m.$utils=function(){return O},m.isValid=function(){return!(this.$d.toString()===l)},m.isSame=function(t,e){var n=w(t);return this.startOf(e)<=n&&n<=this.endOf(e)},m.isAfter=function(t,e){return w(t) {
+ 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()
\ No newline at end of file
diff --git a/edge/js/ja.js b/edge/js/ja.js
new file mode 100644
index 0000000..2da47dc
--- /dev/null
+++ b/edge/js/ja.js
@@ -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}));
diff --git a/edge/js/jquery.min.js b/edge/js/jquery.min.js
new file mode 100644
index 0000000..7f37b5d
--- /dev/null
+++ b/edge/js/jquery.min.js
@@ -0,0 +1,2 @@
+/*! jQuery v3.7.1 | (c) OpenJS Foundation and other contributors | jquery.org/license */
+!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(ie,e){"use strict";var oe=[],r=Object.getPrototypeOf,ae=oe.slice,g=oe.flat?function(e){return oe.flat.call(e)}:function(e){return oe.concat.apply([],e)},s=oe.push,se=oe.indexOf,n={},i=n.toString,ue=n.hasOwnProperty,o=ue.toString,a=o.call(Object),le={},v=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},y=function(e){return null!=e&&e===e.window},C=ie.document,u={type:!0,src:!0,nonce:!0,noModule:!0};function m(e,t,n){var r,i,o=(n=n||C).createElement("script");if(o.text=e,t)for(r in u)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[i.call(e)]||"object":typeof e}var t="3.7.1",l=/HTML$/i,ce=function(e,t){return new ce.fn.init(e,t)};function c(e){var t=!!e&&"length"in e&&e.length,n=x(e);return!v(e)&&!y(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+ge+")"+ge+"*"),x=new RegExp(ge+"|>"),j=new RegExp(g),A=new RegExp("^"+t+"$"),D={ID:new RegExp("^#("+t+")"),CLASS:new RegExp("^\\.("+t+")"),TAG:new RegExp("^("+t+"|[*])"),ATTR:new RegExp("^"+p),PSEUDO:new RegExp("^"+g),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+ge+"*(even|odd|(([+-]|)(\\d*)n|)"+ge+"*(?:([+-]|)"+ge+"*(\\d+)|))"+ge+"*\\)|)","i"),bool:new RegExp("^(?:"+f+")$","i"),needsContext:new RegExp("^"+ge+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+ge+"*((?:-\\d)?\\d*)"+ge+"*\\)|)(?=[^-]|$)","i")},N=/^(?:input|select|textarea|button)$/i,q=/^h\d$/i,L=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,H=/[+~]/,O=new RegExp("\\\\[\\da-fA-F]{1,6}"+ge+"?|\\\\([^\\r\\n\\f])","g"),P=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},M=function(){V()},R=J(function(e){return!0===e.disabled&&fe(e,"fieldset")},{dir:"parentNode",next:"legend"});try{k.apply(oe=ae.call(ye.childNodes),ye.childNodes),oe[ye.childNodes.length].nodeType}catch(e){k={apply:function(e,t){me.apply(e,ae.call(t))},call:function(e){me.apply(e,ae.call(arguments,1))}}}function I(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(V(e),e=e||T,C)){if(11!==p&&(u=L.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return k.call(n,a),n}else if(f&&(a=f.getElementById(i))&&I.contains(e,a)&&a.id===i)return k.call(n,a),n}else{if(u[2])return k.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&e.getElementsByClassName)return k.apply(n,e.getElementsByClassName(i)),n}if(!(h[t+" "]||d&&d.test(t))){if(c=t,f=e,1===p&&(x.test(t)||m.test(t))){(f=H.test(t)&&U(e.parentNode)||e)==e&&le.scope||((s=e.getAttribute("id"))?s=ce.escapeSelector(s):e.setAttribute("id",s=S)),o=(l=Y(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+Q(l[o]);c=l.join(",")}try{return k.apply(n,f.querySelectorAll(c)),n}catch(e){h(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return re(t.replace(ve,"$1"),e,n,r)}function W(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function F(e){return e[S]=!0,e}function $(e){var t=T.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function B(t){return function(e){return fe(e,"input")&&e.type===t}}function _(t){return function(e){return(fe(e,"input")||fe(e,"button"))&&e.type===t}}function z(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&R(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function X(a){return F(function(o){return o=+o,F(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function U(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}function V(e){var t,n=e?e.ownerDocument||e:ye;return n!=T&&9===n.nodeType&&n.documentElement&&(r=(T=n).documentElement,C=!ce.isXMLDoc(T),i=r.matches||r.webkitMatchesSelector||r.msMatchesSelector,r.msMatchesSelector&&ye!=T&&(t=T.defaultView)&&t.top!==t&&t.addEventListener("unload",M),le.getById=$(function(e){return r.appendChild(e).id=ce.expando,!T.getElementsByName||!T.getElementsByName(ce.expando).length}),le.disconnectedMatch=$(function(e){return i.call(e,"*")}),le.scope=$(function(){return T.querySelectorAll(":scope")}),le.cssHas=$(function(){try{return T.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}}),le.getById?(b.filter.ID=function(e){var t=e.replace(O,P);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(O,P);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&C){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):t.querySelectorAll(e)},b.find.CLASS=function(e,t){if("undefined"!=typeof t.getElementsByClassName&&C)return t.getElementsByClassName(e)},d=[],$(function(e){var t;r.appendChild(e).innerHTML="",e.querySelectorAll("[selected]").length||d.push("\\["+ge+"*(?:value|"+f+")"),e.querySelectorAll("[id~="+S+"-]").length||d.push("~="),e.querySelectorAll("a#"+S+"+*").length||d.push(".#.+[+~]"),e.querySelectorAll(":checked").length||d.push(":checked"),(t=T.createElement("input")).setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),r.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&d.push(":enabled",":disabled"),(t=T.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||d.push("\\["+ge+"*name"+ge+"*="+ge+"*(?:''|\"\")")}),le.cssHas||d.push(":has"),d=d.length&&new RegExp(d.join("|")),l=function(e,t){if(e===t)return a=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!le.sortDetached&&t.compareDocumentPosition(e)===n?e===T||e.ownerDocument==ye&&I.contains(ye,e)?-1:t===T||t.ownerDocument==ye&&I.contains(ye,t)?1:o?se.call(o,e)-se.call(o,t):0:4&n?-1:1)}),T}for(e in I.matches=function(e,t){return I(e,null,null,t)},I.matchesSelector=function(e,t){if(V(e),C&&!h[t+" "]&&(!d||!d.test(t)))try{var n=i.call(e,t);if(n||le.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){h(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(O,P),e[3]=(e[3]||e[4]||e[5]||"").replace(O,P),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||I.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&I.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return D.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&j.test(n)&&(t=Y(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(O,P).toLowerCase();return"*"===e?function(){return!0}:function(e){return fe(e,t)}},CLASS:function(e){var t=s[e+" "];return t||(t=new RegExp("(^|"+ge+")"+e+"("+ge+"|$)"))&&s(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=I.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function T(e,n,r){return v(n)?ce.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?ce.grep(e,function(e){return e===n!==r}):"string"!=typeof n?ce.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(ce.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||k,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:S.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof ce?t[0]:t,ce.merge(this,ce.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:C,!0)),w.test(r[1])&&ce.isPlainObject(t))for(r in t)v(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=C.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):v(e)?void 0!==n.ready?n.ready(e):e(ce):ce.makeArray(e,this)}).prototype=ce.fn,k=ce(C);var E=/^(?:parents|prev(?:Until|All))/,j={children:!0,contents:!0,next:!0,prev:!0};function A(e,t){while((e=e[t])&&1!==e.nodeType);return e}ce.fn.extend({has:function(e){var t=ce(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,Ce=/^$|^module$|\/(?:java|ecma)script/i;xe=C.createDocumentFragment().appendChild(C.createElement("div")),(be=C.createElement("input")).setAttribute("type","radio"),be.setAttribute("checked","checked"),be.setAttribute("name","t"),xe.appendChild(be),le.checkClone=xe.cloneNode(!0).cloneNode(!0).lastChild.checked,xe.innerHTML="",le.noCloneChecked=!!xe.cloneNode(!0).lastChild.defaultValue,xe.innerHTML="",le.option=!!xe.lastChild;var ke={thead:[1,""],col:[2,""],tr:[2,""],td:[3,""],_default:[0,"",""]};function Se(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&fe(e,t)?ce.merge([e],n):n}function Ee(e,t){for(var n=0,r=e.length;n",""]);var je=/<|?\w+;/;function Ae(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function Re(e,t){return fe(e,"table")&&fe(11!==t.nodeType?t:t.firstChild,"tr")&&ce(e).children("tbody")[0]||e}function Ie(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function We(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Fe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(_.hasData(e)&&(s=_.get(e).events))for(i in _.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),C.head.appendChild(r[0])},abort:function(){i&&i()}}});var Jt,Kt=[],Zt=/(=)\?(?=&|$)|\?\?/;ce.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Kt.pop()||ce.expando+"_"+jt.guid++;return this[e]=!0,e}}),ce.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Zt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Zt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=v(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Zt,"$1"+r):!1!==e.jsonp&&(e.url+=(At.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||ce.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=ie[r],ie[r]=function(){o=arguments},n.always(function(){void 0===i?ce(ie).removeProp(r):ie[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Kt.push(r)),o&&v(i)&&i(o[0]),o=i=void 0}),"script"}),le.createHTMLDocument=((Jt=C.implementation.createHTMLDocument("").body).innerHTML="",2===Jt.childNodes.length),ce.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(le.createHTMLDocument?((r=(t=C.implementation.createHTMLDocument("")).createElement("base")).href=C.location.href,t.head.appendChild(r)):t=C),o=!n&&[],(i=w.exec(e))?[t.createElement(i[1])]:(i=Ae([e],t,o),o&&o.length&&ce(o).remove(),ce.merge([],i.childNodes)));var r,i,o},ce.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(ce.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},ce.expr.pseudos.animated=function(t){return ce.grep(ce.timers,function(e){return t===e.elem}).length},ce.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=ce.css(e,"position"),c=ce(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=ce.css(e,"top"),u=ce.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),v(t)&&(t=t.call(e,n,ce.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},ce.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){ce.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===ce.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===ce.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=ce(e).offset()).top+=ce.css(e,"borderTopWidth",!0),i.left+=ce.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-ce.css(r,"marginTop",!0),left:t.left-i.left-ce.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===ce.css(e,"position"))e=e.offsetParent;return e||J})}}),ce.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;ce.fn[t]=function(e){return M(this,function(e,t,n){var r;if(y(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),ce.each(["top","left"],function(e,n){ce.cssHooks[n]=Ye(le.pixelPosition,function(e,t){if(t)return t=Ge(e,n),_e.test(t)?ce(e).position()[n]+"px":t})}),ce.each({Height:"height",Width:"width"},function(a,s){ce.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){ce.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return M(this,function(e,t,n){var r;return y(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?ce.css(e,t,i):ce.style(e,t,n,i)},s,n?e:void 0,n)}})}),ce.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){ce.fn[t]=function(e){return this.on(t,e)}}),ce.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.on("mouseenter",e).on("mouseleave",t||e)}}),ce.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){ce.fn[n]=function(e,t){return 0'+defaults.message+'
\n' +
+ '';
+ 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);
+ };
+ }
+});
\ No newline at end of file
diff --git a/edge/js/oper.js b/edge/js/oper.js
new file mode 100644
index 0000000..461c6be
--- /dev/null
+++ b/edge/js/oper.js
@@ -0,0 +1,1259 @@
+dayjs.extend(window.dayjs_plugin_relativeTime)
+let currentMemoLock = ''
+
+function isFullscreenMode() {
+ try {
+ const params = new URLSearchParams(window.location.search || '')
+ return params.get('mode') === 'full'
+ } catch (_) {
+ return false
+ }
+}
+
+function openFullscreenTab() {
+ try {
+ const url = chrome.runtime.getURL('popup.html?mode=full')
+ chrome.tabs.create({ url })
+ } catch (_) {
+ // best-effort only
+ }
+}
+
+function initProportionalEditorResize() {
+ try {
+ if (isFullscreenMode()) return
+
+ const editor = document.querySelector('.memo-editor')
+ const tools = document.querySelector('.common-tools-wrapper')
+ const handle = document.getElementById('editor-resize-handle')
+ if (!editor || !tools || !handle) return
+
+ const safety = 8
+ const initialRect = editor.getBoundingClientRect()
+ const baseW = Math.ceil(initialRect.width)
+ const baseH = Math.ceil(initialRect.height)
+
+ // Lock the base size. Scaling will be applied by setting width/height.
+ editor.style.width = `${baseW}px`
+ editor.style.height = `${baseH}px`
+ editor.style.minWidth = `${baseW}px`
+ editor.style.minHeight = `${baseH}px`
+
+ 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 toolsRect = tools.getBoundingClientRect()
+ const toolsStyle = window.getComputedStyle(tools)
+ const toolsMarginTop = parseFloat(toolsStyle.marginTop || '0') || 0
+ const extraWidth = safety * 2
+ const extraHeight = nonEditorHeight + Math.ceil(toolsRect.height + toolsMarginTop) + safety
+ const widthScale = (viewportW - extraWidth) / baseW
+ const heightScale = (viewportH - extraHeight) / baseH
+ maxScale = Math.max(1, Math.min(widthScale, heightScale))
+ applyScale(currentScale)
+ }
+
+ const endDrag = () => {
+ if (!dragging) return
+ dragging = false
+ handle.classList.remove('dragging')
+ persistScale()
+ }
+
+ const onPointerMove = (ev) => {
+ if (!dragging) return
+ const dx = ev.clientX - dragStartX
+ const dy = ev.clientY - dragStartY
+ const widthScale = (baseW * dragStartScale + dx) / baseW
+ const heightScale = (baseH * dragStartScale + dy) / baseH
+ applyScale(Math.max(widthScale, heightScale))
+ }
+
+ const startDrag = (ev) => {
+ ev.preventDefault()
+ dragging = true
+ dragStartX = ev.clientX
+ dragStartY = ev.clientY
+ dragStartScale = currentScale
+ handle.classList.add('dragging')
+ if (typeof handle.setPointerCapture === 'function') {
+ try {
+ handle.setPointerCapture(ev.pointerId)
+ } catch (_) {
+ // Ignore capture failures.
+ }
+ }
+ }
+
+ computeMaxScale()
+
+ try {
+ const localValue = window.localStorage ? Number(window.localStorage.getItem(storageKey)) : NaN
+ if (Number.isFinite(localValue) && localValue >= 1) {
+ applyScale(localValue)
+ }
+ } catch (_) {
+ // ignore
+ }
+
+ try {
+ chrome.storage.sync.get({ [storageKey]: 1 }, (items) => {
+ const savedScale = Number(items && items[storageKey])
+ if (Number.isFinite(savedScale) && savedScale >= 1) {
+ applyScale(savedScale)
+ }
+ })
+ } catch (_) {
+ // ignore
+ }
+
+ handle.addEventListener('pointerdown', startDrag)
+ window.addEventListener('pointermove', onPointerMove)
+ handle.addEventListener('pointerup', endDrag)
+ handle.addEventListener('pointercancel', endDrag)
+ window.addEventListener('pointerup', endDrag)
+ window.addEventListener('resize', computeMaxScale)
+ } catch (_) {
+ // best-effort only
+ }
+}
+
+function msg(key) {
+ if (typeof window.t === 'function') return window.t(key)
+ return chrome.i18n.getMessage(key) || ''
+}
+
+function applyDayjsLocaleByUiLanguage(uiLang) {
+ const lang = String(uiLang || 'auto')
+ if (lang === 'zh_CN') {
+ dayjs.locale('zh-cn')
+ return
+ }
+
+ if (lang === 'ja') {
+ dayjs.locale('ja')
+ return
+ }
+
+ if (lang === 'ko') {
+ dayjs.locale('ko')
+ return
+ }
+
+ if (lang === 'en') {
+ dayjs.locale('en')
+ return
+ }
+
+ // auto: best-effort infer from browser UI language
+ const ui = String(chrome.i18n.getUILanguage ? chrome.i18n.getUILanguage() : '').toLowerCase()
+ if (ui.startsWith('zh')) {
+ dayjs.locale('zh-cn')
+ return
+ }
+ if (ui.startsWith('ja')) {
+ dayjs.locale('ja')
+ return
+ }
+ if (ui.startsWith('ko')) {
+ dayjs.locale('ko')
+ return
+ }
+ dayjs.locale('en')
+}
+
+function updateLockNowText(lockType) {
+ if (lockType === 'PUBLIC') {
+ $('#lock-now').text(msg('lockPublic'))
+ } else if (lockType === 'PRIVATE') {
+ $('#lock-now').text(msg('lockPrivate'))
+ } else if (lockType === 'PROTECTED') {
+ $('#lock-now').text(msg('lockProtected'))
+ }
+}
+
+applyDayjsLocaleByUiLanguage(typeof window.getUiLanguage === 'function' ? window.getUiLanguage() : 'auto')
+
+if (isFullscreenMode()) {
+ document.body.classList.add('fullscreen')
+}
+
+window.addEventListener('i18n:changed', (ev) => {
+ applyDayjsLocaleByUiLanguage(ev && ev.detail ? ev.detail.lang : 'auto')
+ updateLockNowText(currentMemoLock)
+ renderUploadList(relistNow)
+})
+
+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(
+ {
+ apiUrl: '',
+ apiTokens: '',
+ apiFlavor: '',
+ hidetag: '',
+ showtag: '',
+ memo_lock: '',
+ open_action: '',
+ open_content: '',
+ userid: '',
+ memoUiPath: 'memos',
+ resourceIdList: [],
+ attachmentOnlyDefaultText: ''
+ },
+ function (items) {
+ var flag = false
+ var returnObject = {}
+ if (items.apiUrl === '' || items.apiTokens === '') {
+ flag = false
+ } else {
+ flag = true
+ }
+ returnObject.status = flag
+ returnObject.apiUrl = items.apiUrl
+ returnObject.apiTokens = items.apiTokens
+ returnObject.apiFlavor = items.apiFlavor
+ returnObject.hidetag = items.hidetag
+ returnObject.showtag = items.showtag
+ returnObject.memo_lock = items.memo_lock
+ returnObject.open_content = items.open_content
+ returnObject.open_action = items.open_action
+ returnObject.userid = items.userid
+ returnObject.memoUiPath = items.memoUiPath
+ returnObject.resourceIdList = items.resourceIdList
+ returnObject.attachmentOnlyDefaultText = items.attachmentOnlyDefaultText
+
+ if (callback) callback(returnObject)
+ }
+ )
+}
+
+function getApiAdapter(info) {
+ if (window.MemosApiAdapter && typeof window.MemosApiAdapter.resolve === 'function') {
+ return window.MemosApiAdapter.resolve(info)
+ }
+ return null
+}
+
+function getMemoUid(memo) {
+ if (!memo) return ''
+ if (memo.uid != null && memo.uid !== '') return String(memo.uid)
+ if (typeof memo.name === 'string' && memo.name) return memo.name.split('/').pop()
+ return ''
+}
+
+get_info(function (info) {
+ if (info.status) {
+ //已经有绑定信息了,折叠
+ $('#blog_info').hide()
+ }
+ var memoNow = info.memo_lock
+ if (memoNow == '') {
+ chrome.storage.sync.set(
+ { memo_lock: 'PUBLIC' }
+ )
+ memoNow = 'PUBLIC'
+ }
+ currentMemoLock = memoNow
+ updateLockNowText(memoNow)
+ $('#apiUrl').val(info.apiUrl)
+ $('#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)
+ } else {
+ const $textarea = $("textarea[name=text]")
+ $textarea.val(info.open_content)
+ focusTextareaToEnd($textarea)
+ }
+
+ relistNow = Array.isArray(info.resourceIdList) ? info.resourceIdList : []
+ renderUploadList(relistNow)
+ initProportionalEditorResize()
+ //从localstorage 里面读取数据
+ setTimeout(get_info, 1)
+})
+
+chrome.storage.onChanged.addListener(function (changes, areaName) {
+ if (areaName !== 'sync') return
+ if (!changes.resourceIdList) return
+ relistNow = Array.isArray(changes.resourceIdList.newValue)
+ ? changes.resourceIdList.newValue
+ : []
+ renderUploadList(relistNow)
+})
+
+// focus is handled after textarea content is set
+
+//监听输入结束,保存未发送内容到本地
+$("textarea[name=text]").blur(function () {
+ chrome.storage.sync.set(
+ { open_action: 'save_text', open_content: $("textarea[name=text]").val() }
+ )
+})
+
+$("textarea[name=text]").on('keydown', function (ev) {
+ if (ev.code === 'Enter' && (ev.ctrlKey || ev.metaKey)) {
+ $('#content_submit_text').click()
+ }
+})
+
+$('#fullscreen').on('click', function () {
+ if (isFullscreenMode()) return
+ openFullscreenTab()
+})
+
+//监听拖拽事件,实现拖拽到窗口上传图片
+initDrag()
+
+//监听复制粘贴事件,实现粘贴上传图片
+document.addEventListener('paste', function (e) {
+ let photo = null
+ if (e.clipboardData.files[0]) {
+ photo = e.clipboardData.files[0]
+ } else if (e.clipboardData.items[0] && e.clipboardData.items[0].getAsFile()) {
+ photo = e.clipboardData.items[0].getAsFile()
+ }
+
+ if (photo != null) {
+ uploadImage(photo)
+ }
+})
+
+function initDrag() {
+ var file = null
+ var obj = $("textarea[name=text]")[0]
+ obj.ondragenter = function (ev) {
+ if (ev.target.className === 'common-editor-inputer') {
+ $.message({
+ message: msg('picDrag'),
+ autoClose: false
+ })
+ $('body').css('opacity', 0.3)
+ }
+ ev.dataTransfer.dropEffect = 'copy'
+ }
+ obj.ondragover = function (ev) {
+ ev.preventDefault()
+ ev.dataTransfer.dropEffect = 'copy'
+ }
+ obj.ondrop = function (ev) {
+ $('body').css('opacity', 1)
+ ev.preventDefault()
+ var files = ev.dataTransfer.files || ev.target.files
+ for (var i = 0; i < files.length; i++) {
+ file = files[i]
+ }
+ uploadImage(file)
+ }
+ obj.ondragleave = function (ev) {
+ ev.preventDefault()
+ if (ev.target.className === 'common-editor-inputer') {
+ $.message({
+ message: msg('picCancelDrag')
+ })
+ $('body').css('opacity', 1)
+ }
+ }
+}
+
+function escapeHtml(input) {
+ return String(input)
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''')
+}
+
+function focusTextareaToEnd($textarea) {
+ try {
+ const el = $textarea && $textarea[0]
+ if (!el) return
+ el.focus()
+ const len = typeof el.value === 'string' ? el.value.length : 0
+ if (typeof el.setSelectionRange === 'function') {
+ el.setSelectionRange(len, len)
+ }
+ } catch (_) {
+ // best-effort only
+ }
+}
+
+function buildV1ResourceStreamUrl(info, resource) {
+ if (!info || !info.apiUrl || !resource) return ''
+ // Use the configured apiUrl as the base (may include a reverse-proxy subpath).
+ // Do NOT reduce to origin-only, otherwise deployments like https://host/memos/ will break.
+ let root = String(info.apiUrl)
+ try {
+ const u = new URL(root)
+ u.hash = ''
+ u.search = ''
+ root = u.toString()
+ } catch (_) {
+ // keep as-is
+ }
+ if (root && !root.endsWith('/')) root += '/'
+
+ function isImageResource(r) {
+ if (!r) return false
+ const t = typeof r.type === 'string' ? r.type.toLowerCase() : ''
+ if (t.startsWith('image/')) return true
+ const fn = typeof r.filename === 'string' ? r.filename.toLowerCase() : ''
+ return /\.(png|jpe?g|gif|webp|bmp|svg|avif|heic)$/.test(fn)
+ }
+
+ function isProbablyUid(s) {
+ if (typeof s !== 'string') return false
+ const v = s.trim()
+ if (!v) return false
+ if (v.indexOf('/') !== -1) return false
+ if (/^\d+$/.test(v)) return false
+ // shortuuid v4 typically uses URL-safe base57-ish; allow a conservative charset.
+ return /^[A-Za-z0-9_-]{8,}$/.test(v)
+ }
+
+ function buildStreamUrl(uid) {
+ const base = root + 'o/r/' + encodeURIComponent(uid)
+ return isImageResource(resource) ? base + '?thumbnail=1' : base
+ }
+
+ const uidRaw = resource.uid != null ? resource.uid : resource.UID != null ? resource.UID : resource.Uid
+ const uid = typeof uidRaw === 'string' ? uidRaw : uidRaw != null ? String(uidRaw) : ''
+ if (uid.trim() !== '') return buildStreamUrl(uid.trim())
+
+ // Legacy versions (e.g. v0.18) may only expose numeric `id` without `uid/name`.
+ const idRaw = resource.id != null ? resource.id : resource.ID != null ? resource.ID : resource.Id
+ const id = typeof idRaw === 'number' && Number.isFinite(idRaw)
+ ? String(Math.floor(idRaw))
+ : typeof idRaw === 'string' && idRaw.trim() !== '' && !Number.isNaN(Number(idRaw))
+ ? String(Math.floor(Number(idRaw)))
+ : ''
+ if (id) return buildStreamUrl(id)
+
+ // Fallback for older resource shapes.
+ const name = typeof resource.name === 'string' ? resource.name : ''
+
+ // In some memo payloads, the uid may appear as `name` directly.
+ // Example: name="ETU6hjuR..." should map to /o/r/:uid, not /file/:name/:filename.
+ if (isProbablyUid(name)) return buildStreamUrl(name.trim())
+
+ const fileId = resource.publicId || resource.filename
+ if (name && fileId) return root + 'file/' + name + '/' + fileId
+ return ''
+}
+
+function normalizeUnixTimeToMs(input) {
+ if (input == null) return null
+ if (typeof input === 'number' && Number.isFinite(input)) {
+ // Heuristic: seconds are typically 10 digits; milliseconds are 13 digits.
+ if (input > 0 && input < 1e12) return input * 1000
+ return input
+ }
+ if (typeof input === 'string') {
+ const s = input.trim()
+ if (/^\d+$/.test(s)) {
+ const n = Number(s)
+ if (!Number.isFinite(n)) return null
+ if (n > 0 && n < 1e12) return n * 1000
+ return n
+ }
+ // ISO/RFC3339 etc.
+ return s
+ }
+ return null
+}
+
+function memoFromNow(memo) {
+ if (!memo) return ''
+ const raw = memo.createTime || memo.createdAt || memo.createdTs
+ const normalized = normalizeUnixTimeToMs(raw)
+ if (!normalized) return ''
+ return dayjs(normalized).fromNow()
+}
+
+function hydrateV1PreviewImages(info) {
+ const adapter = getApiAdapter(info)
+ if (!adapter || !adapter.needsAuthenticatedImagePreview()) return
+ if (!info || !info.apiUrl) return
+
+ const token = info && info.apiTokens != null ? String(info.apiTokens).trim() : ''
+ let root = String(info.apiUrl)
+ let apiOrigin = ''
+ try {
+ const u = new URL(root)
+ u.hash = ''
+ u.search = ''
+ root = u.toString()
+ apiOrigin = u.origin
+ } catch (_) {
+ // keep as-is
+ }
+ if (root && !root.endsWith('/')) root += '/'
+ const nodes = document.querySelectorAll('img.random-image')
+ if (!nodes || nodes.length === 0) return
+
+ // Revoke blob URLs on popup unload to avoid leaking memory.
+ if (!window.__memosBberObjectUrls) {
+ window.__memosBberObjectUrls = []
+ window.addEventListener('unload', function () {
+ const list = window.__memosBberObjectUrls || []
+ for (let i = 0; i < list.length; i++) {
+ try { URL.revokeObjectURL(list[i]) } catch (_) {}
+ }
+ window.__memosBberObjectUrls = []
+ })
+ }
+
+ const transparentPixel = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=='
+
+ function resolveToAbsoluteUrl(url) {
+ const u = String(url || '').trim()
+ if (!u) return ''
+ if (u.startsWith('data:') || u.startsWith('blob:') || u.startsWith('chrome-extension:')) return ''
+ if (u.startsWith('#')) return ''
+ try {
+ return new URL(u, root).toString()
+ } catch (_) {
+ return ''
+ }
+ }
+
+ function isSameOrigin(url) {
+ if (!apiOrigin) return false
+ try {
+ return new URL(url).origin === apiOrigin
+ } catch (_) {
+ return false
+ }
+ }
+
+ function looksLikeMemosResourceUrl(absUrl) {
+ const s = String(absUrl || '')
+ return s.indexOf('/o/r/') !== -1 || s.indexOf('/file/') !== -1
+ }
+
+ nodes.forEach(function (img) {
+ const hasAuthAttr = img.hasAttribute('data-auth-src')
+ const url = img.getAttribute('data-auth-src') || img.getAttribute('src')
+ if (!url) return
+ if (img.getAttribute('data-auth-loaded') === '1') return
+
+ const abs = resolveToAbsoluteUrl(url)
+ if (!abs) return
+ // Only hydrate same-origin resources that require Authorization.
+ if (!isSameOrigin(abs)) return
+
+ // Reduce unnecessary fetches: only hydrate known resource endpoints,
+ // or images explicitly marked as auth-required.
+ if (!hasAuthAttr && !looksLikeMemosResourceUrl(abs)) return
+
+ img.setAttribute('data-auth-loaded', '1')
+
+ // Prevent a broken-image icon before hydration completes.
+ // Only do this for images explicitly marked as auth-required.
+ if (hasAuthAttr) {
+ const currentSrc = img.getAttribute('src')
+ if (!currentSrc || currentSrc === abs) {
+ img.setAttribute('src', transparentPixel)
+ }
+ }
+
+ fetch(abs, {
+ method: 'GET',
+ credentials: 'include',
+ headers: token ? {
+ Authorization: 'Bearer ' + token
+ } : {}
+ })
+ .then(function (res) {
+ if (!res || !res.ok) throw new Error('HTTP ' + (res ? res.status : '0'))
+ const ct = (res.headers && typeof res.headers.get === 'function') ? (res.headers.get('content-type') || '') : ''
+ if (ct && !ct.toLowerCase().startsWith('image/')) throw new Error('Not an image')
+ return res.blob()
+ })
+ .then(function (blob) {
+ const objectUrl = URL.createObjectURL(blob)
+ window.__memosBberObjectUrls.push(objectUrl)
+ img.src = objectUrl
+ })
+ .catch(function () {
+ // Fall back to the original URL so the browser can still try cookie-based auth.
+ if (hasAuthAttr) {
+ try { img.setAttribute('src', abs) } catch (_) {}
+ }
+ })
+ })
+}
+
+function renderUploadList(list) {
+ const $wrapper = $('.upload-list-wrapper')
+ const $list = $('#uploadlist')
+ if ($list.length === 0) return
+
+ const items = Array.isArray(list) ? list : []
+ if (items.length === 0) {
+ if ($wrapper.length) $wrapper.hide()
+ $list.html('')
+ return
+ }
+
+ if ($wrapper.length) $wrapper.show()
+
+ const tipReorder = escapeHtml(msg('tipReorder'))
+ const tipDelete = escapeHtml(msg('tipDeleteAttachment'))
+
+ let html = ''
+ for (let i = 0; i < items.length; i++) {
+ const att = items[i] || {}
+ const name = att.name || ''
+ const id = att.id != null ? String(att.id) : ''
+ const filename = att.filename || name
+ html +=
+ '' +
+ '
' +
+ '≡' +
+ '' +
+ escapeHtml(filename) +
+ '' +
+ '
' +
+ '
' +
+ '
'
+ }
+
+ $list.html(html)
+}
+
+function saveUploadList(nextList, callback) {
+ relistNow = Array.isArray(nextList) ? nextList : []
+ chrome.storage.sync.set({ resourceIdList: relistNow }, callback)
+}
+
+let uploadDragIndex = null
+
+$(document).on('dragstart', '.upload-item', function (e) {
+ uploadDragIndex = Number($(this).data('index'))
+ const dt = e.originalEvent && e.originalEvent.dataTransfer
+ if (dt) {
+ dt.effectAllowed = 'move'
+ dt.setData('text/plain', String(uploadDragIndex))
+ }
+})
+
+$(document).on('dragover', '.upload-item', function (e) {
+ e.preventDefault()
+ $(this).addClass('drag-over')
+ const dt = e.originalEvent && e.originalEvent.dataTransfer
+ if (dt) dt.dropEffect = 'move'
+})
+
+$(document).on('dragleave', '.upload-item', function () {
+ $(this).removeClass('drag-over')
+})
+
+$(document).on('drop', '.upload-item', function (e) {
+ e.preventDefault()
+ $('.upload-item.drag-over').removeClass('drag-over')
+
+ const fromIndex =
+ uploadDragIndex != null
+ ? uploadDragIndex
+ : Number(
+ (e.originalEvent && e.originalEvent.dataTransfer
+ ? e.originalEvent.dataTransfer.getData('text/plain')
+ : '') || -1
+ )
+ const toIndex = Number($(this).data('index'))
+
+ uploadDragIndex = null
+ if (!Number.isFinite(fromIndex) || !Number.isFinite(toIndex)) return
+ if (fromIndex < 0 || toIndex < 0) return
+ if (fromIndex === toIndex) return
+
+ const next = (Array.isArray(relistNow) ? relistNow : []).slice()
+ if (fromIndex >= next.length || toIndex >= next.length) return
+ const moved = next.splice(fromIndex, 1)[0]
+ next.splice(toIndex, 0, moved)
+
+ saveUploadList(next, function () {
+ renderUploadList(relistNow)
+ })
+})
+
+$(document).on('click', '.upload-del', function () {
+ const name = $(this).data('name')
+ const rid = $(this).data('id')
+ if (!name) return
+
+ get_info(function (info) {
+ if (!info.status) {
+ $.message({ message: msg('placeApiUrl') })
+ return
+ }
+
+ 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
+ })
+ saveUploadList(next, function () {
+ $.message({ message: msg('attachmentDeleteSuccess') })
+ renderUploadList(relistNow)
+ })
+ },
+ function () {
+ $.message({ message: msg('attachmentDeleteFailed') })
+ }
+ )
+ })
+})
+
+function uploadImage(file) {
+ $.message({
+ message: msg('picUploading'),
+ autoClose: false
+ });
+ get_info(function (info) {
+ const adapter = getApiAdapter(info)
+ adapter.uploadFile(
+ file,
+ {
+ editorContent: $("textarea[name=text]").val(),
+ hideTag: info.hidetag,
+ showTag: info.showtag,
+ memoLock: info.memo_lock
+ },
+ function (entity) {
+ 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 saveSettingsPanel() {
+ var apiUrl = $('#apiUrl').val()
+ if (apiUrl.length > 0 && !apiUrl.endsWith('/')) {
+ apiUrl += '/'
+ }
+ var apiTokens = $('#apiTokens').val()
+ var customSettings = buildCustomSettingsPayload()
+
+ if (!apiUrl && !apiTokens) {
+ chrome.storage.sync.set(customSettings, function () {
+ $.message({ message: msg('saveSuccess') })
+ $('#blog_info').slideUp(200)
+ })
+ return
+ }
+
+ window.MemosApiModern.authWithFallback(apiUrl, apiTokens, function (auth) {
+ if (!auth || auth.userId == null) {
+ $.message({ message: msg('invalidToken') })
+ return
+ }
+
+ chrome.storage.sync.set(
+ Object.assign({}, customSettings, {
+ apiUrl: apiUrl,
+ apiTokens: apiTokens,
+ userid: auth.userId,
+ memoUiPath: auth.uiPath || 'memos',
+ apiFlavor: ''
+ }),
+ function () {
+ $.message({ message: msg('saveSuccess') })
+ $('#blog_info').hide()
+
+ // Auto-detect API flavor once; keep default behavior when unknown.
+ if (window.MemosApiAdapter && typeof window.MemosApiAdapter.probeFlavor === 'function') {
+ window.MemosApiAdapter.probeFlavor(apiUrl, apiTokens, function (res) {
+ const flavor = res && res.flavor ? res.flavor : ''
+ 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) {
+ chrome.tabs.create({url:info.apiUrl})
+ })
+})
+
+// 0.23.1版本 GET api/v1/{parent}/tags 接口已移除,参考 https://github.com/usememos/memos/issues/4161
+$('#tags').click(function () {
+ get_info(function (info) {
+ if (info.apiUrl) {
+ var tagDom = "";
+ const adapter = getApiAdapter(info)
+
+ const renderTags = function (tags) {
+ const uniTags = [...new Set((Array.isArray(tags) ? tags : []).filter(Boolean))]
+ $.each(uniTags, function (_, tag) {
+ tagDom += '#' + tag + '';
+ });
+ tagDom += ''
+ $("#taglist").html(tagDom).slideToggle(500)
+ }
+
+ adapter.listTags(renderTags, function () {
+ $.message({ message: msg('placeApiUrl') })
+ })
+ } else {
+ $.message({
+ message: msg('placeApiUrl')
+ })
+ }
+ })
+})
+
+$(document).on("click","#hideTag",function () {
+ $('#taghide').slideToggle(500)
+ $('#hideInput').trigger('focus')
+})
+
+$('#saveTag').click(function () {
+ chrome.storage.sync.set(
+ {
+ hidetag: $('#hideInput').val(),
+ showtag: $('#showInput').val()
+ },
+ function () {
+ $.message({
+ message: msg('saveSuccess')
+ })
+ $('#taghide').hide()
+ }
+ )
+})
+
+$('#lock').click(function () {
+ $("#lock-wrapper").toggleClass( "!hidden", 1000 );
+})
+
+$(document).on("click",".item-lock",function () {
+ $("#lock-wrapper").toggleClass( "!hidden", 1000 );
+ $("#lock-now").text($(this).text())
+ _this = $(this)[0].dataset.type;
+ currentMemoLock = _this
+ chrome.storage.sync.set(
+ {memo_lock: _this}
+ )
+})
+
+$('#search').click(function () {
+ get_info(function (info) {
+ const pattern = $("textarea[name=text]").val()
+ if (info.status) {
+ $("#randomlist").html('').hide()
+ var searchDom = ""
+ if(pattern){
+ const adapter = getApiAdapter(info)
+
+ adapter.searchMemos(
+ pattern,
+ function (searchData) {
+ if(searchData.length == 0){
+ $.message({
+ message: msg('searchNone')
+ })
+ }else{
+ for(var i=0;i < searchData.length;i++){
+ var memosID = getMemoUid(searchData[i])
+ var timeText = memoFromNow(searchData[i])
+ searchDom += ''+(searchData[i].content || '').replace(/!\[.*?\]\((.*?)\)/g,'

').replace(/\[(.*?)\]\((.*?)\)/g,'
$1 ')+'
'
+ var resources = (searchData[i].attachments && searchData[i].attachments.length > 0) ? searchData[i].attachments : ((searchData[i].resources && searchData[i].resources.length > 0) ? searchData[i].resources : (searchData[i].resourceList || []));
+ if(resources && resources.length > 0){
+ for(var j=0;j < resources.length;j++){
+ var restype = (resources[j].type || '').slice(0,5);
+ var resexlink = resources[j].externalLink
+ var resLink = '',fileId=''
+ if(resexlink){
+ resLink = resexlink
+ }else{
+ resLink = buildV1ResourceStreamUrl(info, resources[j])
+ }
+ if (!resLink) {
+ continue
+ }
+ if(restype == 'image'){
+ if (adapter.needsAuthenticatedImagePreview()) {
+ searchDom += '
![]()
'
+ } else {
+ searchDom += '

'
+ }
+ }
+ if(restype !== 'image'){
+ searchDom += '
'+resources[j].filename+''
+ }
+ }
+ }
+ searchDom += '
'
+ }
+ window.ViewImage && ViewImage.init('.random-image')
+ $("#randomlist").html(searchDom).slideDown(500);
+ hydrateV1PreviewImages(info)
+ }
+ },
+ function (xhr) {
+ $.message({ message: msg('searchNone') })
+ }
+ )
+ }else{
+ $.message({
+ message: msg('searchNow')
+ })
+ }
+ } else {
+ $.message({
+ message: msg('placeApiUrl')
+ })
+ }
+})
+})
+
+$('#random').click(function () {
+ get_info(function (info) {
+ if (info.status) {
+ $("#randomlist").html('').hide()
+ const adapter = getApiAdapter(info)
+
+ adapter.listRandomMemos(
+ function (memos) {
+ let randomNum = Math.floor(Math.random() * (memos.length));
+ var randomData = memos[randomNum]
+ randDom(randomData)
+ },
+ function () {
+ $.message({ message: msg('placeApiUrl') })
+ }
+ )
+ } else {
+ $.message({
+ message: msg('placeApiUrl')
+ })
+ }
+ })
+})
+
+function randDom(randomData){
+ get_info(function (info) {
+ const adapter = getApiAdapter(info)
+ var memosID = getMemoUid(randomData)
+ var timeText = memoFromNow(randomData)
+ var randomDom = ''+(randomData && randomData.content ? randomData.content : '').replace(/!\[.*?\]\((.*?)\)/g,'

').replace(/\[(.*?)\]\((.*?)\)/g,'
$1 ')+'
'
+ var resources = (randomData.attachments && randomData.attachments.length > 0) ? randomData.attachments : ((randomData.resources && randomData.resources.length > 0) ? randomData.resources : (randomData.resourceList || []));
+ if(resources && resources.length > 0){
+ for(var j=0;j < resources.length;j++){
+ var restype = (resources[j].type || '').slice(0,5);
+ var resexlink = resources[j].externalLink
+ var resLink = '',fileId=''
+ if(resexlink){
+ resLink = resexlink
+ }else{
+ resLink = buildV1ResourceStreamUrl(info, resources[j])
+ }
+ if (!resLink) {
+ continue
+ }
+ if(restype == 'image'){
+ if (adapter.needsAuthenticatedImagePreview()) {
+ randomDom += '
![]()
'
+ } else {
+ randomDom += '

'
+ }
+ }
+ if(restype !== 'image'){
+ randomDom += '
'+resources[j].filename+''
+ }
+ }
+ }
+ randomDom += '
'
+ window.ViewImage && ViewImage.init('.random-image')
+ $("#randomlist").html(randomDom).slideDown(500);
+ hydrateV1PreviewImages(info)
+ })
+}
+
+$(document).on("click","#random-link",function () {
+ var memoUid = $("#random-link").data('uid');
+ get_info(function (info) {
+ const path = (info.memoUiPath || 'memos').replace(/^\/+|\/+$/g, '')
+ chrome.tabs.create({url:info.apiUrl + path + "/" + memoUid})
+ })
+})
+
+$(document).on("click","#random-delete",function () {
+get_info(function (info) {
+ var memosName = $("#random-delete").data('name');
+ var memoId = $("#random-delete").data('id');
+
+ const adapter = getApiAdapter(info)
+ adapter.archiveMemo(
+ { name: memosName, id: memoId },
+ function () {
+ $("#randomlist").html('').hide()
+ $.message({ message: msg('archiveSuccess') })
+ },
+ function () {
+ $.message({ message: msg('archiveFailed') })
+ }
+ )
+})
+})
+
+$(document).on("click",".item-container",function () {
+ var tagHtml = $(this).text()+" "
+ add(tagHtml);
+})
+
+$('#newtodo').click(function () {
+ var tagHtml = "\n- [ ] "
+ add(tagHtml);
+})
+
+$('#getlink').click(function () {
+ chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {
+ var linkHtml = " ["+tab.title+"]("+tab.url+") "
+ if(tab.url){
+ add(linkHtml);
+ }else{
+ $.message({
+ message: msg('getTabFailed')
+ })
+ }
+ })
+})
+
+$('#upres').click(async function () {
+ $('#inFile').click()
+})
+
+$('#inFile').on('change', function(data){
+ var fileVal = $('#inFile').val();
+ var file = null
+ if(fileVal == '') {
+ return;
+ }
+ file= this.files[0];
+ uploadImage(file)
+});
+
+function add(str) {
+ var tc = document.getElementById("content");
+ var tclen = tc.value.length;
+ tc.focus();
+ if(typeof document.selection != "undefined"){
+ document.selection.createRange().text = str;
+ }else{
+ tc.value =
+ tc.value.substr(0, tc.selectionStart) +
+ str +
+ tc.value.substring(tc.selectionStart, tclen);
+ }
+}
+
+$('#blog_info_edit').click(function () {
+ $('#blog_info').slideToggle()
+})
+
+$('#content_submit_text').click(function () {
+ var contentVal = $("textarea[name=text]").val()
+ var contentToSend = resolveSendContent(
+ contentVal,
+ relistNow,
+ $('#attachmentOnlyDefaultText').val()
+ )
+ if(contentToSend){
+ sendText(contentToSend)
+ }else{
+ $.message({
+ message: msg('placeContent')
+ })
+ }
+})
+
+function getOne(memosId){
+ get_info(function (info) {
+ if (info.apiUrl) {
+ $("#randomlist").html('').hide()
+ const adapter = getApiAdapter(info)
+ adapter.getMemo(memosId, function (memoEntity) {
+ randDom(memoEntity)
+ })
+ } else {
+ $.message({
+ message: msg('placeApiUrl')
+ })
+ }
+ })
+}
+
+function sendText(preparedContent) {
+ get_info(function (info) {
+ if (info.status) {
+ $.message({
+ message: msg('memoUploading')
+ })
+ //$("#content_submit_text").attr('disabled','disabled');
+ 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 = content.match(/(#[^\s#]+)/)
+ var sendvisi = info.memo_lock || ''
+ if(nowTag){
+ if(nowTag[1] == showTag){
+ sendvisi = 'PUBLIC'
+ }else if(nowTag[1] == hideTag){
+ sendvisi = 'PRIVATE'
+ }
+ }
+
+ const adapter = getApiAdapter(info)
+ adapter.createMemo(
+ {
+ content: content,
+ visibility: sendvisi,
+ resourceIdList: info.resourceIdList
+ },
+ function (data) {
+ chrome.storage.sync.set(
+ { open_action: '', open_content: '', resourceIdList: [] },
+ function () {
+ $.message({ message: msg('memoSuccess') })
+ $("textarea[name=text]").val('')
+ relistNow = []
+ renderUploadList(relistNow)
+ randDom(data)
+ }
+ )
+ },
+ function () {
+ chrome.storage.sync.set(
+ { open_action: '', open_content: '', resourceIdList: [] },
+ function () {
+ $.message({ message: msg('memoFailed') })
+ }
+ )
+ }
+ )
+ } else {
+ $.message({
+ message: msg('placeApiUrl')
+ })
+ }
+ })
+}
\ No newline at end of file
diff --git a/edge/js/relativeTime.js b/edge/js/relativeTime.js
new file mode 100644
index 0000000..898eee6
--- /dev/null
+++ b/edge/js/relativeTime.js
@@ -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;c0,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)}}}));
\ No newline at end of file
diff --git a/edge/js/view-image.js b/edge/js/view-image.js
new file mode 100644
index 0000000..9d5a8cf
--- /dev/null
+++ b/edge/js/view-image.js
@@ -0,0 +1,12 @@
+/**
+ * ViewImage.min.js 2.0.2
+ * MIT License - http://www.opensource.org/licenses/mit-license.php
+ * https://tokinx.github.io/ViewImage/
+ */
+var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.createTemplateTagFirstArg=function(b){return b.raw=b};$jscomp.createTemplateTagFirstArgWithRaw=function(b,a){b.raw=a;return b};$jscomp.arrayIteratorImpl=function(b){var a=0;return function(){return a\n \n \n \n \n ',
+"text/html").body.firstChild,g=function(f){var h={Escape:"close",ArrowLeft:"tools__flip-prev",ArrowRight:"tools__flip-next"};h[f.key]&&e.querySelector(".view-image-"+h[f.key]).click()},l=function(f){var h=new Image,k=e.querySelector(".view-image-lead");k.className="view-image-lead view-image-lead__out";setTimeout(function(){k.innerHTML="";h.onload=function(){setTimeout(function(){k.innerHTML='
';k.className="view-image-lead view-image-lead__in"},100)};
+h.src=f},300)};document.body.appendChild(e);l(d);window.addEventListener("keydown",g);e.onclick=function(f){f.target.closest(".view-image-close")?(window.removeEventListener("keydown",g),e.onclick=null,e.classList.add("view-image__out"),setTimeout(function(){return e.remove()},290)):f.target.closest(".view-image-tools__flip")&&(c=f.target.closest(".view-image-tools__flip-prev")?0===c?a.length-1:c-1:c===a.length-1?0:c+1,l(a[c]),e.querySelector(".view-image-index").innerHTML=c+1)}}}})();
diff --git a/edge/js/zh-cn.js b/edge/js/zh-cn.js
new file mode 100644
index 0000000..21cf228
--- /dev/null
+++ b/edge/js/zh-cn.js
@@ -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}));
\ No newline at end of file
diff --git a/edge/manifest.json b/edge/manifest.json
new file mode 100644
index 0000000..dd01993
--- /dev/null
+++ b/edge/manifest.json
@@ -0,0 +1,39 @@
+{
+ "manifest_version": 3,
+ "name": "__MSG_extName__",
+ "default_locale": "en",
+ "version": "2026.04.23",
+ "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",
+ "icons": {
+ "128": "assets/logo.png",
+ "16": "assets/logo.png",
+ "48": "assets/logo.png"
+ },
+ "background": {
+ "service_worker": "js/background.js"
+ },
+ "permissions": [
+ "tabs",
+ "scripting",
+ "windows",
+ "storage",
+ "activeTab",
+ "contextMenus"
+ ],
+ "host_permissions": ["http://*/*", "https://*/*"],
+ "commands": {
+ "open-extension": {
+ "description": "Open my extension",
+ "suggested_key": {
+ "default": "Ctrl+Shift+F",
+ "mac": "MacCtrl+Shift+F"
+ }
+ }
+ }
+}
diff --git a/edge/popup.html b/edge/popup.html
new file mode 100644
index 0000000..ad1741e
--- /dev/null
+++ b/edge/popup.html
@@ -0,0 +1,194 @@
+
+
+
+
+
+
+ MEMOS
+
+
+
+ MEMOS
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/firefox/js/oper.js b/firefox/js/oper.js
index 461c6be..d9ed077 100644
--- a/firefox/js/oper.js
+++ b/firefox/js/oper.js
@@ -44,6 +44,7 @@ function initProportionalEditorResize() {
const nonEditorHeight = Math.max(0, Math.ceil(document.body.scrollHeight - initialRect.height))
let maxScale = 1
let currentScale = 1
+ let nextScale = 1
let dragging = false
let dragStartX = 0
let dragStartY = 0
@@ -54,8 +55,21 @@ function initProportionalEditorResize() {
return Math.min(Math.max(scale, 1), maxScale)
}
+ const showPreviewScale = (scale) => {
+ const previewScale = clampScale(scale)
+ const relativeScale = previewScale / currentScale
+ editor.style.transformOrigin = 'top left'
+ editor.style.transform = `scale(${relativeScale})`
+ }
+
+ const clearPreviewScale = () => {
+ editor.style.transform = ''
+ editor.style.transformOrigin = ''
+ }
+
const applyScale = (scale) => {
currentScale = clampScale(scale)
+ nextScale = currentScale
editor.style.width = `${Math.round(baseW * currentScale)}px`
editor.style.height = `${Math.round(baseH * currentScale)}px`
}
@@ -75,6 +89,7 @@ function initProportionalEditorResize() {
}
const computeMaxScale = () => {
+ if (dragging) return
// 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
@@ -94,6 +109,9 @@ function initProportionalEditorResize() {
if (!dragging) return
dragging = false
handle.classList.remove('dragging')
+ clearPreviewScale()
+ applyScale(nextScale)
+ computeMaxScale()
persistScale()
}
@@ -103,7 +121,8 @@ function initProportionalEditorResize() {
const dy = ev.clientY - dragStartY
const widthScale = (baseW * dragStartScale + dx) / baseW
const heightScale = (baseH * dragStartScale + dy) / baseH
- applyScale(Math.max(widthScale, heightScale))
+ nextScale = clampScale(Math.max(widthScale, heightScale))
+ showPreviewScale(nextScale)
}
const startDrag = (ev) => {
@@ -112,6 +131,8 @@ function initProportionalEditorResize() {
dragStartX = ev.clientX
dragStartY = ev.clientY
dragStartScale = currentScale
+ nextScale = currentScale
+ showPreviewScale(currentScale)
handle.classList.add('dragging')
if (typeof handle.setPointerCapture === 'function') {
try {
@@ -209,6 +230,10 @@ function updateLockNowText(lockType) {
}
}
+function showRandomList(content) {
+ $('#randomlist').stop(true, true).html(content).show()
+}
+
applyDayjsLocaleByUiLanguage(typeof window.getUiLanguage === 'function' ? window.getUiLanguage() : 'auto')
if (isFullscreenMode()) {
@@ -994,7 +1019,7 @@ $('#search').click(function () {
searchDom += ''
}
window.ViewImage && ViewImage.init('.random-image')
- $("#randomlist").html(searchDom).slideDown(500);
+ showRandomList(searchDom)
hydrateV1PreviewImages(info)
}
},
@@ -1073,7 +1098,7 @@ function randDom(randomData){
}
randomDom += ''
window.ViewImage && ViewImage.init('.random-image')
- $("#randomlist").html(randomDom).slideDown(500);
+ showRandomList(randomDom)
hydrateV1PreviewImages(info)
})
}