diff --git a/bundled-addons/translate/addon.json b/bundled-addons/translate/addon.json index c5bad71f..5d48cbc2 100644 --- a/bundled-addons/translate/addon.json +++ b/bundled-addons/translate/addon.json @@ -1,12 +1,15 @@ { "id": "translate", "name": "Translate", - "version": "0.1.2", + "version": "0.1.3", "description": "Right-click a selection to translate it. Sidebar panel with LibreTranslate or Google as the backend.", "author": "Silent Mode", - "icon": "🌐", + "icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ij4gPGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMTEiIGZpbGw9IiMxZDRlZDgiLz4gPGcgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEuMSIgb3BhY2l0eT0iLjQ1Ij48ZWxsaXBzZSBjeD0iMTIiIGN5PSIxMiIgcng9IjQuNiIgcnk9IjExIi8+PC9nPiA8cmVjdCB4PSIwLjYiIHk9IjEuOCIgd2lkdGg9IjExLjYiIGhlaWdodD0iMTEuNiIgcng9IjIuNiIgZmlsbD0iI2ZmZiIvPiA8dGV4dCB4PSI2LjQiIHk9IjExLjEiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZvbnQtZmFtaWx5PSJzeXN0ZW0tdWksU2Vnb2UgVUksc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxMCIgZm9udC13ZWlnaHQ9IjgwMCIgZmlsbD0iIzFkNGVkOCI+QTwvdGV4dD4gPHJlY3QgeD0iMTEuOCIgeT0iMTAuNiIgd2lkdGg9IjExLjYiIGhlaWdodD0iMTEuNiIgcng9IjIuNiIgZmlsbD0iIzBiMTIyMCIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEiLz4gPGcgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjEuOSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBmaWxsPSJub25lIj4gPHBhdGggZD0iTTE3LjYgMTIuNXYxIi8+IDxwYXRoIGQ9Ik0xNC40IDE0LjdoNi40Ii8+IDxwYXRoIGQ9Ik0xOC4zIDE1LjYgMTQuNyAyMCIvPiA8cGF0aCBkPSJNMTYuOSAxNS42IDIwLjUgMjAiLz4gPC9nPiA8L3N2Zz4=", "main": "index.js", - "capabilities": ["sidebar-panel", "context-menu-item"], + "capabilities": [ + "sidebar-panel", + "context-menu-item" + ], "context-menu-items": [ { "id": "translate-selection", diff --git a/bundled-addons/translate/index.js b/bundled-addons/translate/index.js index 08267858..8bb98dce 100644 --- a/bundled-addons/translate/index.js +++ b/bundled-addons/translate/index.js @@ -100,10 +100,11 @@ async function doTranslate(payload) { module.exports = { activate(api) { + // No `icon` here on purpose — registerSidebarPanel defaults to + // manifest.icon, and passing one would shadow the data-URI mark. api.registerSidebarPanel({ id: "main", title: "Translate", - icon: "🌐", page: "panel.html", }); diff --git a/bundled-addons/translate/panel.html b/bundled-addons/translate/panel.html index 3a001aa2..6a3fd836 100644 --- a/bundled-addons/translate/panel.html +++ b/bundled-addons/translate/panel.html @@ -26,6 +26,23 @@ header .m { color: var(--dim); font-size: 11.5px; } header .m.err { color: #ff9081; } header .m.ok { color: var(--acid); } + header .t .em { display: inline-flex; } + header .t .em svg { display: block; width: 17px; height: 17px; } + .zoom { display: flex; align-items: center; gap: 2px; margin-left: 10px; } + .zoom button { + width: 20px; height: 20px; padding: 0; line-height: 1; + display: grid; place-items: center; + background: var(--btn); color: var(--mut); + border: 1px solid var(--line); border-radius: 5px; + cursor: pointer; font: inherit; font-size: 13px; + } + .zoom button:hover:not(:disabled) { background: var(--btn-h); color: var(--ink); } + .zoom button:disabled { opacity: .35; cursor: default; } + .zoom .pct { + min-width: 34px; text-align: center; color: var(--dim); + font-size: 10.5px; font-variant-numeric: tabular-nums; cursor: pointer; + } + .zoom .pct:hover { color: var(--ink); } .langs { display: flex; gap: 6px; align-items: center; padding: 8px 12px; border-bottom: 1px solid var(--line); background: var(--panel2); } .langs select { flex: 1; min-width: 0; padding: 6px 8px; font: inherit; @@ -43,10 +60,14 @@ .zone label .action { color: var(--mut); font-size: 10.5px; cursor: pointer; letter-spacing: normal; text-transform: none; } .zone label .action:hover { color: var(--acid); } + /* --tsize drives both panes so zoom keeps them matched. 15px is the + default: the old 13.5px was small for reading a paragraph of a language + you don't know well, which is the whole job here. */ textarea, .output { flex: 1; padding: 8px 14px 14px; border: none; outline: none; resize: none; background: transparent; color: var(--ink); - font: 13.5px/1.55 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; + font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; + font-size: var(--tsize, 15px); line-height: 1.55; overflow-y: auto; white-space: pre-wrap; word-wrap: break-word; } textarea::placeholder { color: var(--dim); } @@ -79,7 +100,15 @@
-
🌐 Translate
+
+ + Translate + + + 100% + + +
ready
@@ -176,7 +205,20 @@ // current working default so the panel isn't broken out of the box. const DEAD_LT_URLS = new Set(["https://translate.argosopentech.com"]); const settingsState = { backend: "libretranslate", ltUrl: DEFAULT_LT_URL, ltKey: "" }; - const uiState = { src: "auto", tgt: defaultTarget }; + const uiState = { src: "auto", tgt: defaultTarget, zoom: 100 }; + + // ---- text zoom --------------------------------------------------------- + // Percent of the 15px base, clamped so the panes stay usable at either end. + const ZOOM_MIN = 70, ZOOM_MAX = 220, ZOOM_STEP = 10, ZOOM_BASE = 15; + function applyZoom() { + const z = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, uiState.zoom || 100)); + uiState.zoom = z; + document.documentElement.style.setProperty("--tsize", (ZOOM_BASE * z / 100).toFixed(2) + "px"); + $("zoomPct").textContent = z + "%"; + $("zoomOut").disabled = z <= ZOOM_MIN; + $("zoomIn").disabled = z >= ZOOM_MAX; + } + function setZoom(z) { uiState.zoom = z; applyZoom(); saveUi(); } async function loadState() { if (!S) return; @@ -196,6 +238,7 @@ ltKey.value = settingsState.ltKey; srcSel.value = uiState.src; tgtSel.value = uiState.tgt; + applyZoom(); onBackendChange(); } async function saveSettings() { @@ -293,12 +336,38 @@ backendSel.addEventListener("change", async () => { onBackendChange(); await saveSettings(); }); ltUrl.addEventListener("change", saveSettings); ltKey.addEventListener("change", saveSettings); - srcSel.addEventListener("change", saveUi); - tgtSel.addEventListener("change", saveUi); + // Changing either language re-runs the translation straight away. Picking a + // new target and then having to hit Translate was the single most confusing + // thing about this panel: the old handler saved the choice and stopped + // there, so the pane below kept showing the previous language's text with + // no sign it was stale. + async function onLangChange() { + await saveUi(); + if (!inp.value.trim()) return; // nothing to re-translate + const src = srcSel.value, tgt = tgtSel.value; + if (src !== "auto" && src === tgt) return; // doTranslate would just echo + doTranslate(); + } + srcSel.addEventListener("change", onLangChange); + tgtSel.addEventListener("change", onLangChange); - // Ctrl/Cmd+Enter to translate. - inp.addEventListener("keydown", (e) => { - if ((e.ctrlKey || e.metaKey) && e.key === "Enter") { e.preventDefault(); doTranslate(); } + // Zoom controls. + $("zoomIn").addEventListener("click", () => setZoom((uiState.zoom || 100) + ZOOM_STEP)); + $("zoomOut").addEventListener("click", () => setZoom((uiState.zoom || 100) - ZOOM_STEP)); + $("zoomPct").addEventListener("click", () => setZoom(100)); + $("zoomPct").addEventListener("keydown", (e) => { + if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setZoom(100); } + }); + + // Ctrl/Cmd+Enter to translate; Ctrl/Cmd +/-/0 to zoom the text panes. + // Bound on the document so it works whichever pane has focus. + document.addEventListener("keydown", (e) => { + if (!(e.ctrlKey || e.metaKey)) return; + if (e.key === "Enter" && e.target === inp) { e.preventDefault(); doTranslate(); return; } + // "=" is the unshifted "+" on most layouts; accept both. + if (e.key === "+" || e.key === "=") { e.preventDefault(); setZoom((uiState.zoom || 100) + ZOOM_STEP); } + else if (e.key === "-") { e.preventDefault(); setZoom((uiState.zoom || 100) - ZOOM_STEP); } + else if (e.key === "0") { e.preventDefault(); setZoom(100); } }); // Pending selection from a right-click. When the addon dispatches @@ -321,7 +390,8 @@ // becomes visible again (the sidebar preload dispatches a "sidebar- // visibility" event on its window). (async () => { - await loadState(); + applyZoom(); // paint the control before storage answers (or if it never does) + await loadState(); // re-applies with the saved zoom once it's read await drainPending(); inp.focus(); })();