From adfd797e8471851976e25787268ff86a9202163c Mon Sep 17 00:00:00 2001 From: jonny Date: Sat, 7 Mar 2026 14:42:31 +0800 Subject: [PATCH] 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. --- _locales/en/messages.json | 9 ++ _locales/ja/messages.json | 9 ++ _locales/ko/messages.json | 9 ++ _locales/zh_CN/messages.json | 9 ++ css/main.css | 69 ++++++++++++++- js/background.js | 145 +++++++++++++++++++++++++------ js/i18n.js | 3 + js/oper.js | 160 ++++++++++++++++++++++++++++++++++- manifest.json | 2 + popup.html | 2 + 10 files changed, 385 insertions(+), 32 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index d71667a..f1a1c96 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -163,5 +163,14 @@ }, "langKorean": { "message": "한국어" + }, + "fullscreenBtn": { + "message": "Fullscreen" + }, + "tipFullscreen": { + "message": "Open fullscreen editor" + }, + "tipResize": { + "message": "Drag to resize (min: default size)" } } \ No newline at end of file diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json index a084e35..20aafec 100644 --- a/_locales/ja/messages.json +++ b/_locales/ja/messages.json @@ -163,5 +163,14 @@ }, "langKorean": { "message": "한국어" + }, + "fullscreenBtn": { + "message": "全画面" + }, + "tipFullscreen": { + "message": "全画面で編集" + }, + "tipResize": { + "message": "ドラッグで拡大/縮小(最小:初期サイズ)" } } diff --git a/_locales/ko/messages.json b/_locales/ko/messages.json index 2e74e88..da14da3 100644 --- a/_locales/ko/messages.json +++ b/_locales/ko/messages.json @@ -163,5 +163,14 @@ }, "langKorean": { "message": "한국어" + }, + "fullscreenBtn": { + "message": "전체화면" + }, + "tipFullscreen": { + "message": "전체화면 편집" + }, + "tipResize": { + "message": "드래그로 확대/축소(최소: 기본 크기)" } } diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index c67c0d1..6f04e1d 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -163,5 +163,14 @@ }, "langKorean": { "message": "한국어" + }, + "fullscreenBtn": { + "message": "全屏" + }, + "tipFullscreen": { + "message": "全屏编辑" + }, + "tipResize": { + "message": "拖拽缩放编辑框(最小为默认大小)" } } \ No newline at end of file diff --git a/css/main.css b/css/main.css index 597efab..4e22fb6 100644 --- a/css/main.css +++ b/css/main.css @@ -48,6 +48,37 @@ a{color: #555;} transition-timing-function: cubic-bezier(.4,0,.2,1); transition-duration: .15s; } +.memo-editor{ + position: relative; + resize: none; + overflow: hidden; + box-sizing: border-box; + contain: layout paint; +} + +#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; +} .random-item{ border: 1px solid rgb(229,231,235); color: #666; @@ -66,6 +97,25 @@ a{color: #555;} overflow-wrap: anywhere; word-break: normal;} .btns-container{text-align:right;} +.memo-editor #fullscreen{ + position: absolute; + right: .5rem; + top: .5rem; + 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.5rem; + padding: 0 .4rem; + cursor: pointer; + opacity: .9; +} +.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 +127,25 @@ 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-top: 1.8rem; + padding-right: 3.2rem; +} + +.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;} @@ -248,9 +313,7 @@ input.inputer{border-bottom: 1px solid #ccc;width:75%;} padding: .15rem .35rem; cursor: pointer; } -#blog_info{ -} .tip{ margin-left: 36%; diff --git a/js/background.js b/js/background.js index 76e051e..6b1628c 100644 --- a/js/background.js +++ b/js/background.js @@ -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') + } }) \ No newline at end of file diff --git a/js/i18n.js b/js/i18n.js index 22c0c46..870738f 100644 --- a/js/i18n.js +++ b/js/i18n.js @@ -86,6 +86,7 @@ function applyStaticI18n() { setText('lockPublic', 'lockPublic') setText('content_submit_text', 'submitBtn') + setText('fullscreen', 'fullscreenBtn') setPlaceholder('hideInput', 'placeHideInput') setPlaceholder('showInput', 'placeShowInput') @@ -111,6 +112,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 } = {}) { diff --git a/js/oper.js b/js/oper.js index bde96d8..0e3b270 100644 --- a/js/oper.js +++ b/js/oper.js @@ -1,6 +1,136 @@ 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` + + 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 readCurrentScale = () => { + const w = parseFloat(editor.style.width || '') + const h = parseFloat(editor.style.height || '') + const sw = baseW > 0 && Number.isFinite(w) ? w / baseW : 1 + const sh = baseH > 0 && Number.isFinite(h) ? h / baseH : 1 + return Math.max(1, sw, sh) + } + + const applyScale = (scale) => { + const s = Math.max(1, Math.min(maxScale, scale)) + editor.style.width = `${Math.round(baseW * s)}px` + editor.style.height = `${Math.round(baseH * s)}px` + } + + const scheduleApply = () => { + if (rafId) return + rafId = window.requestAnimationFrame(() => { + rafId = 0 + if (pendingScale == null) return + const s = pendingScale + pendingScale = null + applyScale(s) + }) + } + + handle.addEventListener('pointerdown', (ev) => { + dragging = true + startX = ev.clientX + startY = ev.clientY + startScale = readCurrentScale() + computeMaxScale() + try { handle.setPointerCapture(ev.pointerId) } catch (_) {} + ev.preventDefault() + }) + + handle.addEventListener('pointermove', (ev) => { + if (!dragging) return + const dx = ev.clientX - startX + const dy = ev.clientY - startY + + // Proportional scale based on diagonal length for smoother, more linear feel. + const diag0 = Math.hypot(baseW, baseH) + const targetW = baseW * startScale + dx + const targetH = baseH * startScale + dy + const diag1 = Math.hypot(targetW, targetH) + const next = diag0 > 0 ? diag1 / diag0 : startScale + + pendingScale = next + scheduleApply() + }) + + const endDrag = () => { + dragging = false + } + + 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 +187,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 +277,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 +298,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 +313,11 @@ $("textarea[name=text]").on('keydown', function (ev) { } }) +$('#fullscreen').on('click', function () { + if (isFullscreenMode()) return + openFullscreenTab() +}) + //监听拖拽事件,实现拖拽到窗口上传图片 initDrag() @@ -239,6 +381,20 @@ function escapeHtml(input) { .replace(/'/g, ''') } +function focusTextareaToEnd($textarea) { + try { + const el = $textarea && $textarea[0] + if (!el) return + el.focus() + const len = typeof el.value === 'string' ? el.value.length : 0 + if (typeof el.setSelectionRange === 'function') { + el.setSelectionRange(len, len) + } + } catch (_) { + // best-effort only + } +} + function buildV1ResourceStreamUrl(info, resource) { if (!info || !info.apiUrl || !resource) return '' // Use the configured apiUrl as the base (may include a reverse-proxy subpath). diff --git a/manifest.json b/manifest.json index ac34184..0ce38a3 100644 --- a/manifest.json +++ b/manifest.json @@ -21,6 +21,8 @@ }, "permissions": [ "tabs", + "scripting", + "windows", "storage", "activeTab", "contextMenus" diff --git a/popup.html b/popup.html index b508d23..f2df32e 100644 --- a/popup.html +++ b/popup.html @@ -51,6 +51,7 @@
+ +