openvibers/openvibe.tools Commits

Use shared local picker for watermark and thumbnail images (#309)

OP OpenVibers committed 2 days ago · 5a6dde8040 · 3 files, +64 −45

Changes

ddiff --git a/apps/text/public/thumbnail.html b/apps/text/public/thumbnail.html
iindex 89ea2db..74ec29f 100644
---- a/apps/text/public/thumbnail.html
++++ b/apps/text/public/thumbnail.html
@@@ -19,6 +19,7 @@
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/shared.css">
++ <link rel="stylesheet" href="/shared/ov-upload.css">
<script src="/shared/theme-loader.js" defer></script>
<style>
.thumb-layout { display: grid; grid-template-columns: 1fr 320px; gap: 2rem; align-items: start; }
@@@ -37,9 +38,6 @@
.ctrl-row input[type="color"] { width: 36px; height: 30px; border: 1px solid var(--border); background: var(--bg-secondary); border-radius: 8px; cursor: pointer; padding: 2px; }
.ctrl-row input[type="range"] { flex: 1; }
.ctrl-row .val { min-width: 32px; text-align: right; font-size: .78rem; color: var(--text-muted); font-family: monospace; }
-- .upload-btn { width: 100%; padding: .75rem; border-radius: 12px; border: 2px dashed var(--border); background: var(--bg-tertiary); color: var(--text-secondary); cursor: pointer; text-align: center; transition: all var(--ov-dur-2, .2s); font-size: .9rem; }
-- .upload-btn:hover { border-color: var(--accent); color: var(--accent); }
-- .upload-btn input { display: none; }
.size-pills { display: flex; gap: .4rem; flex-wrap: wrap; }
.size-pill { padding: .35rem .7rem; border-radius: 16px; border: 1px solid var(--border); background: var(--bg-tertiary); color: var(--text-secondary); font-size: .75rem; cursor: pointer; transition: all var(--ov-dur-2, .15s); }
.size-pill:hover, .size-pill.active { border-color: var(--accent); color: var(--accent); background: rgba(192,150,92,0.1); }
@@@ -70,10 +68,9 @@
<div class="thumb-controls">
<div class="ctrl-group">
<h3><i class="fas fa-image"></i> Background</h3>
-- <label class="upload-btn" id="uploadBtn">
-- <input type="file" id="fileInput" accept="image/*">
-- <i class="fas fa-cloud-arrow-up"></i> Upload Background Image
-- </label>
++ <div id="backgroundPicker"></div>
++ <p class="subtitle">PNG, JPG, WebP or another image · up to 20 MB. Your image stays in this browser.</p>
++ <p id="backgroundError" role="alert" hidden></p>
<div class="ctrl-row" style="margin-top: .75rem;">
<label>BG Color</label>
<input type="color" id="bgColor" value="#0b0c12">
@@@ -128,6 +125,7 @@
<script src="/shared/account-switcher.js"></script>
<script src="/js/logo-engine.js"></script>
<script src="/js/shared.js"></script>
++ <script src="/shared/ov-upload.js"></script>
<script>
OpenVibeUI.initPage();
const E = LogoOpenVibeEngine;
@@@ -153,17 +151,28 @@
};
});
-- // File upload
-- document.getElementById('fileInput').addEventListener('change', e => {
-- const file = e.target.files[0];
-- if (!file) return;
-- const reader = new FileReader();
-- reader.onload = ev => {
-- const img = new Image();
-- img.onload = () => { bgImg = img; renderThumb(); };
-- img.src = ev.target.result;
-- };
-- reader.readAsDataURL(file);
++ // Local background image: the shared picker owns drops, file rows and removal.
++ const backgroundError = document.getElementById('backgroundError');
++ const backgroundPicker = OVUpload.picker(document.getElementById('backgroundPicker'), {
++ label: 'Choose a background image', accept: 'image/*', multiple: false,
++ onFile: (file, row) => {
++ if (!file.type.startsWith('image/') || !file.size || file.size > 20 * 1048576) {
++ row.dispatchEvent(new Event('remove')); row.remove();
++ backgroundError.textContent = file.size > 20 * 1048576 ? 'Choose an image under 20 MB.' : 'Choose an image this browser can read.';
++ backgroundError.hidden = false; return;
++ }
++ backgroundError.hidden = true;
++ const reader = new FileReader();
++ reader.onload = ev => {
++ const img = new Image();
++ img.onload = () => { backgroundPicker.rows.querySelectorAll('.ov-upload__row').forEach(old => { if (old !== row) { old.dispatchEvent(new Event('remove')); old.remove(); } }); bgImg = img; renderThumb(); };
++ img.onerror = () => { row.dispatchEvent(new Event('remove')); row.remove(); backgroundError.textContent = 'That image could not be opened.'; backgroundError.hidden = false; };
++ img.src = ev.target.result;
++ };
++ reader.onerror = () => { row.dispatchEvent(new Event('remove')); row.remove(); backgroundError.textContent = 'That image could not be read.'; backgroundError.hidden = false; };
++ reader.readAsDataURL(file);
++ },
++ onRemove: () => { bgImg = null; renderThumb(); }
});
async function renderThumb() {
ddiff --git a/apps/text/public/watermark.html b/apps/text/public/watermark.html
iindex 38ce1a6..4faa3cd 100644
---- a/apps/text/public/watermark.html
++++ b/apps/text/public/watermark.html
@@@ -19,6 +19,7 @@
</script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/shared.css">
++ <link rel="stylesheet" href="/shared/ov-upload.css">
<script src="/shared/theme-loader.js" defer></script>
<style>
.wm-layout { display: grid; grid-template-columns: 1fr 320px; gap: 2rem; align-items: start; }
@@@ -26,9 +27,6 @@
.wm-preview canvas { max-width: 100%; height: auto; border-radius: 8px; }
.wm-preview .placeholder { color: var(--text-muted); font-size: 1.1rem; }
.wm-preview .placeholder i { font-size: 3rem; display: block; margin-bottom: 1rem; color: var(--border); }
-- .drop-zone { border: 2px dashed var(--border); border-radius: var(--radius); padding: 2rem; text-align: center; cursor: pointer; transition: all var(--ov-dur-2, .2s); }
-- .drop-zone:hover, .drop-zone.drag-over { border-color: var(--accent); background: rgba(192,150,92,0.05); }
-- .drop-zone input { display: none; }
.wm-controls { display: flex; flex-direction: column; gap: 1rem; }
.ctrl-group { background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); padding: 1.25rem; }
.ctrl-group h3 { margin: 0 0 .75rem; color: var(--accent); font-size: .9rem; }
@@@ -53,11 +51,9 @@
<div class="wm-layout">
<div>
-- <div class="drop-zone" id="dropZone" onclick="document.getElementById('fileInput').click()">
-- <input type="file" id="fileInput" accept="image/*">
-- <i class="fas fa-cloud-arrow-up" style="font-size: 2rem; color: var(--text-muted); margin-bottom: .5rem; display: block;"></i>
-- <p style="color: var(--text-secondary); margin: 0;">Click or drag an image here</p>
-- </div>
++ <div id="imagePicker"></div>
++ <p class="subtitle">PNG, JPG, WebP or another image · up to 20 MB. Your image stays in this browser.</p>
++ <p id="imageError" role="alert" hidden></p>
<div class="wm-preview" id="previewArea" style="margin-top: 1.5rem;">
<div class="placeholder" id="placeholder">
<i class="fas fa-image"></i>
@@@ -111,6 +107,7 @@
<script src="/shared/account-switcher.js"></script>
<script src="/js/logo-engine.js"></script>
<script src="/js/shared.js"></script>
++ <script src="/shared/ov-upload.js"></script>
<script>
OpenVibeUI.initPage();
const E = LogoOpenVibeEngine;
@@@ -132,25 +129,29 @@
};
});
-- // File handling
-- const dropZone = document.getElementById('dropZone');
-- const fileInput = document.getElementById('fileInput');
--
-- ['dragenter', 'dragover'].forEach(evt => dropZone.addEventListener(evt, e => { e.preventDefault(); dropZone.classList.add('drag-over'); }));
-- ['dragleave', 'drop'].forEach(evt => dropZone.addEventListener(evt, e => { e.preventDefault(); dropZone.classList.remove('drag-over'); }));
-- dropZone.addEventListener('drop', e => { if (e.dataTransfer.files.length) handleFile(e.dataTransfer.files[0]); });
-- fileInput.addEventListener('change', () => { if (fileInput.files.length) handleFile(fileInput.files[0]); });
--
-- function handleFile(file) {
-- if (!file.type.startsWith('image/')) return OpenVibeUI.showToast('Please upload an image file', 'error');
-- const reader = new FileReader();
-- reader.onload = e => {
-- const img = new Image();
-- img.onload = () => { loadedImg = img; document.getElementById('placeholder').style.display = 'none'; canvas.style.display = 'block'; document.getElementById('dlBtn').disabled = false; renderPreview(); };
-- img.src = e.target.result;
-- };
-- reader.readAsDataURL(file);
-- }
++ // Local image selection: the shared picker owns drops, file rows and removal.
++ const imageError = document.getElementById('imageError');
++ const picker = OVUpload.picker(document.getElementById('imagePicker'), {
++ label: 'Choose an image to watermark', accept: 'image/*', multiple: false,
++ onFile: (file, row) => {
++ if (!file.type.startsWith('image/') || !file.size || file.size > 20 * 1048576) {
++ row.dispatchEvent(new Event('remove')); row.remove();
++ imageError.textContent = file.size > 20 * 1048576 ? 'Choose an image under 20 MB.' : 'Choose an image this browser can read.';
++ imageError.hidden = false; return;
++ }
++ imageError.hidden = true;
++ const reader = new FileReader();
++ reader.onload = e => {
++ const img = new Image();
++ img.onload = () => { picker.rows.querySelectorAll('.ov-upload__row').forEach(old => { if (old !== row) { old.dispatchEvent(new Event('remove')); old.remove(); } }); loadedImg = img; document.getElementById('placeholder').style.display = 'none'; canvas.style.display = 'block'; document.getElementById('dlBtn').disabled = false; renderPreview(); };
++ img.onerror = () => { row.dispatchEvent(new Event('remove')); row.remove(); imageError.textContent = 'That image could not be opened.'; imageError.hidden = false; };
++ img.src = e.target.result;
++ };
++ reader.onerror = () => { row.dispatchEvent(new Event('remove')); row.remove(); imageError.textContent = 'That image could not be read.'; imageError.hidden = false; };
++ reader.readAsDataURL(file);
++ },
++ onRemove: () => { loadedImg = null; document.getElementById('placeholder').style.display = ''; canvas.style.display = 'none'; document.getElementById('dlBtn').disabled = true; }
++ });
function hexToRgba(hex, alpha) {
const r = parseInt(hex.slice(1, 3), 16);
ddiff --git a/apps/text/test/frame.test.js b/apps/text/test/frame.test.js
iindex cd9519c..eb43538 100644
---- a/apps/text/test/frame.test.js
++++ b/apps/text/test/frame.test.js
@@@ -42,6 +42,15 @@ function pageOf(host, file) {
return frameAround(html, host, { file, hostnameMap: HOSTNAME_MAP });
}
++// Browser-only image tools use the same accessible picker without sending files to a server.
++for (const [host, file] of [['watermark.openvibe.tools', 'watermark.html'], ['thumbnail.openvibe.tools', 'thumbnail.html']]) {
++ const html = pageOf(host, file);
++ assert.ok(html.includes('/shared/ov-upload.css') && html.includes('/shared/ov-upload.js'), `${host}: shared picker assets`);
++ assert.ok(html.includes('OVUpload.picker(') && html.includes('role="alert"'), `${host}: picker and readable errors`);
++ assert.ok(html.includes('Your image stays in this browser.'), `${host}: local processing is explained`);
++ assert.ok(!html.includes('type="file"'), `${host}: old picker is gone`);
++}
++
// ── Every text tool's page is a framed tool page, in the same order ──
{
for (const [host, file] of TOOL_HOSTS) {