Add Edge extension and update release workflow

Add a complete Edge extension under edge/ (manifest, popup, CSS, JS, locales, assets, LICENSE) and a change.log. Update README with Edge/mobile notes and simplify content. Update GitHub Actions workflow to read edge/manifest.json version, package Edge builds, and include Edge asset in the release summary (also bump default release_tag). Minor update to firefox/js/oper.js and include .DS_Store change.
This commit is contained in:
jonny
2026-04-23 10:46:27 +08:00
parent 235401a14a
commit 88c92652c3
30 changed files with 4995 additions and 79 deletions
Vendored
BIN
View File
Binary file not shown.
+13 -1
View File
@@ -9,7 +9,7 @@ on:
release_tag: release_tag:
description: Release tag used for direct asset upload, for example v2026.04.23 description: Release tag used for direct asset upload, for example v2026.04.23
required: true required: true
default: 'v2026.04.23' default: 'v2026.04.24'
push: push:
tags: tags:
- 'v*' - 'v*'
@@ -48,8 +48,15 @@ jobs:
print(json.load(fp)['version']) print(json.load(fp)['version'])
PY 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 "chrome_version=$chrome_version" >> "$GITHUB_OUTPUT"
echo "firefox_version=$firefox_version" >> "$GITHUB_OUTPUT" echo "firefox_version=$firefox_version" >> "$GITHUB_OUTPUT"
echo "edge_version=$edge_version" >> "$GITHUB_OUTPUT"
- name: Build package files - name: Build package files
run: | run: |
@@ -59,6 +66,10 @@ jobs:
zip -qr "../dist/release/memos-bber-chrome-${{ steps.versions.outputs.chrome_version }}.zip" . zip -qr "../dist/release/memos-bber-chrome-${{ steps.versions.outputs.chrome_version }}.zip" .
popd >/dev/null 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 pushd firefox >/dev/null
zip -qr "../dist/release/memos-bber-firefox-${{ steps.versions.outputs.firefox_version }}.xpi" . zip -qr "../dist/release/memos-bber-firefox-${{ steps.versions.outputs.firefox_version }}.xpi" .
popd >/dev/null popd >/dev/null
@@ -77,4 +88,5 @@ jobs:
tag='${{ steps.release.outputs.tag }}' tag='${{ steps.release.outputs.tag }}'
echo "## Release assets" >> "$GITHUB_STEP_SUMMARY" 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 "- 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" echo "- Firefox: https://github.com/${{ github.repository }}/releases/download/$tag/memos-bber-firefox-${{ steps.versions.outputs.firefox_version }}.xpi" >> "$GITHUB_STEP_SUMMARY"
+10 -75
View File
@@ -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/ - Chrome 应用商店:https://chrome.google.com/webstore/detail/memos-bber/cbhjebjfccgchgbmfbobjmebjjckgofe/
- (审核中)FireFox 应用商店: https://addons.mozilla.org/zh-CN/firefox/addon/memos-bber/ - (审核中)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,其它版本不支持 - 随机和搜索功能在`0.24`以上版本支持私有权限的 memo,其它版本不支持
<details>
<summary>点击展开/折叠内容</summary>
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` 模式。
<img width="483" alt="123" src="https://github.com/lmm214/memos-bber/assets/1472390/4ce2edc2-ce64-44d5-b4ef-d2e79b9d6a1a">
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 发送设置。
</details>
+71
View File
@@ -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
<details>
<summary>点击展开/折叠内容</summary>
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` 模式。
<img width="483" alt="123" src="https://github.com/lmm214/memos-bber/assets/1472390/4ce2edc2-ce64-44d5-b4ef-d2e79b9d6a1a">
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 发送设置。
</details>
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Charles Chin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+188
View File
@@ -0,0 +1,188 @@
{
"extName": {
"message": "Memos"
},
"actionTitle": {
"message": "Send Memos"
},
"extDescription": {
"message": "memos: A lightweight, self-hosted memo hub."
},
"sendTo": {
"message": "SendTo Memos \"%s\""
},
"sendLinkTo": {
"message": "Send link to Memos"
},
"sendImageTo": {
"message": "Send image to Memos"
},
"saveBtn":{
"message": "Save"
},
"supportedMemosVersion": {
"message": "Compatible with Memos v0.15.0 - 0.27.x"
},
"settingsConnectionTitle": {
"message": "Connection"
},
"settingsConnectionDesc": {
"message": "Configure the Memos site URL and access token."
},
"settingsPostingTitle": {
"message": "Posting"
},
"settingsPostingDesc": {
"message": "Default text for attachment-only sends"
},
"placeApiUrl":{
"message": "Memos site URL"
},
"placeApiTokens":{
"message": "Memos Access Tokens"
},
"placeContent":{
"message": "What's on your mind..."
},
"lockPrivate":{
"message": "Private"
},
"lockProtected":{
"message": "Protected"
},
"lockPublic":{
"message": "Public"
},
"submitBtn":{
"message": "Save"
},
"placeHideInput":{
"message": "Default 'Private' tag name"
},
"placeShowInput":{
"message": "Default 'Everyone can see' Tag name"
},
"placeAttachmentOnlyDefaultText": {
"message": "Default text for attachment-only sends (leave blank to use built-in text)"
},
"uploadedListTitle": {
"message": "Uploaded files, Drag to reorder"
},
"uploadedListEmpty": {
"message": "No uploaded files"
},
"tipReorder": {
"message": "Drag to reorder"
},
"tipDeleteAttachment": {
"message": "Delete"
},
"attachmentDeleteSuccess": {
"message": "Deleted"
},
"attachmentDeleteFailed": {
"message": "Delete failed 😭"
},
"picDrag":{
"message": "Drag upload the image"
},
"picCancelDrag":{
"message": "Cancel upload"
},
"picUploading":{
"message": "Upload the picture..."
},
"picSuccess":{
"message": "Upload completed"
},
"picFailed":{
"message": "Uploading failed"
},
"picPending":{
"message": "Image uploading is in progress"
},
"saveSuccess":{
"message": "Save Info Success!"
},
"searchNow":{
"message": "What are you looking for?"
},
"searchNone":{
"message": "Try another word!"
},
"archiveSuccess":{
"message": "Archive Success 😊"
},
"archiveFailed":{
"message": "Archive Failed 😭"
},
"getTabFailed":{
"message": "Get Tab Failed 😭"
},
"memoUploading":{
"message": "Sending"
},
"memoSuccess":{
"message": "Success! 😊"
},
"memoFailed":{
"message": "Failed! 😭"
},
"invalidToken":{
"message": "Invalid token or url 😭"
},
"tipOpenSite": {
"message": "Open Memos"
},
"tipSettings": {
"message": "Settings"
},
"tipTags": {
"message": "Insert tag"
},
"tipTodo": {
"message": "Insert todo"
},
"tipUpload": {
"message": "Upload file"
},
"tipLink": {
"message": "Insert current tab link"
},
"tipRandom": {
"message": "Random memo"
},
"tipSearch": {
"message": "Search"
},
"tipVisibility": {
"message": "Visibility"
},
"tipSend": {
"message": "Send (Ctrl/⌘+Enter)"
},
"tipLanguage": {
"message": "Language"
},
"langAuto": {
"message": "Auto"
},
"langEnglish": {
"message": "English"
},
"langChineseSimplified": {
"message": "简体中文"
},
"langJapanese": {
"message": "日本語"
},
"langKorean": {
"message": "한국어"
},
"tipFullscreen": {
"message": "Open fullscreen editor"
},
"tipResize": {
"message": "Drag to resize (min: default size)"
}
}
+188
View File
@@ -0,0 +1,188 @@
{
"extName": {
"message": "Memos"
},
"actionTitle": {
"message": "Memos に送信"
},
"extDescription": {
"message": "memos: 軽量なセルフホスト型メモハブ。"
},
"sendTo": {
"message": "Memos に \"%s\" を送信"
},
"sendLinkTo": {
"message": "リンクを Memos に送信"
},
"sendImageTo": {
"message": "画像を Memos に送信"
},
"saveBtn": {
"message": "保存"
},
"supportedMemosVersion": {
"message": "Memos v0.15.0 - 0.27.x に対応"
},
"settingsConnectionTitle": {
"message": "接続設定"
},
"settingsConnectionDesc": {
"message": "Memos のURLとアクセストークンを設定します。"
},
"settingsPostingTitle": {
"message": "投稿設定"
},
"settingsPostingDesc": {
"message": "添付ファイルのみ送信時の既定テキスト"
},
"placeApiUrl": {
"message": "Memos サイトURL"
},
"placeApiTokens": {
"message": "Memos アクセストークン"
},
"placeContent": {
"message": "今のメモは…"
},
"lockPrivate": {
"message": "非公開"
},
"lockProtected": {
"message": "保護"
},
"lockPublic": {
"message": "公開"
},
"submitBtn": {
"message": "送信"
},
"placeHideInput": {
"message": "既定の「非公開」タグ名"
},
"placeShowInput": {
"message": "既定の「全員に公開」タグ名"
},
"placeAttachmentOnlyDefaultText": {
"message": "添付ファイルのみ送信時の既定テキスト(空欄で内蔵文言を使用)"
},
"uploadedListTitle": {
"message": "アップロード済みファイル(ドラッグで並べ替え)"
},
"uploadedListEmpty": {
"message": "アップロード済みファイルはありません"
},
"tipReorder": {
"message": "ドラッグして並べ替え"
},
"tipDeleteAttachment": {
"message": "削除"
},
"attachmentDeleteSuccess": {
"message": "削除しました"
},
"attachmentDeleteFailed": {
"message": "削除に失敗しました 😭"
},
"picDrag": {
"message": "画像をここにドラッグしてアップロード"
},
"picCancelDrag": {
"message": "アップロードをキャンセル"
},
"picUploading": {
"message": "画像をアップロード中..."
},
"picSuccess": {
"message": "アップロード完了"
},
"picFailed": {
"message": "アップロード失敗"
},
"picPending": {
"message": "画像のアップロードが進行中です"
},
"saveSuccess": {
"message": "保存しました!"
},
"searchNow": {
"message": "何を探していますか?"
},
"searchNone": {
"message": "別のキーワードを試してください!"
},
"archiveSuccess": {
"message": "アーカイブ成功 😊"
},
"archiveFailed": {
"message": "アーカイブ失敗 😭"
},
"getTabFailed": {
"message": "タブの取得に失敗 😭"
},
"memoUploading": {
"message": "送信中"
},
"memoSuccess": {
"message": "成功!😊"
},
"memoFailed": {
"message": "失敗!😭"
},
"invalidToken": {
"message": "無効なトークンまたはURL 😭"
},
"tipOpenSite": {
"message": "Memos を開く"
},
"tipSettings": {
"message": "設定"
},
"tipTags": {
"message": "タグを挿入"
},
"tipTodo": {
"message": "ToDo を挿入"
},
"tipUpload": {
"message": "ファイルをアップロード"
},
"tipLink": {
"message": "現在のタブのリンクを挿入"
},
"tipRandom": {
"message": "ランダムメモ"
},
"tipSearch": {
"message": "検索"
},
"tipVisibility": {
"message": "公開範囲"
},
"tipSend": {
"message": "送信(Ctrl/⌘+Enter"
},
"tipLanguage": {
"message": "言語"
},
"langAuto": {
"message": "自動"
},
"langEnglish": {
"message": "English"
},
"langChineseSimplified": {
"message": "简体中文"
},
"langJapanese": {
"message": "日本語"
},
"langKorean": {
"message": "한국어"
},
"tipFullscreen": {
"message": "全画面で編集"
},
"tipResize": {
"message": "ドラッグで拡大/縮小(最小:初期サイズ)"
}
}
+188
View File
@@ -0,0 +1,188 @@
{
"extName": {
"message": "Memos"
},
"actionTitle": {
"message": "Memos 보내기"
},
"extDescription": {
"message": "memos: 가볍고 셀프호스팅 가능한 메모 허브."
},
"sendTo": {
"message": "Memos로 \"%s\" 보내기"
},
"sendLinkTo": {
"message": "링크를 Memos로 보내기"
},
"sendImageTo": {
"message": "이미지를 Memos로 보내기"
},
"saveBtn": {
"message": "저장"
},
"supportedMemosVersion": {
"message": "Memos v0.15.0 - 0.27.x 호환"
},
"settingsConnectionTitle": {
"message": "연결 설정"
},
"settingsConnectionDesc": {
"message": "Memos 사이트 URL과 액세스 토큰을 설정합니다."
},
"settingsPostingTitle": {
"message": "전송 설정"
},
"settingsPostingDesc": {
"message": "첨부만 전송할 때의 기본 텍스트"
},
"placeApiUrl": {
"message": "Memos 사이트 URL"
},
"placeApiTokens": {
"message": "Memos 액세스 토큰"
},
"placeContent": {
"message": "지금 떠오른 생각은..."
},
"lockPrivate": {
"message": "비공개"
},
"lockProtected": {
"message": "보호됨"
},
"lockPublic": {
"message": "공개"
},
"submitBtn": {
"message": "전송"
},
"placeHideInput": {
"message": "기본 '비공개' 태그 이름"
},
"placeShowInput": {
"message": "기본 '모두 공개' 태그 이름"
},
"placeAttachmentOnlyDefaultText": {
"message": "첨부만 전송할 때의 기본 텍스트(비워두면 내장 문구 사용)"
},
"uploadedListTitle": {
"message": "업로드된 파일(드래그로 순서 변경)"
},
"uploadedListEmpty": {
"message": "업로드된 파일이 없습니다"
},
"tipReorder": {
"message": "드래그하여 순서 변경"
},
"tipDeleteAttachment": {
"message": "삭제"
},
"attachmentDeleteSuccess": {
"message": "삭제됨"
},
"attachmentDeleteFailed": {
"message": "삭제 실패 😭"
},
"picDrag": {
"message": "이미지를 드래그하여 업로드"
},
"picCancelDrag": {
"message": "업로드 취소"
},
"picUploading": {
"message": "이미지 업로드 중..."
},
"picSuccess": {
"message": "업로드 완료"
},
"picFailed": {
"message": "업로드 실패"
},
"picPending": {
"message": "이미지 업로드가 진행 중입니다"
},
"saveSuccess": {
"message": "저장 성공!"
},
"searchNow": {
"message": "무엇을 찾고 있나요?"
},
"searchNone": {
"message": "다른 단어를 시도해 보세요!"
},
"archiveSuccess": {
"message": "보관 성공 😊"
},
"archiveFailed": {
"message": "보관 실패 😭"
},
"getTabFailed": {
"message": "탭 가져오기 실패 😭"
},
"memoUploading": {
"message": "전송 중"
},
"memoSuccess": {
"message": "성공! 😊"
},
"memoFailed": {
"message": "실패! 😭"
},
"invalidToken": {
"message": "유효하지 않은 토큰 또는 URL 😭"
},
"tipOpenSite": {
"message": "Memos 열기"
},
"tipSettings": {
"message": "설정"
},
"tipTags": {
"message": "태그 삽입"
},
"tipTodo": {
"message": "할 일 삽입"
},
"tipUpload": {
"message": "파일 업로드"
},
"tipLink": {
"message": "현재 탭 링크 삽입"
},
"tipRandom": {
"message": "랜덤 메모"
},
"tipSearch": {
"message": "검색"
},
"tipVisibility": {
"message": "공개 범위"
},
"tipSend": {
"message": "전송(Ctrl/⌘+Enter)"
},
"tipLanguage": {
"message": "언어"
},
"langAuto": {
"message": "자동"
},
"langEnglish": {
"message": "English"
},
"langChineseSimplified": {
"message": "简体中文"
},
"langJapanese": {
"message": "日本語"
},
"langKorean": {
"message": "한국어"
},
"tipFullscreen": {
"message": "전체화면 편집"
},
"tipResize": {
"message": "드래그로 확대/축소(최소: 기본 크기)"
}
}
+188
View File
@@ -0,0 +1,188 @@
{
"extName": {
"message": "Memos"
},
"actionTitle": {
"message": "发送 Memos"
},
"extDescription": {
"message": "一键发送灵感时刻,珍藏你的记忆"
},
"sendTo": {
"message": "发送至 Memos “%s”"
},
"sendLinkTo": {
"message": "发送链接至 Memos"
},
"sendImageTo": {
"message": "发送图片至 Memos"
},
"saveBtn":{
"message": "保存"
},
"supportedMemosVersion": {
"message": "兼容 Memos v0.15.0 - 0.27.x"
},
"settingsConnectionTitle": {
"message": "连接设置"
},
"settingsConnectionDesc": {
"message": "配置 Memos 服务地址和访问令牌。"
},
"settingsPostingTitle": {
"message": "发送设置"
},
"settingsPostingDesc": {
"message": "仅发送附件时的默认文本"
},
"placeApiUrl":{
"message": "请填入 Memos 主页网址"
},
"placeApiTokens":{
"message": "请填入 Memos Access Tokens"
},
"placeContent":{
"message": "现在的想法是..."
},
"lockPrivate":{
"message": "私有"
},
"lockProtected":{
"message": "登录可见"
},
"lockPublic":{
"message": "公开"
},
"submitBtn":{
"message": "记下"
},
"placeHideInput":{
"message": "默认“私有”标签名"
},
"placeShowInput":{
"message": "默认“公开”标签名"
},
"placeAttachmentOnlyDefaultText":{
"message": "仅发送附件时的默认文本(留空则使用内置文案)"
},
"picDrag":{
"message": "拖拽到窗口上传该图片"
},
"picCancelDrag":{
"message": "取消上传"
},
"picUploading":{
"message": "图片上传中……"
},
"picSuccess":{
"message": "上传完成"
},
"picFailed":{
"message": "上传图片失败"
},
"picPending":{
"message": "有图片等待上传"
},
"saveSuccess":{
"message": "保存信息成功"
},
"searchNow":{
"message": "想搜点啥?"
},
"searchNone":{
"message": "搜不到,换个词试试"
},
"archiveSuccess":{
"message": "归档成功!😊"
},
"archiveFailed":{
"message": "归档失败 😭"
},
"getTabFailed":{
"message": "获取标签失败 😭"
},
"memoUploading":{
"message": "发送中……"
},
"memoSuccess":{
"message": "发送成功!😊"
},
"memoFailed":{
"message": "发送失败 😭"
},
"invalidToken":{
"message": "无效的 token 或 url 😭"
},
"uploadedListTitle": {
"message": "已上传文件,可拖动排序"
},
"uploadedListEmpty": {
"message": "暂无已上传文件"
},
"tipReorder": {
"message": "拖动排序"
},
"tipDeleteAttachment": {
"message": "删除"
},
"attachmentDeleteSuccess": {
"message": "删除成功"
},
"attachmentDeleteFailed": {
"message": "删除失败 😭"
},
"tipOpenSite": {
"message": "打开 Memos"
},
"tipSettings": {
"message": "设置"
},
"tipTags": {
"message": "插入标签"
},
"tipTodo": {
"message": "插入待办"
},
"tipUpload": {
"message": "上传文件"
},
"tipLink": {
"message": "插入当前页面链接"
},
"tipRandom": {
"message": "随机一条"
},
"tipSearch": {
"message": "搜索"
},
"tipVisibility": {
"message": "可见性"
},
"tipSend": {
"message": "发送(Ctrl/⌘+Enter"
},
"tipLanguage": {
"message": "语言"
},
"langAuto": {
"message": "跟随浏览器"
},
"langEnglish": {
"message": "English"
},
"langChineseSimplified": {
"message": "简体中文"
},
"langJapanese": {
"message": "日本語"
},
"langKorean": {
"message": "한국어"
},
"tipFullscreen": {
"message": "全屏编辑"
},
"tipResize": {
"message": "拖拽缩放编辑框(最小为默认大小)"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

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