17 Commits

Author SHA1 Message Date
jonny 944be49edb Support Memos v0.27 and improve API parsing
Bump supported Memos version to v0.27 across locales, README changelog, and manifest (version and version_name). Improve memos API compatibility: prefer and return trimmed username when present, handle non-numeric trailing name segments safely, and encode userid in the user-scoped memos URL to avoid issues with special characters.
2026-04-21 15:26:22 +08:00
jonny 25fbd486bf Bump extension version to 2026.04.11
Update manifest.json version from "2026.03.25" to "2026.04.11" to reflect the new release. No other changes to manifest fields or supported version range.
2026-04-11 10:44:40 +08:00
jonny 15d99cb51f Make editor container and input flex-aware
Enable flex layout for the editor wrapper and make .common-editor-inputer a flexible child. Added display:flex and flex-direction:column to the container rule, replaced height:auto with flex: 1 1 auto and added min-height:0 on .common-editor-inputer to allow proper shrinking/growth and avoid overflow issues in flex layouts. (css/main.css)
2026-04-11 10:43:47 +08:00
jonny ed5fc86e39 Replace language select with toggle menu
Replace the old <select> language picker with a custom toggle button and dropdown menu. Updates include new popup HTML (langToggle button, langMenu with menuitemradio buttons), new CSS for .lang-toggle/.lang-menu/.lang-menu-item (styling, hover/active states, shadow, rounded corners), and JS to manage label sync, menu state, click handlers, outside-click and Escape to close, and applying the selected UI language. Also update references in i18n code (setTitle target and language init flow) and persist language selection. Bump manifest version from 2026.03.24 to 2026.03.25.
2026-03-23 11:29:49 +08:00
jonny 0f468d49aa Update Chinese strings and bump version
Optimize Chinese display text and bump extension version. Add a README changelog entry for "优化中文显示效果"; simplify several zh_CN locale strings (lock labels shortened to "私有", "登录可见", "公开" and default tag placeholders updated). Also increment manifest version to 2026.03.24.
2026-03-23 10:52:59 +08:00
jonny f3e55ec53e Update version number to 2026.03.23 2026-03-22 23:27:14 +08:00
jonny f05581f88a Adjust body min-width
Reduced minimum width for the body class and removed media query styles for mobile responsiveness.
2026-03-22 23:25:50 +08:00
jonny d237a7f1c6 Bump manifest version and add mobile portrait note
Update manifest.json version to 2026.03.22 and add a README changelog entry (20260322) noting adaptation for mobile portrait windows.
2026-03-22 20:48:23 +08:00
jonny 13cc7659ea Add responsive styles for small portrait screens
Introduce a media query for max-width:480px, portrait, touch-only devices to improve layout on small screens. Adjustments include removing body min-width, enabling wrapping and spacing for .common-tools-wrapper, forcing .common-tools-container and .btns-container to full width with flexible sizing, adding bottom margin for .mr-5 elements, and increasing minimum dimensions for #content_submit_text to improve tap targets and prevent overflow.
2026-03-22 20:45:15 +08:00
jonny e9730b5839 Lower minimum supported Memos to v0.15.0
Update compatibility range to include Memos v0.15.0 across README and locale files (en, ja, ko, zh_CN). Bump manifest version/date to 2026.03.12 and update version_name to reflect the new supported range. This extends backward compatibility to older Memos releases.
2026-03-12 18:35:06 +08:00
jonny f2f1ff2c10 Bump manifest version and update changelog
Update manifest.json to version 2026.03.11 and set version_name to "Supports 0.18.0 - 0.26.x". Also add a README changelog entry for 2026-03-10 noting remembered drag window size and removal of drag window animation. This prepares a small release update and documents the recent UI tweak.
2026-03-09 20:07:57 +08:00
jonny 5fac00b5ce - feat:记忆拖拽窗口大小和优化动画效果
Add persistent, robust scaling for the proportional editor and remove CSS transitions that interfered with instant restores. Introduces a storageKey, parseScale, applyScaleInstant and logic to restore scale synchronously from localStorage (for immediate UX) and asynchronously from chrome.storage.sync (best-effort), keeping localStorage in sync. Ensures pending RAF updates are flushed before persisting and guards against missing APIs or invalid values. Also removes transition properties in main.css so scale restores remain immediate and predictable.
2026-03-09 20:00:42 +08:00
jonny 2b36fda137 Bump manifest version to 2026.03.10
Update manifest.json version field from 2026.03.09 to 2026.03.10. No other changes to manifest content; this is a minor release/version bump.
2026-03-07 21:22:52 +08:00
jonny da150b8788 Refactor fullscreen button markup, styles, i18n
Replace the old fullscreen localization key and button implementation with an iconized, accessible button and updated styling. Removed fullscreenBtn entries from locale files and updated js/i18n.js to set the fullscreen button's aria-label using tipFullscreen. popup.html now wraps the button in a header and includes an SVG icon; main.css adjusts layout (overflow, sticky header, button positioning, sizing, and hiding in fullscreen) and tweaks editor input spacing for the new layout.
2026-03-07 21:18:23 +08:00
jonny 85cc964836 Bump manifest version and update changelog
Add a 20260309 changelog entry noting: right-click send selected text preserves original formatting and added fullscreen and window-zoom features. Also bump manifest version from 2026.03.08 to 2026.03.09 to reflect the update.
2026-03-07 14:45:17 +08:00
jonny adfd797e84 Add fullscreen editor and proportional resize
Introduce a fullscreen editor mode and a proportional resize handle for the memo editor. Updates include:

- UI: add fullscreen button and resize handle to popup.html and related i18n keys for en/ja/ko/zh_CN.
- CSS: styles for .memo-editor, fullscreen state, and #editor-resize-handle, plus layout tweaks for fullscreen.
- Background: enhance context menu handler to retrieve selection text from the active tab using chrome.scripting.executeScript, support opening the extension popup programmatically (tryOpenActionPopup), and factor appendContent logic.
- Oper: implement isFullscreenMode(), openFullscreenTab(), proportional editor resize logic with pointer events (initProportionalEditorResize), focus handling adjustments, and init call. Added helper focusTextareaToEnd().
- Manifest: request scripting and windows permissions required for selection injection and window focus.

These changes enable sending accurate selection text from web pages, allow users to open a fullscreen editor tab, and provide a draggable, proportional resize experience in the popup editor.
2026-03-07 14:42:31 +08:00
jonny 8f51bb399b Move memosApi to compat/memosApi.v024.js
Rename js/memosApi.js to js/compat/memosApi.v024.js (contents unchanged) and update popup.html to load the new path. Organizes the legacy memosApi into the compat folder and ensures the popup references the v0.24 compatibility script alongside other compat shims.
2026-03-07 11:41:15 +08:00
12 changed files with 655 additions and 78 deletions
+7 -2
View File
@@ -5,8 +5,13 @@ Chrome 应用商店:<https://chrome.google.com/webstore/detail/memos-bber/cbhj
一个通过浏览器插件发布 [Memos](https://usememos.com/) 的插件。基于 iSpeak-bber 修改,原作者为 [DreamyTZK](https://www.antmoe.com/)。
## 更新日志
### 20260308 向前兼容到0.18.0,可能再往前也行,只测试到0.18.0
- 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反馈
+7 -1
View File
@@ -21,7 +21,7 @@
"message": "Save"
},
"supportedMemosVersion": {
"message": "Compatible with Memos v0.18.0 - 0.26.x"
"message": "Compatible with Memos v0.15.0 - 0.27.x"
},
"placeApiUrl":{
"message": "Memos site URL"
@@ -163,5 +163,11 @@
},
"langKorean": {
"message": "한국어"
},
"tipFullscreen": {
"message": "Open fullscreen editor"
},
"tipResize": {
"message": "Drag to resize (min: default size)"
}
}
+7 -1
View File
@@ -21,7 +21,7 @@
"message": "保存"
},
"supportedMemosVersion": {
"message": "Memos v0.18.0 - 0.26.x に対応"
"message": "Memos v0.15.0 - 0.27.x に対応"
},
"placeApiUrl": {
"message": "Memos サイトURL"
@@ -163,5 +163,11 @@
},
"langKorean": {
"message": "한국어"
},
"tipFullscreen": {
"message": "全画面で編集"
},
"tipResize": {
"message": "ドラッグで拡大/縮小(最小:初期サイズ)"
}
}
+7 -1
View File
@@ -21,7 +21,7 @@
"message": "저장"
},
"supportedMemosVersion": {
"message": "Memos v0.18.0 - 0.26.x 호환"
"message": "Memos v0.15.0 - 0.27.x 호환"
},
"placeApiUrl": {
"message": "Memos 사이트 URL"
@@ -163,5 +163,11 @@
},
"langKorean": {
"message": "한국어"
},
"tipFullscreen": {
"message": "전체화면 편집"
},
"tipResize": {
"message": "드래그로 확대/축소(최소: 기본 크기)"
}
}
+12 -6
View File
@@ -21,7 +21,7 @@
"message": "保存"
},
"supportedMemosVersion": {
"message": "兼容 Memos v0.18.0 - 0.26.x"
"message": "兼容 Memos v0.15.0 - 0.27.x"
},
"placeApiUrl":{
"message": "请填入 Memos 主页网址"
@@ -33,22 +33,22 @@
"message": "现在的想法是..."
},
"lockPrivate":{
"message": "仅自己可见"
"message": "私有"
},
"lockProtected":{
"message": "登录用户可见"
"message": "登录可见"
},
"lockPublic":{
"message": "所有人可见"
"message": "公开"
},
"submitBtn":{
"message": "记下"
},
"placeHideInput":{
"message": "默认“仅自己可见”标签名"
"message": "默认“私有”标签名"
},
"placeShowInput":{
"message": "默认“所有人可见”标签名"
"message": "默认“公开”标签名"
},
"picDrag":{
"message": "拖拽到窗口上传该图片"
@@ -163,5 +163,11 @@
},
"langKorean": {
"message": "한국어"
},
"tipFullscreen": {
"message": "全屏编辑"
},
"tipResize": {
"message": "拖拽缩放编辑框(最小为默认大小)"
}
}
+158 -11
View File
@@ -20,7 +20,7 @@ input:focus::placeholder ,.common-editor-inputer:focus::placeholder {
}
.body{
min-width:460px;
min-width:360px;
background-color: #f6f5f4;
padding:0 1rem 1rem;
font-family: eafont,PingFang SC,Hiragino Sans GB,Microsoft YaHei,STHeiti,WenQuanYi Micro Hei,Helvetica,Arial,sans-serif;
@@ -29,6 +29,7 @@ input:focus::placeholder ,.common-editor-inputer:focus::placeholder {
line-height: 1.5;
position: relative;
}
a{color: #555;}
.title{
width: 100px;
@@ -44,9 +45,51 @@ a{color: #555;}
background-color: rgb(255,255,255);
margin-top:0.8rem;
padding: 0.6rem;
transition-property: all;
transition-timing-function: cubic-bezier(.4,0,.2,1);
transition-duration: .15s;
}
.memo-editor{
position: relative;
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);
@@ -66,6 +109,32 @@ a{color: #555;}
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%;
@@ -77,10 +146,28 @@ a{color: #555;}
background-color: transparent;
font-size: 1rem;
min-height: 40px;
max-height: 400px;
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%;}
#saveKey{margin:0;flex:1;}
@@ -238,20 +325,80 @@ input.inputer{border-bottom: 1px solid #ccc;width:75%;}
top: .55rem;
}
.lang-select{
border: 1px solid rgb(229,231,235);
border-radius: .25rem;
background-color: rgb(255,255,255);
.lang-toggle{
border: none;
border-radius: 0;
background-color: transparent;
color: #666;
min-width: 24px;
height: 24px;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-sizing: border-box;
padding: 0;
opacity: .6;
}
.lang-toggle:hover,
.lang-toggle[aria-expanded="true"]{
background-color: transparent;
color: #666;
opacity: 1;
}
.lang-toggle-text{
display: inline-block;
min-width: 24px;
text-align: center;
font-size: 12px;
line-height: 24px;
font-weight: 700;
letter-spacing: .02em;
}
.lang-menu{
position: absolute;
top: calc(100% + .35rem);
right: 0;
min-width: 8rem;
padding: .25rem;
border: 1px solid rgb(229,231,235);
border-radius: .5rem;
background-color: rgb(255,255,255);
box-shadow: 0 8px 24px rgba(15,23,42,.12);
z-index: 10;
}
.lang-menu.hidden{
display: none;
}
.lang-menu-item{
width: 100%;
display: block;
text-align: left;
padding: .4rem .5rem;
border-radius: .35rem;
background: transparent;
color: #555;
font-size: .75rem;
line-height: 1.25rem;
padding: .15rem .35rem;
cursor: pointer;
}
#blog_info{
.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;
+116 -25
View File
@@ -23,6 +23,89 @@ function updateContextMenu(id, update) {
})
}
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
@@ -93,35 +176,43 @@ chrome.storage.onChanged.addListener((changes, areaName) => {
refreshContextMenus()
})
chrome.contextMenus.onClicked.addListener((info) => {
let tempCont = ''
switch (info.menuItemId) {
case 'Memos-send-selection':
tempCont =
info.selectionText +
'\n' +
`[Reference Link](${info.linkUrl || info.pageUrl})` +
'\n'
break
case 'Memos-send-link':
tempCont = (info.linkUrl || info.pageUrl) + '\n'
break
case 'Memos-send-image':
tempCont = `![](${info.srcUrl})` + '\n'
break
}
chrome.storage.sync.get(
{ open_action: 'save_text', open_content: '' },
function (items) {
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))
} else {
chrome.storage.sync.set({
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')
}
})
@@ -11,12 +11,19 @@
return Number(user.id)
}
if (typeof user.username === 'string' && user.username.trim() !== '') {
return user.username.trim()
}
const name = user.name || (user.user && user.user.name)
if (typeof name === 'string') {
const m = name.match(/\busers\/(\d+)\b/)
if (m) return Number(m[1])
const last = name.split('/').pop()
if (last && !Number.isNaN(Number(last))) return Number(last)
if (last) {
if (!Number.isNaN(Number(last))) return Number(last)
if (last.trim() !== '') return last.trim()
}
}
return null
@@ -88,7 +95,7 @@
// the full set (including private), which affects tag extraction.
// Newer versions may not expose the user-scoped endpoint, so we fallback by 404/405.
const urlUserScoped = info.userid
? info.apiUrl + 'api/v1/users/' + info.userid + '/memos' + qs
? info.apiUrl + 'api/v1/users/' + encodeURIComponent(String(info.userid)) + '/memos' + qs
: null
const urlGlobal = info.apiUrl + 'api/v1/memos' + qs
+68 -11
View File
@@ -47,6 +47,36 @@ function formatSubstitutions(message, substitutions) {
let currentUiLanguage = 'auto'
let overrideMessages = null
function getLanguageToggleLabel(lang) {
if (lang === 'en') return 'EN'
if (lang === 'zh_CN') return '中'
if (lang === 'ja') return '日'
if (lang === 'ko') return '한'
return 'A'
}
function syncLanguageToggleText(lang) {
const text = document.getElementById('langToggleText')
if (text) text.textContent = getLanguageToggleLabel(lang)
}
function syncLanguageMenuState(lang) {
const items = document.querySelectorAll('.lang-menu-item')
items.forEach((item) => {
const isActive = item.getAttribute('data-lang') === lang
item.classList.toggle('active', isActive)
item.setAttribute('aria-checked', isActive ? 'true' : 'false')
})
}
function setLanguageMenuOpen(isOpen) {
const toggle = document.getElementById('langToggle')
const menu = document.getElementById('langMenu')
if (!toggle || !menu) return
toggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false')
menu.classList.toggle('hidden', !isOpen)
}
function t(key, substitutions) {
const msg = overrideMessages && overrideMessages[key] && overrideMessages[key].message
if (typeof msg === 'string' && msg.length > 0) {
@@ -86,6 +116,8 @@ function applyStaticI18n() {
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')
@@ -98,7 +130,9 @@ function applyStaticI18n() {
setText('langOptionZhCN', 'langChineseSimplified')
setText('langOptionJa', 'langJapanese')
setText('langOptionKo', 'langKorean')
setTitle('langSelect', 'tipLanguage')
setTitle('langToggle', 'tipLanguage')
const langToggle = document.getElementById('langToggle')
if (langToggle) langToggle.setAttribute('aria-label', t('tipLanguage'))
// Native hover tooltips (title)
setTitle('opensite', 'tipOpenSite')
@@ -111,6 +145,8 @@ function applyStaticI18n() {
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 } = {}) {
@@ -118,26 +154,47 @@ async function setUiLanguage(nextLang, { persist = true } = {}) {
currentUiLanguage = lang
overrideMessages = await loadLocaleMessages(lang)
applyStaticI18n()
const select = document.getElementById('langSelect')
if (select && select.value !== lang) select.value = lang
syncLanguageToggleText(lang)
syncLanguageMenuState(lang)
if (persist) await storageSyncSet({ [UI_LANGUAGE_STORAGE_KEY]: lang })
window.dispatchEvent(new CustomEvent('i18n:changed', { detail: { lang } }))
}
async function initLanguageSwitcher() {
const select = document.getElementById('langSelect')
if (select) {
select.addEventListener('change', async () => {
await setUiLanguage(select.value)
const switcher = document.getElementById('lang_switcher')
const toggle = document.getElementById('langToggle')
const langItems = document.querySelectorAll('.lang-menu-item')
if (toggle) {
toggle.addEventListener('click', (event) => {
event.stopPropagation()
const isOpen = toggle.getAttribute('aria-expanded') === 'true'
setLanguageMenuOpen(!isOpen)
})
}
const items = await storageSyncGet({ [UI_LANGUAGE_STORAGE_KEY]: 'auto' })
const stored = normalizeUiLanguage(items[UI_LANGUAGE_STORAGE_KEY])
if (select) select.value = stored
langItems.forEach((item) => {
item.addEventListener('click', async (event) => {
event.stopPropagation()
setLanguageMenuOpen(false)
await setUiLanguage(item.getAttribute('data-lang'))
})
})
document.addEventListener('click', (event) => {
if (!switcher || switcher.contains(event.target)) return
setLanguageMenuOpen(false)
})
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') setLanguageMenuOpen(false)
})
const storedItems = await storageSyncGet({ [UI_LANGUAGE_STORAGE_KEY]: 'auto' })
const stored = normalizeUiLanguage(storedItems[UI_LANGUAGE_STORAGE_KEY])
await setUiLanguage(stored, { persist: false })
setLanguageMenuOpen(false)
}
window.t = t
+229 -2
View File
@@ -1,6 +1,207 @@
dayjs.extend(window.dayjs_plugin_relativeTime)
let currentMemoLock = ''
function isFullscreenMode() {
try {
const params = new URLSearchParams(window.location.search || '')
return params.get('mode') === 'full'
} catch (_) {
return false
}
}
function openFullscreenTab() {
try {
const url = chrome.runtime.getURL('popup.html?mode=full')
chrome.tabs.create({ url })
} catch (_) {
// best-effort only
}
}
function initProportionalEditorResize() {
try {
if (isFullscreenMode()) return
const editor = document.querySelector('.memo-editor')
const tools = document.querySelector('.common-tools-wrapper')
const handle = document.getElementById('editor-resize-handle')
if (!editor || !tools || !handle) return
const safety = 8
const initialRect = editor.getBoundingClientRect()
const baseW = Math.ceil(initialRect.width)
const baseH = Math.ceil(initialRect.height)
// Lock the base size. Scaling will be applied by setting width/height.
editor.style.width = `${baseW}px`
editor.style.height = `${baseH}px`
editor.style.minWidth = `${baseW}px`
editor.style.minHeight = `${baseH}px`
const storageKey = 'popupEditorScale'
let maxScale = 1
const computeMaxScale = () => {
// In popup mode, allow scaling up to Chrome's max popup size.
// Do not clamp by current window.innerWidth/innerHeight, otherwise the popup can't grow to the max.
const viewportW = 800
const viewportH = 600
const editorRect = editor.getBoundingClientRect()
const toolsRect = tools.getBoundingClientRect()
const toolsStyle = window.getComputedStyle(tools)
const gap = parseFloat(toolsStyle.marginTop || '0') || 0
const availW = Math.max(0, viewportW - safety - editorRect.left)
const availH = Math.max(0, viewportH - safety - toolsRect.height - editorRect.top - gap)
const scaleW = baseW > 0 ? availW / baseW : 1
const scaleH = baseH > 0 ? availH / baseH : 1
maxScale = Math.max(1, Math.min(scaleW, scaleH))
}
computeMaxScale()
window.addEventListener('resize', computeMaxScale)
let dragging = false
let startX = 0
let startY = 0
let startScale = 1
let rafId = 0
let pendingScale = null
const parseScale = (raw) => {
const s = typeof raw === 'number' && Number.isFinite(raw)
? raw
: typeof raw === 'string' && raw.trim() !== '' && !Number.isNaN(Number(raw))
? Number(raw)
: 1
return s > 0 ? s : 1
}
const readCurrentScale = () => {
const w = parseFloat(editor.style.width || '')
const h = parseFloat(editor.style.height || '')
const sw = baseW > 0 && Number.isFinite(w) ? w / baseW : 1
const sh = baseH > 0 && Number.isFinite(h) ? h / baseH : 1
return Math.max(1, sw, sh)
}
const applyScale = (scale) => {
const s = Math.max(1, Math.min(maxScale, scale))
editor.style.width = `${Math.round(baseW * s)}px`
editor.style.height = `${Math.round(baseH * s)}px`
}
const applyScaleInstant = (scale) => {
// In case CSS transitions exist (or get reintroduced), keep restores immediate.
const prevTransition = editor.style.transition
editor.style.transition = 'none'
applyScale(scale)
window.requestAnimationFrame(function () {
editor.style.transition = prevTransition
})
}
// Restore previously saved scale synchronously (localStorage) first.
// This makes the popup *feel* synchronous because it can apply before async chrome.storage returns.
let restoredFromLocal = false
let localScale = 1
try {
const raw = window.localStorage ? window.localStorage.getItem(storageKey) : null
const s = parseScale(raw)
if (s && s !== 1) {
localScale = s
restoredFromLocal = true
applyScaleInstant(s)
}
} catch (_) {
// ignore
}
// Restore from chrome.storage.sync (best-effort) and keep localStorage in sync.
try {
chrome.storage.sync.get({ [storageKey]: 1 }, function (items) {
const raw = items ? items[storageKey] : 1
const s = parseScale(raw)
const shouldApply = !restoredFromLocal || Math.abs(s - localScale) > 1e-6
if (shouldApply) applyScaleInstant(s)
try {
if (window.localStorage) window.localStorage.setItem(storageKey, String(s))
} catch (_) {}
})
} catch (_) {
// ignore
}
const scheduleApply = () => {
if (rafId) return
rafId = window.requestAnimationFrame(() => {
rafId = 0
if (pendingScale == null) return
const s = pendingScale
pendingScale = null
applyScale(s)
})
}
handle.addEventListener('pointerdown', (ev) => {
dragging = true
startX = ev.clientX
startY = ev.clientY
startScale = readCurrentScale()
computeMaxScale()
try { handle.setPointerCapture(ev.pointerId) } catch (_) {}
ev.preventDefault()
})
handle.addEventListener('pointermove', (ev) => {
if (!dragging) return
const dx = ev.clientX - startX
const dy = ev.clientY - startY
// Proportional scale based on diagonal length for smoother, more linear feel.
const diag0 = Math.hypot(baseW, baseH)
const targetW = baseW * startScale + dx
const targetH = baseH * startScale + dy
const diag1 = Math.hypot(targetW, targetH)
const next = diag0 > 0 ? diag1 / diag0 : startScale
pendingScale = next
scheduleApply()
})
const endDrag = () => {
dragging = false
// Flush any pending RAF update before persisting.
if (pendingScale != null) {
applyScale(pendingScale)
pendingScale = null
}
// Persist current scale (best-effort).
try {
const s = readCurrentScale()
if (typeof s === 'number' && Number.isFinite(s)) {
try {
if (window.localStorage) window.localStorage.setItem(storageKey, String(s))
} catch (_) {}
chrome.storage.sync.set({ [storageKey]: s })
}
} catch (_) {
// ignore
}
}
handle.addEventListener('pointerup', endDrag)
handle.addEventListener('pointercancel', endDrag)
} catch (_) {
// best-effort only
}
}
function msg(key) {
if (typeof window.t === 'function') return window.t(key)
return chrome.i18n.getMessage(key) || ''
@@ -57,6 +258,10 @@ function updateLockNowText(lockType) {
applyDayjsLocaleByUiLanguage(typeof window.getUiLanguage === 'function' ? window.getUiLanguage() : 'auto')
if (isFullscreenMode()) {
document.body.classList.add('fullscreen')
}
window.addEventListener('i18n:changed', (ev) => {
applyDayjsLocaleByUiLanguage(ev && ev.detail ? ev.detail.lang : 'auto')
updateLockNowText(currentMemoLock)
@@ -143,11 +348,14 @@ get_info(function (info) {
//打开的时候就是上传图片
uploadImage(info.open_content)
} else {
$("textarea[name=text]").val(info.open_content)
const $textarea = $("textarea[name=text]")
$textarea.val(info.open_content)
focusTextareaToEnd($textarea)
}
relistNow = Array.isArray(info.resourceIdList) ? info.resourceIdList : []
renderUploadList(relistNow)
initProportionalEditorResize()
//从localstorage 里面读取数据
setTimeout(get_info, 1)
})
@@ -161,7 +369,7 @@ chrome.storage.onChanged.addListener(function (changes, areaName) {
renderUploadList(relistNow)
})
$("textarea[name=text]").focus()
// focus is handled after textarea content is set
//监听输入结束,保存未发送内容到本地
$("textarea[name=text]").blur(function () {
@@ -176,6 +384,11 @@ $("textarea[name=text]").on('keydown', function (ev) {
}
})
$('#fullscreen').on('click', function () {
if (isFullscreenMode()) return
openFullscreenTab()
})
//监听拖拽事件,实现拖拽到窗口上传图片
initDrag()
@@ -239,6 +452,20 @@ function escapeHtml(input) {
.replace(/'/g, '&#39;')
}
function focusTextareaToEnd($textarea) {
try {
const el = $textarea && $textarea[0]
if (!el) return
el.focus()
const len = typeof el.value === 'string' ? el.value.length : 0
if (typeof el.setSelectionRange === 'function') {
el.setSelectionRange(len, len)
}
} catch (_) {
// best-effort only
}
}
function buildV1ResourceStreamUrl(info, resource) {
if (!info || !info.apiUrl || !resource) return ''
// Use the configured apiUrl as the base (may include a reverse-proxy subpath).
+4 -2
View File
@@ -2,8 +2,8 @@
"manifest_version": 3,
"name": "__MSG_extName__",
"default_locale": "en",
"version": "2026.03.08",
"version_name": "Supports 0.18.0 to the latest version",
"version": "2026.04.21",
"version_name": "Supports 0.15.0 - 0.27.x",
"action": {
"default_popup": "popup.html",
"default_icon": "assets/logo_24x24.png",
@@ -21,6 +21,8 @@
},
"permissions": [
"tabs",
"scripting",
"windows",
"storage",
"activeTab",
"contextMenus"
+25 -8
View File
@@ -13,13 +13,22 @@
<body class="body">
<div class="title" id="opensite">MEMOS</div>
<div id="lang_switcher" class="lang-switcher">
<select id="langSelect" class="lang-select" aria-label="Language" title="">
<option id="langOptionAuto" value="auto"></option>
<option id="langOptionEn" value="en"></option>
<option id="langOptionZhCN" value="zh_CN"></option>
<option id="langOptionJa" value="ja"></option>
<option id="langOptionKo" value="ko"></option>
</select>
<button
id="langToggle"
class="lang-toggle"
type="button"
aria-haspopup="true"
aria-expanded="false"
>
<span id="langToggleText" class="lang-toggle-text" aria-hidden="true">A</span>
</button>
<div id="langMenu" class="lang-menu hidden" role="menu" aria-labelledby="langToggle">
<button id="langOptionAuto" class="lang-menu-item" type="button" data-lang="auto" role="menuitemradio" aria-checked="false"></button>
<button id="langOptionEn" class="lang-menu-item" type="button" data-lang="en" role="menuitemradio" aria-checked="false"></button>
<button id="langOptionZhCN" class="lang-menu-item" type="button" data-lang="zh_CN" role="menuitemradio" aria-checked="false"></button>
<button id="langOptionJa" class="lang-menu-item" type="button" data-lang="ja" role="menuitemradio" aria-checked="false"></button>
<button id="langOptionKo" class="lang-menu-item" type="button" data-lang="ko" role="menuitemradio" aria-checked="false"></button>
</div>
</div>
<div id="blog_info_edit"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024">
<path d="M914 432c-5-26-21-43-41-43h-4c-54 0-99-44-99-99 0-17 9-37 9-38 10-22 2-50-18-65l-103-57h-1c-21-9-49-4-64 12-12 12-50 44-79 44s-68-33-79-45a60 60 0 0 0-64-13l-106 58-2 1a54 54 0 0 0-18 65c0 1 9 21 9 38 0 55-45 99-99 99h-5c-19 0-35 17-40 43 0 2-9 45-9 80s9 79 9 81c5 25 21 42 41 42h4c54 0 99 45 99 99 0 18-9 37-9 38-10 23-2 51 18 65l101 56 1 1c21 9 49 3 65-13 14-15 52-47 80-47 30 0 69 35 81 48a58 58 0 0 0 64 14l104-58 2-1c20-14 28-42 18-65 0-1-9-20-9-38 0-54 45-99 99-99h5c19 0 35-17 40-42 0-2 9-46 9-81s-9-78-9-80m-51 80c0 23-5 52-7 64a158 158 0 0 0-134 215l-89 49c-4-5-17-18-35-31-31-23-61-35-88-35s-57 12-88 34c-17 13-30 26-34 31l-86-48a159 159 0 0 0-134-215c-2-12-7-41-7-64 0-22 5-51 7-64a157 157 0 0 0 134-214l91-50c4 4 17 17 35 29 30 22 59 33 86 33s55-11 85-32c18-13 31-25 35-29l88 49a159 159 0 0 0 134 214c2 13 7 42 7 64"/>
@@ -51,6 +60,13 @@
</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"
@@ -59,6 +75,7 @@
placeholder=""
required=""
></textarea>
<div id="editor-resize-handle" aria-label="Resize"></div>
</div>
<div class="common-tools-wrapper">
@@ -151,7 +168,7 @@
<script src="../js/ko.js"></script>
<script src="../js/relativeTime.js"></script>
<script src="../js/view-image.js"></script>
<script src="../js/memosApi.js"></script>
<script src="../js/compat/memosApi.v024.js"></script>
<script src="../js/compat/memosApi.v1.js"></script>
<script src="../js/compat/memosApi.v023.js"></script>
<script src="../js/oper.js"></script>