diff --git a/.DS_Store b/.DS_Store index aa13abe..4a054aa 100644 Binary files a/.DS_Store and b/.DS_Store differ 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` 模式。 - -123 - -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 输入框。 - -![iShot_2023-03-05](https://user-images.githubusercontent.com/1472390/222957393-fc2e933e-b18f-4e69-a8c0-4609f84a0a90.png) - -2023.03.05 新增指定标签“仅自己可见”或“所有人可见”;图片上传文件名添加时间戳。 - -2023.02.26 更改 Memos 可见范围按钮样式。支持 Ctrl/Meta + Enter 记下。点击标题跳转主站。 - -2023.02.25 修复 v0.11.0 下随机按钮失效。(api amount 失效导致,换用 stats 获取总条数) - -![iShot_2023-02-06_19 16 28](https://user-images.githubusercontent.com/1472390/216958098-1f4fab2a-e77c-41bd-8ba3-5786f42744d7.png) - -2023.02.07 新增发布后显示最新一条 Memo ,具体一条新增归档按钮。 - -2023.02.06 新增搜索按钮;新增图片灯箱。 - -![iShot_2023-02-04_20 42 40](https://user-images.githubusercontent.com/1472390/216768533-4a93124a-666e-4617-a60b-29c826dc1584.png) - -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` 模式。 + +123 + +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 输入框。 + +![iShot_2023-03-05](https://user-images.githubusercontent.com/1472390/222957393-fc2e933e-b18f-4e69-a8c0-4609f84a0a90.png) + +2023.03.05 新增指定标签“仅自己可见”或“所有人可见”;图片上传文件名添加时间戳。 + +2023.02.26 更改 Memos 可见范围按钮样式。支持 Ctrl/Meta + Enter 记下。点击标题跳转主站。 + +2023.02.25 修复 v0.11.0 下随机按钮失效。(api amount 失效导致,换用 stats 获取总条数) + +![iShot_2023-02-06_19 16 28](https://user-images.githubusercontent.com/1472390/216958098-1f4fab2a-e77c-41bd-8ba3-5786f42744d7.png) + +2023.02.07 新增发布后显示最新一条 Memo ,具体一条新增归档按钮。 + +2023.02.06 新增搜索按钮;新增图片灯箱。 + +![iShot_2023-02-04_20 42 40](https://user-images.githubusercontent.com/1472390/216768533-4a93124a-666e-4617-a60b-29c826dc1584.png) + +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 0000000..4b880cd Binary files /dev/null and b/edge/assets/logo.png differ diff --git a/edge/assets/logo_24x24.png b/edge/assets/logo_24x24.png new file mode 100644 index 0000000..e3fa21b Binary files /dev/null and b/edge/assets/logo_24x24.png differ diff --git a/edge/css/main.css b/edge/css/main.css new file mode 100644 index 0000000..ae1df6b --- /dev/null +++ b/edge/css/main.css @@ -0,0 +1,658 @@ +button, input, textarea { + font-family: inherit; + font-size: 100%; + font-weight: inherit; + line-height: inherit; + color: inherit; + margin: 0; + padding: 0; + border: none; + outline: none; +} +input:focus::-moz-placeholder ,.common-editor-inputer:focus::-moz-placeholder { + color: #d3d3d3 +} +input::placeholder ,.common-editor-inputer::placeholder { + color: #999; +} +input:focus::placeholder ,.common-editor-inputer:focus::placeholder { + color: #d3d3d3 +} + +.body{ + min-width:360px; + background-color: #f6f5f4; + padding:0 1rem 1rem; + font-family: eafont,PingFang SC,Hiragino Sans GB,Microsoft YaHei,STHeiti,WenQuanYi Micro Hei,Helvetica,Arial,sans-serif; + font-size: 16px; + font-size: 1rem; + line-height: 1.5; + position: relative; +} + +a{color: #555;} +.title{ + width: 100px; + cursor: pointer; + font-size: 1.125rem; + font-weight: 700; + line-height: 2.5rem; + color: rgb(55,65,81); +} +.memo-editor,.random-item{ + border: 2px solid rgb(229,231,235); + border-radius: .5rem; + background-color: rgb(255,255,255); + margin-top:0.8rem; + padding: 0.6rem; +} +.memo-editor{ + position: relative; + resize: none; + overflow: visible; + box-sizing: border-box; + contain: layout; + display: flex; + flex-direction: column; +} + +.memo-editor-header{ + position: sticky; + top: .5rem; + z-index: 3; + height: 0; + display: block; +} + +#editor-resize-handle{ + position: absolute; + right: .35rem; + bottom: .35rem; + width: 14px; + height: 14px; + border-right: 2px solid #bbb; + border-bottom: 2px solid #bbb; + cursor: nwse-resize; + opacity: .8; + user-select: none; + touch-action: none; +} + +#editor-resize-handle:hover{ + opacity: 1; + border-right-color: #888; + border-bottom-color: #888; +} + +.body.fullscreen #editor-resize-handle{ + display: none; +} + +.body.fullscreen #fullscreen{ + display: none; +} +.random-item{ + border: 1px solid rgb(229,231,235); + color: #666; + padding: 1rem; +} +.random-time{font-size:13px;margin-bottom:6px;color: #999;font-style:italic;} +.random-time span{float: right;margin-top: -3px;cursor: pointer;} +.random-time span svg.icon{width:15px;height:15px;padding:4px 6px 0;opacity: 0.6;margin-left:4px;} +.random-time span svg.icon:hover{opacity: 1;} + +.random-image{width:180px;height:180px;max-width:100%;object-fit: cover;border-radius: .25rem;margin:5px 5px 5px 0;} +.random-content{width:100%; + max-width:100%; + font-size: 1rem; + line-height: 1.5rem; + overflow-wrap: anywhere; + word-break: normal;} +.btns-container{text-align:right;} +.memo-editor #fullscreen{ + position: absolute; + right: 0; + top: 0; + z-index: 2; + border: 1px solid rgb(229,231,235); + border-radius: .25rem; + background-color: rgb(255,255,255); + color: #666; + font-size: .75rem; + line-height: 1; + padding: .25rem; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + opacity: .9; +} + +.memo-editor #fullscreen svg{ + display: block; +} +.memo-editor #fullscreen:hover{ + opacity: 1; + background-color: rgb(243,244,246); +} +.common-editor-inputer,input.inputer{ + font-family: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace; + height: 100%; + width: 100%; + resize: none; + overflow-y: auto; + overflow-x: hidden; + white-space: pre-wrap; + background-color: transparent; + font-size: 1rem; + min-height: 40px; + scrollbar-width: none; + line-height: 1.5rem; +} + +.common-editor-inputer{ + padding-right: 1.5rem; + flex: 1 1 auto; + min-height: 0; + display: block; + box-sizing: border-box; +} + +.body.fullscreen{ + min-width: 0; + width: 100vw; + height: 100vh; + box-sizing: border-box; +} + +.body.fullscreen .common-editor-inputer{ + min-height: 60vh; +} +input.inputer{border-bottom: 1px solid #ccc;width:75%;} + +.settings-panel{ + display: flex; + flex-direction: column; + gap: .75rem; + margin-top: .75rem; +} + +.settings-section{ + border: 1px solid rgb(229,231,235); + border-radius: .5rem; + background-color: rgb(255,255,255); + padding: .75rem; + display: flex; + flex-direction: column; + gap: .6rem; +} + +.settings-section-title{ + font-size: .9rem; + font-weight: 700; + color: rgb(55,65,81); +} + +.settings-section-desc{ + font-size: .75rem; + line-height: 1.35; + color: #7a7a7a; +} + +.settings-input{ + width: 100% !important; + box-sizing: border-box; + border: 1px solid rgb(229,231,235); + border-radius: .35rem; + background-color: #fafafa; + padding: .55rem .7rem; +} + +.settings-input:focus{ + border-color: rgb(22,163,74); + background-color: rgb(255,255,255); +} + +.settings-textarea{ + resize: vertical; + min-height: 4.5rem; + white-space: pre-wrap; +} + +.settings-actions{ + display: flex; + justify-content: flex-end; +} + +#saveKey{margin:0;flex:1;} + +.common-tools-wrapper { + position: relative; + display: flex; + width: 100%; + flex-direction: row; + align-items: center; + margin-top: 1rem; + justify-content: space-between; +} + +.upload-list-wrapper{ + margin-top: .5rem; +} +.upload-list-title{ + font-size: .875rem; + color: #999; + margin-top: .5rem; + margin-bottom: .25rem; +} +.upload-list{ + border: 1px solid rgb(229,231,235); + border-radius: .5rem; + background-color: rgb(255,255,255); + padding: .25rem; +} +.upload-empty{ + padding: .5rem .75rem; + font-size: .875rem; + color: #999; +} +.upload-item{ + display:flex; + align-items:center; + justify-content: space-between; + padding: .4rem .5rem; + border-radius: .25rem; + color:#666; +} +.upload-item + .upload-item{ + border-top: 1px solid rgb(243,244,246); +} +.upload-item.drag-over{ + background-color: rgb(243,244,246); +} +.upload-left{ + display:flex; + align-items:center; + min-width: 0; + gap: .5rem; +} +.upload-drag{ + cursor: grab; + opacity: .6; + user-select: none; +} +.upload-filename{ + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: .875rem; +} +.upload-del{ + cursor:pointer; + font-size: 1rem; + line-height: 1; + padding: .15rem .35rem; + border-radius: .25rem; + opacity: .6; + background-color: transparent; +} +.upload-del:hover{ + opacity: 1; + background-color: rgb(243,244,246); +} +.common-tools-container { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; +} +.confirm-btn { + display: inline-flex; + cursor: pointer; + flex-direction: row; + align-items: center; + justify-content: center; + background-color: rgb(22,163,74); + padding:0 1rem; + font-size: .875rem; + line-height: 2rem; + color: rgb(255,255,255); + box-shadow: 0 1px 3px 0 + rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1); + user-select: none; + border-radius: .25rem; + border-style: none; + opacity: .6; +} +.confirm-btn:hover { + opacity: 1; +} +.confirm-btn:disabled{ + opacity: .6; + cursor: not-allowed; +} + +.common-tools-container .mr-5{margin-right: .5rem;} +.common-tools-container svg.icon,#blog_info_edit svg.icon{width:24px;height:24px;opacity: 0.6;cursor: pointer;} +#newtodo svg.icon{padding-top: 2px;} +#tags svg.icon{padding: 2px;width:23px;height:23px;} +#random svg.icon{padding:2px;width:19px;height:19px;} +.common-tools-container svg.icon:hover{opacity: 1;} + +#locked,#taglist,#visibilitylis,#randomlist{display: none;} +.tag-list,.visibility-list { + margin-top: .5rem; + max-height: 13rem; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + font-family: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace; +} +.tag-list>.item-container,.visibility-list >.item-lock,.tag-list .hidetag{ + display: inline-block; + background-color: #666; + cursor: pointer; + padding: .2rem .5rem; + border-radius: .25rem; + font-size: .875rem; + line-height: 1.25rem; + color: #fff; + margin:0 6px 6px 0; +} +.tag-list .hidetag{padding:0;float:right;background-color:#ddd;} +.tag-list .hidetag:hover{background-color:#666;} +.tag-hide{display: none;} +.tag-hide input.inputer{width:40%;font-size:11px;} + +.visibility-list .item-lock.lock-now{ + background-color:rgb(22,163,74); +} +#blog_info_edit{ + position: absolute; + right: 1rem; + top: 0.5rem; +} + +.lang-switcher{ + position: absolute; + right: 3.5rem; + top: .55rem; +} + +.lang-toggle{ + border: none; + border-radius: 0; + background-color: transparent; + color: #666; + min-width: 24px; + height: 24px; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + box-sizing: border-box; + padding: 0; + opacity: .6; +} + +.lang-toggle:hover, +.lang-toggle[aria-expanded="true"]{ + background-color: transparent; + color: #666; + opacity: 1; +} + +.lang-toggle-text{ + display: inline-block; + min-width: 24px; + text-align: center; + font-size: 12px; + line-height: 24px; + font-weight: 700; + letter-spacing: .02em; +} + +.lang-menu{ + position: absolute; + top: calc(100% + .35rem); + right: 0; + min-width: 8rem; + padding: .25rem; + border: 1px solid rgb(229,231,235); + border-radius: .5rem; + background-color: rgb(255,255,255); + box-shadow: 0 8px 24px rgba(15,23,42,.12); + z-index: 10; +} + +.lang-menu.hidden{ + display: none; +} + +.lang-menu-item{ + width: 100%; + display: block; + text-align: left; + padding: .4rem .5rem; + border-radius: .35rem; + background: transparent; + color: #555; + font-size: .75rem; + line-height: 1.25rem; + cursor: pointer; +} + +.lang-menu-item:hover{ + background-color: rgb(243,244,246); +} + +.lang-menu-item.active{ + background-color: rgb(220,252,231); + color: rgb(22,101,52); + font-weight: 600; +} + + +.tip{ + margin-left: 36%; + max-width: 640px; + position: fixed; + text-align: center; + top: 15px; + width: 58%; + z-index: 10001; + left: 50%; + margin-left: -320px; +} + +.tip-info{ + background: -webkit-gradient(linear,left top,right top,from(#9c51ff),to(#816bff)); + background: -webkit-linear-gradient(90deg,#9c51ff,#816bff); + background: linear-gradient(90deg,#9c51ff,#816bff); + -moz-box-shadow: 3px 3px 20px #d7ceff38; + -webkit-box-shadow: 3px 3px 20px #d7ceff38; + box-shadow: 3px 3px 20px #d7ceff38; + color: #fff; + font-size: 12px; + padding: 8px 40px; + display: inline-block; + border-radius: 3px; + margin: 0; + line-height: 1; + font-weight: 300; +} + +@-webkit-keyframes bounceIn { + 0% { + opacity: 0; + -webkit-transform: scale(.3); + } + 50% { + opacity: 1; + -webkit-transform: scale(1); + } + 70% { + -webkit-transform: scale(.95); + } + 100% { + -webkit-transform: scale(1); + } +} + +@keyframes bounceIn { + 0% { + opacity: 0; + transform: scale(.3); + } + 50% { + opacity: 1; + transform: scale(1); + } + 70% { + transform: scale(.95); + } + 100% { + transform: scale(1); + } +} + +.bounceIn { + -webkit-animation-name: bounceIn; + animation-name: bounceIn; +} +.animate { + -webkit-animation-duration: .3s; + animation-duration: .3s; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; +} + +.\!hidden{ + display: none!important; +} +.selector-wrapper { + position: relative; + display: flex; + height: 2rem; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start +} +.selector-wrapper>.current-value-container { + display: flex; + height: 100%; + width: 100%; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + flex-direction: row; + align-items: center; + justify-content: space-between; + border-radius: .25rem; + border-width: 1px; + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); + padding-left: .5rem; + padding-right: .25rem; +} +.selector-wrapper>.current-value-container>.value-text { + margin-right: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: .875rem +} +.selector-wrapper>.current-value-container>.value-text { + width: calc(100% - 20px) +} +.selector-wrapper>.current-value-container>.lock-text { + margin-right: .25rem; + display: flex; + width: 1rem; + flex-shrink: 0; + flex-direction: row; + align-items: center; + justify-content: center +} +.selector-wrapper>.current-value-container>.arrow-text { + display: flex; + width: 1rem; + flex-shrink: 0; + flex-direction: row; + align-items: center; + justify-content: center +} +.selector-wrapper>.current-value-container>.arrow-text>.icon-img { + height: auto; + width: 1rem; + opacity: .4 +} +.selector-wrapper>.items-wrapper { + position: absolute; + bottom: 100%; + left: 0px; + z-index: 1; + margin-top: .25rem; + margin-left: -.5rem; + display: flex; + width: auto; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + overflow-y: auto; + border-radius: .375rem; + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); + padding: .25rem; + -ms-overflow-style: none; + scrollbar-width: none +} +.selector-wrapper>.items-wrapper::-webkit-scrollbar { + display: none +} +.selector-wrapper>.items-wrapper { + min-width: calc(100% + 16px); + max-height: 256px; + box-shadow: 0 0 8px #0003 +} +.selector-wrapper>.items-wrapper>.item-lock { + display: flex; + width: 100%; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + flex-direction: column; + align-items: flex-start; + justify-content: flex-start; + white-space: nowrap; + border-radius: .25rem; + padding-left: .75rem; + padding-right: .75rem; + font-size: .875rem; + line-height: 2rem +} +.selector-wrapper>.items-wrapper>.item-lock:hover { + --tw-bg-opacity: 1; + background-color: rgb(243 244 246 / var(--tw-bg-opacity)) +} +.selector-wrapper>.items-wrapper>.item-lock.selected { + --tw-text-opacity: 1; + color: rgb(22 163 74 / var(--tw-text-opacity)) +} + +.selector-wrapper>.items-wrapper>.tip-text { + padding: .25rem .75rem; + font-size: .875rem; + --tw-text-opacity: 1; + color: rgb(75 85 99 / var(--tw-text-opacity)) +} +.selector-wrapper>.selector-disabled { + cursor: not-allowed; + pointer-events: none; + --tw-bg-opacity: 1; + background-color: rgb(229 231 235 / var(--tw-bg-opacity)); + --tw-text-opacity: 1; + color: rgb(156 163 175 / var(--tw-text-opacity)) +} 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(`![](${info.srcUrl})` + '\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 += '
'+timeText+'
'+(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 = '
'+timeText+'
'+(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
\n
\n
\n
\n '+ +(c+1)+"/"+a.length+'\n
\n
\n
\n \n
\n
\n \n
\n
\n
\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='ViewImage';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) }) }