Skip to content

Commit dc4ef04

Browse files
committed
fix: restore truncated tails of app-media.js and app-ui.js (corrupted in 361d584, blanked the app)
1 parent ddfecc1 commit dc4ef04

2 files changed

Lines changed: 193 additions & 2 deletions

File tree

public/js/modules/app-media.js

Lines changed: 187 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2677,4 +2677,190 @@ _setupModalExpand() {
26772677
// divs, settings headers, etc) without depending on h3 internal flex.
26782678
const _injectModalControls = () => {
26792679
document.querySelectorAll('.modal').forEach(modal => {
2680-
// Skip promo/cent
2680+
// Skip promo/centered popups and the media gallery (which has its own
2681+
// header close button) — they're not regular modals (#5352)
2682+
if (modal.classList.contains('android-beta-promo') ||
2683+
modal.classList.contains('desktop-promo') ||
2684+
modal.classList.contains('donors-modal-box') ||
2685+
modal.classList.contains('media-gallery-modal')) return;
2686+
// Idempotent — skip already-injected
2687+
if (modal.dataset.modalControlsInjected === '1') return;
2688+
modal.dataset.modalControlsInjected = '1';
2689+
2690+
// Settings/activities headers have their own close button — keep it
2691+
// but inject the expand toggle next to it.
2692+
const settingsClose = modal.querySelector('.settings-close-btn');
2693+
2694+
const expandBtn = document.createElement('button');
2695+
expandBtn.type = 'button';
2696+
expandBtn.className = 'modal-expand-btn';
2697+
expandBtn.title = 'Expand / Restore';
2698+
expandBtn.textContent = '⛶';
2699+
expandBtn.addEventListener('click', (e) => {
2700+
e.stopPropagation();
2701+
const isMax = modal.classList.toggle('modal-maximized');
2702+
expandBtn.textContent = isMax ? '⊖' : '⛶';
2703+
expandBtn.title = isMax ? 'Restore size' : 'Expand';
2704+
});
2705+
2706+
// When a settings-style header is present, slot the expand button
2707+
// directly next to its close button so the two stay aligned on
2708+
// every viewport size. Otherwise drop both controls into a floating
2709+
// group at the top-right of the modal.
2710+
if (settingsClose) {
2711+
expandBtn.classList.add('modal-expand-btn-inline');
2712+
settingsClose.parentElement.insertBefore(expandBtn, settingsClose);
2713+
} else {
2714+
const group = document.createElement('div');
2715+
group.className = 'modal-controls';
2716+
group.appendChild(expandBtn);
2717+
2718+
const closeBtn = document.createElement('button');
2719+
closeBtn.type = 'button';
2720+
closeBtn.className = 'modal-expand-btn';
2721+
closeBtn.title = 'Close';
2722+
closeBtn.textContent = '✕';
2723+
closeBtn.addEventListener('click', (e) => {
2724+
e.stopPropagation();
2725+
const overlay = modal.closest('.modal-overlay');
2726+
if (overlay) overlay.style.display = 'none';
2727+
if (modal.classList.contains('modal-maximized')) {
2728+
modal.classList.remove('modal-maximized');
2729+
expandBtn.textContent = '⛶';
2730+
expandBtn.title = 'Expand / Restore';
2731+
}
2732+
});
2733+
group.appendChild(closeBtn);
2734+
modal.appendChild(group);
2735+
}
2736+
});
2737+
};
2738+
_injectModalControls();
2739+
// Re-run if new modals get inserted later (some plugins/lazy templates)
2740+
this._injectModalControls = _injectModalControls;
2741+
},
2742+
2743+
/** Show a custom image context menu (Save / Copy / Open in tab) */
2744+
_showImageContextMenu(e, src) {
2745+
this._hideImageContextMenu();
2746+
const menu = document.createElement('div');
2747+
menu.id = 'image-context-menu';
2748+
menu.className = 'image-context-menu';
2749+
menu.innerHTML = `
2750+
<button data-action="save">💾 Save Image</button>
2751+
<button data-action="copy">📋 Copy Image</button>
2752+
<button data-action="open">🔗 Open in New Tab</button>
2753+
`;
2754+
menu.style.left = e.clientX + 'px';
2755+
menu.style.top = e.clientY + 'px';
2756+
document.body.appendChild(menu);
2757+
// Clamp to viewport
2758+
const rect = menu.getBoundingClientRect();
2759+
if (rect.right > window.innerWidth) menu.style.left = (window.innerWidth - rect.width - 8) + 'px';
2760+
if (rect.bottom > window.innerHeight) menu.style.top = (window.innerHeight - rect.height - 8) + 'px';
2761+
2762+
menu.addEventListener('click', async (ev) => {
2763+
const action = ev.target.dataset.action;
2764+
if (action === 'save') {
2765+
const a = document.createElement('a');
2766+
a.href = src;
2767+
a.download = src.split('/').pop().split('?')[0] || 'image';
2768+
a.style.display = 'none';
2769+
document.body.appendChild(a);
2770+
a.click();
2771+
a.remove();
2772+
} else if (action === 'copy') {
2773+
// Hide the menu immediately so it doesn't sit on screen during
2774+
// the async fetch + clipboard write. We still control the toast.
2775+
this._hideImageContextMenu();
2776+
(async () => {
2777+
const fetchAsBlob = async () => {
2778+
const resp = await fetch(src, { credentials: 'same-origin' });
2779+
if (!resp.ok) throw new Error('fetch ' + resp.status);
2780+
return await resp.blob();
2781+
};
2782+
const toPngBlob = async (blob) => {
2783+
if (blob.type === 'image/png') return blob;
2784+
const bitmap = await createImageBitmap(blob);
2785+
const canvas = document.createElement('canvas');
2786+
canvas.width = bitmap.width;
2787+
canvas.height = bitmap.height;
2788+
canvas.getContext('2d').drawImage(bitmap, 0, 0);
2789+
return await new Promise((res, rej) =>
2790+
canvas.toBlob(b => b ? res(b) : rej(new Error('toBlob null')), 'image/png'));
2791+
};
2792+
const blobToDataUrl = (blob) => new Promise((res, rej) => {
2793+
const r = new FileReader();
2794+
r.onload = () => res(r.result);
2795+
r.onerror = () => rej(r.error || new Error('FileReader failed'));
2796+
r.readAsDataURL(blob);
2797+
});
2798+
2799+
// Strategy 1: Electron desktop IPC (most reliable — main process
2800+
// clipboard has no user-gesture requirement).
2801+
if (window.havenDesktop?.clipboardWriteImage) {
2802+
try {
2803+
const blob = await fetchAsBlob();
2804+
const png = await toPngBlob(blob);
2805+
const dataUrl = await blobToDataUrl(png);
2806+
const res = await window.havenDesktop.clipboardWriteImage(dataUrl);
2807+
if (res?.ok) { this._showToast('Image copied to clipboard', 'success'); return; }
2808+
console.warn('[Haven] IPC clipboard write failed:', res?.reason);
2809+
} catch (err) {
2810+
console.warn('[Haven] IPC clipboard path errored:', err);
2811+
}
2812+
}
2813+
2814+
// Strategy 2: web navigator.clipboard.write with promise-based
2815+
// ClipboardItem (preserves gesture chain across async fetch).
2816+
try {
2817+
if (typeof ClipboardItem === 'undefined' || !navigator.clipboard?.write) {
2818+
throw new Error('Clipboard API unavailable');
2819+
}
2820+
const blobPromise = (async () => toPngBlob(await fetchAsBlob()))();
2821+
await navigator.clipboard.write([
2822+
new ClipboardItem({ 'image/png': blobPromise })
2823+
]);
2824+
this._showToast('Image copied to clipboard', 'success');
2825+
return;
2826+
} catch (err) {
2827+
console.error('[Haven] Web clipboard.write failed:', err);
2828+
// Strategy 3: at least put the URL on the clipboard so the
2829+
// user has something to paste.
2830+
try {
2831+
await navigator.clipboard.writeText(src);
2832+
this._showToast('Copied image URL (browser blocked image copy)', 'warning');
2833+
return;
2834+
} catch (err2) {
2835+
console.error('[Haven] writeText fallback failed:', err2);
2836+
this._showToast('Failed to copy image: ' + (err.message || err), 'error');
2837+
}
2838+
}
2839+
})();
2840+
return;
2841+
} else if (action === 'open') {
2842+
window.open(src, '_blank', 'noopener,noreferrer');
2843+
}
2844+
this._hideImageContextMenu();
2845+
});
2846+
2847+
// Close on click elsewhere
2848+
const closer = (ev) => {
2849+
if (!menu.contains(ev.target)) {
2850+
this._hideImageContextMenu();
2851+
document.removeEventListener('click', closer, true);
2852+
document.removeEventListener('contextmenu', closer, true);
2853+
}
2854+
};
2855+
setTimeout(() => {
2856+
document.addEventListener('click', closer, true);
2857+
document.addEventListener('contextmenu', closer, true);
2858+
}, 0);
2859+
},
2860+
2861+
_hideImageContextMenu() {
2862+
const existing = document.getElementById('image-context-menu');
2863+
if (existing) existing.remove();
2864+
},
2865+
2866+
};

public/js/modules/app-ui.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6317,4 +6317,9 @@ _showPersonaEditor(id) {
63176317
editor.remove();
63186318
this._renderPersonasList();
63196319
} catch (err) {
6320-
this._showToast?.(err.message || 'Sav
6320+
this._showToast?.(err.message || 'Save failed', 'error');
6321+
}
6322+
});
6323+
},
6324+
6325+
};

0 commit comments

Comments
 (0)