overhaul rethumbing flashs
This commit is contained in:
@@ -10017,7 +10017,7 @@ body.modal-open {
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
@@ -219,7 +219,6 @@
|
||||
|
||||
.upload-form:not(.shitpost-mode-active) .preview-media-small {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
min-height: auto;
|
||||
height: auto;
|
||||
aspect-ratio: 16 / 9;
|
||||
@@ -257,6 +256,7 @@
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-info-small {
|
||||
@@ -442,7 +442,7 @@
|
||||
|
||||
.preview-media-small {
|
||||
flex: 0 0 50%;
|
||||
width: 50% !important;
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
min-height: 120px;
|
||||
max-height: 350px;
|
||||
@@ -1345,3 +1345,138 @@
|
||||
50% { opacity: 0.8; }
|
||||
100% { opacity: 0.4; }
|
||||
}
|
||||
|
||||
/* ── Ruffle (Flash) Upload Preview ─────────────────────────────────────────── */
|
||||
|
||||
/* The container: CSS Grid with 3 rows — player | snapshot preview | button.
|
||||
All rows are auto so the container grows naturally; no overflow clipping. */
|
||||
.swf-upload-preview {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-rows: auto auto auto;
|
||||
max-height: none !important;
|
||||
background: #000;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* Normal (non-shitpost) mode: full-width, player drives container height via aspect-ratio */
|
||||
.upload-form:not(.shitpost-mode-active) .swf-upload-preview.preview-media-small {
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
max-height: none !important;
|
||||
min-height: auto !important;
|
||||
}
|
||||
|
||||
.upload-form:not(.shitpost-mode-active) .swf-upload-preview ruffle-player,
|
||||
.upload-form:not(.shitpost-mode-active) .swf-upload-preview ruffle-object {
|
||||
aspect-ratio: 16 / 9;
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
/* Shitpost mode: player row is a fixed height so the snapshot row doesn't steal its space */
|
||||
.upload-form.shitpost-mode-active .swf-upload-preview.preview-media-small {
|
||||
width: 45% !important;
|
||||
flex: 0 0 45% !important;
|
||||
height: auto !important;
|
||||
max-height: none !important;
|
||||
min-height: auto !important;
|
||||
aspect-ratio: unset;
|
||||
}
|
||||
|
||||
.upload-form.shitpost-mode-active .swf-upload-preview ruffle-player,
|
||||
.upload-form.shitpost-mode-active .swf-upload-preview ruffle-object {
|
||||
width: 100% !important;
|
||||
height: 220px !important;
|
||||
min-height: 220px;
|
||||
}
|
||||
|
||||
/* Placeholder shown while Ruffle is loading */
|
||||
.swf-upload-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
animation: thumbPulse 1.8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.swf-upload-placeholder-icon {
|
||||
font-size: 2.5rem;
|
||||
line-height: 1;
|
||||
filter: drop-shadow(0 0 8px rgba(255, 200, 0, 0.7));
|
||||
}
|
||||
|
||||
.swf-upload-placeholder-text {
|
||||
font-size: 0.8rem;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ── Ruffle Snapshot Button ─────────────────────────────────────────────────── */
|
||||
|
||||
/* The snapshot button lives inside .swf-upload-preview, pinned to the bottom */
|
||||
.swf-upload-preview {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn-ruffle-snapshot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
margin-top: 0;
|
||||
padding: 8px 16px;
|
||||
background: rgba(255, 200, 0, 0.08);
|
||||
border: none;
|
||||
border-top: 1px solid rgba(255, 200, 0, 0.2);
|
||||
border-radius: 0 0 6px 6px;
|
||||
color: rgba(255, 200, 0, 0.9);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
letter-spacing: 0.3px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.btn-ruffle-snapshot:hover:not(:disabled) {
|
||||
background: rgba(255, 200, 0, 0.15);
|
||||
border-top-color: rgba(255, 200, 0, 0.4);
|
||||
color: #ffd700;
|
||||
}
|
||||
|
||||
.btn-ruffle-snapshot:active:not(:disabled) {
|
||||
background: rgba(255, 200, 0, 0.22);
|
||||
}
|
||||
|
||||
.btn-ruffle-snapshot:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
|
||||
/* Snapshot preview: sits in its own grid row, below the player */
|
||||
.swf-upload-preview .ruffle-snapshot-preview {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border-top: 1px solid rgba(81, 207, 102, 0.3);
|
||||
animation: snapPreviewIn 0.35s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
}
|
||||
|
||||
@keyframes snapPreviewIn {
|
||||
from { opacity: 0; transform: scale(0.96) translateY(4px); }
|
||||
to { opacity: 1; transform: scale(1) translateY(0); }
|
||||
}
|
||||
|
||||
|
||||
@@ -1658,7 +1658,8 @@ window.cancelAnimFrame = (function () {
|
||||
"unmuteOverlay": "hidden",
|
||||
"letterbox": "on",
|
||||
"warnOnUnsupportedContent": false,
|
||||
"contextMenu": "on"
|
||||
"contextMenu": "on",
|
||||
"splashScreen": false
|
||||
};
|
||||
|
||||
let ruffleKeepAliveApplied = false;
|
||||
@@ -1743,15 +1744,46 @@ window.cancelAnimFrame = (function () {
|
||||
|
||||
applyRuffleKeepAlive();
|
||||
|
||||
// Detect whether the item is currently behind a blur overlay.
|
||||
// If so, load the SWF with autoplay:"off" so it stays paused until the user reveals it.
|
||||
const mediaObj = container.closest('.media-object');
|
||||
let blurActive = false;
|
||||
if (mediaObj && !mediaObj.classList.contains('revealed') && localStorage.getItem('blurDetail') !== 'false') {
|
||||
const blurNsfw = localStorage.getItem('blurNsfw') === 'true';
|
||||
const blurNsfl = localStorage.getItem('blurNsfl') === 'true';
|
||||
const blurSfw = localStorage.getItem('blurSfw') === 'true';
|
||||
const blurUntagged = localStorage.getItem('blurUntagged') === 'true';
|
||||
const mode = mediaObj.getAttribute('data-mode');
|
||||
blurActive =
|
||||
(mode === 'nsfw' && blurNsfw) ||
|
||||
(mode === 'nsfl' && blurNsfl) ||
|
||||
(mode === 'sfw' && blurSfw) ||
|
||||
(mode === 'untagged' && blurUntagged);
|
||||
}
|
||||
|
||||
// Update config with latest session preferences just before creation
|
||||
if (window.RufflePlayer) {
|
||||
window.RufflePlayer.config = window.RuffleConfig = {
|
||||
...window.RuffleConfig,
|
||||
"autoplay": blurActive ? "off" : "on",
|
||||
"pageVisibility": window.f0ckSession?.ruffle_background === false,
|
||||
"backgroundExecution": window.f0ckSession?.ruffle_background === false ? undefined : "Unthrottled"
|
||||
"backgroundExecution": window.f0ckSession?.ruffle_background === false ? undefined : "Unthrottled",
|
||||
"splashScreen": false
|
||||
};
|
||||
}
|
||||
|
||||
// Patch WebGL getContext to force preserveDrawingBuffer:true so canvas
|
||||
// frames can be captured after Ruffle renders (WebGL clears buffers by default)
|
||||
const _origGetCtx = HTMLCanvasElement.prototype.getContext;
|
||||
HTMLCanvasElement.prototype.getContext = function(type, attrs) {
|
||||
if (type === 'webgl' || type === 'webgl2' || type === 'experimental-webgl') {
|
||||
attrs = Object.assign({}, attrs, { preserveDrawingBuffer: true });
|
||||
}
|
||||
return _origGetCtx.call(this, type, attrs);
|
||||
};
|
||||
// Restore after Ruffle has created its context
|
||||
setTimeout(() => { HTMLCanvasElement.prototype.getContext = _origGetCtx; }, 8000);
|
||||
|
||||
function createPlayer() {
|
||||
if (!window.RufflePlayer) return;
|
||||
if (typeof window.RufflePlayer.newest !== 'function') return; // WASM still initializing
|
||||
@@ -3683,7 +3715,158 @@ window.cancelAnimFrame = (function () {
|
||||
e.preventDefault();
|
||||
const reBtn = target.closest('#a_rethumb');
|
||||
const itemId = reBtn.dataset.itemId;
|
||||
|
||||
|
||||
// Helper: POST a blob as thumbnail
|
||||
const submitRethumb = (blob, currentItemId) => {
|
||||
const fd = new FormData();
|
||||
fd.append('file', blob, 'thumbnail.jpg');
|
||||
window.flashMessage(i18n.uploading || 'Uploading...');
|
||||
fetch(`/api/v2/items/${currentItemId}/thumbnail`, {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-Token': window.f0ckSession?.csrf_token },
|
||||
body: fd
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
window.flashMessage(data.msg);
|
||||
if (window.refreshItemThumbnails) window.refreshItemThumbnails(currentItemId);
|
||||
} else {
|
||||
window.flashError(data.msg || 'Upload failed');
|
||||
}
|
||||
})
|
||||
.catch(err => { window.flashError('Upload error'); console.error(err); });
|
||||
};
|
||||
|
||||
// For Flash items: show a capture modal instead of a file picker
|
||||
const rufflePlayer = document.querySelector('#ruffle-container ruffle-player');
|
||||
if (rufflePlayer) {
|
||||
|
||||
const tryFindCanvas = (root) => {
|
||||
if (!root) return null;
|
||||
const c = root.querySelector('canvas');
|
||||
if (c) return c;
|
||||
for (const el of root.querySelectorAll('*')) {
|
||||
if (el.shadowRoot) { const found = tryFindCanvas(el.shadowRoot); if (found) return found; }
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// POST blob to thumbnail API and close modal
|
||||
const submitRethumb = (blob) => {
|
||||
const fd = new FormData();
|
||||
fd.append('file', blob, 'thumbnail.jpg');
|
||||
window.flashMessage(i18n.uploading || 'Uploading...');
|
||||
fetch(`/api/v2/items/${itemId}/thumbnail`, {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-Token': window.f0ckSession?.csrf_token },
|
||||
body: fd
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
window.flashMessage(data.msg);
|
||||
if (window.refreshItemThumbnails) window.refreshItemThumbnails(itemId);
|
||||
} else {
|
||||
window.flashError(data.msg || 'Upload failed');
|
||||
}
|
||||
})
|
||||
.catch(err => { window.flashError('Upload error'); console.error(err); });
|
||||
};
|
||||
|
||||
// Capture current Ruffle frame → return blob via callback
|
||||
const captureFrame = (cb) => {
|
||||
const canvas = tryFindCanvas(rufflePlayer.shadowRoot || rufflePlayer) || tryFindCanvas(rufflePlayer);
|
||||
if (!canvas || canvas.width === 0 || canvas.height === 0) {
|
||||
cb(null);
|
||||
return;
|
||||
}
|
||||
const out = document.createElement('canvas');
|
||||
const MAX = 640;
|
||||
const w = canvas.width, h = canvas.height;
|
||||
if (w > MAX || h > MAX) {
|
||||
const ratio = Math.min(MAX / w, MAX / h);
|
||||
out.width = Math.round(w * ratio);
|
||||
out.height = Math.round(h * ratio);
|
||||
} else {
|
||||
out.width = w || 320;
|
||||
out.height = h || 240;
|
||||
}
|
||||
out.getContext('2d').drawImage(canvas, 0, 0, out.width, out.height);
|
||||
out.toBlob(cb, 'image/jpeg', 0.92);
|
||||
};
|
||||
|
||||
// Build & show modal using site CSS classes
|
||||
const showCaptureModal = (initialBlob) => {
|
||||
document.getElementById('rethumb-capture-modal')?.remove();
|
||||
|
||||
let currentBlob = initialBlob;
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'rethumb-capture-modal';
|
||||
overlay.className = 'modal-overlay';
|
||||
overlay.style.zIndex = '99999';
|
||||
|
||||
const box = document.createElement('div');
|
||||
box.className = 'modal-content';
|
||||
box.style.cssText = 'max-width:480px;width:90%;text-align:left;position:relative;';
|
||||
|
||||
const title = document.createElement('h3');
|
||||
title.textContent = 'Thumbnail Preview';
|
||||
title.style.marginTop = '0';
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.src = URL.createObjectURL(initialBlob);
|
||||
img.style.cssText = 'width:100%;height:auto;display:block;border-radius:0;margin-bottom:4px;';
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'modal-actions';
|
||||
|
||||
const btnUse = document.createElement('button');
|
||||
btnUse.textContent = 'Use as Thumbnail';
|
||||
btnUse.className = 'btn-danger';
|
||||
|
||||
const btnRecapture = document.createElement('button');
|
||||
btnRecapture.textContent = 'Capture Again';
|
||||
btnRecapture.className = 'btn-secondary';
|
||||
|
||||
const btnCancel = document.createElement('button');
|
||||
btnCancel.textContent = 'Cancel';
|
||||
btnCancel.className = 'btn-secondary';
|
||||
|
||||
const close = () => overlay.remove();
|
||||
|
||||
btnUse.onclick = () => { submitRethumb(currentBlob); close(); };
|
||||
btnRecapture.onclick = () => {
|
||||
captureFrame((blob) => {
|
||||
if (!blob) { window.flashError('Ruffle not ready'); return; }
|
||||
URL.revokeObjectURL(img.src);
|
||||
currentBlob = blob;
|
||||
img.src = URL.createObjectURL(blob);
|
||||
});
|
||||
};
|
||||
btnCancel.onclick = close;
|
||||
overlay.onclick = (ev) => { if (ev.target === overlay) close(); };
|
||||
|
||||
actions.append(btnUse, btnRecapture, btnCancel);
|
||||
box.append(title, img, actions);
|
||||
overlay.appendChild(box);
|
||||
document.body.appendChild(overlay);
|
||||
};
|
||||
|
||||
captureFrame((blob) => {
|
||||
if (blob) {
|
||||
showCaptureModal(blob);
|
||||
} else {
|
||||
window.flashError('Ruffle not ready — play the Flash first');
|
||||
}
|
||||
});
|
||||
|
||||
return; // never open file picker for Flash items
|
||||
}
|
||||
|
||||
|
||||
// Non-Flash items: use the file picker as before
|
||||
let fileInput = document.getElementById('rethumb-file-input');
|
||||
if (!fileInput) {
|
||||
fileInput = document.createElement('input');
|
||||
@@ -3692,52 +3875,24 @@ window.cancelAnimFrame = (function () {
|
||||
fileInput.accept = 'image/png, image/jpeg, image/gif, image/webp';
|
||||
fileInput.style.display = 'none';
|
||||
document.body.appendChild(fileInput);
|
||||
|
||||
|
||||
fileInput.addEventListener('change', function(evt) {
|
||||
const file = evt.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
window.flashError('File is too large (max 5MB)');
|
||||
fileInput.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const currentItemId = fileInput.dataset.itemId;
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
|
||||
window.flashMessage(i18n.uploading || 'Uploading...');
|
||||
|
||||
fetch(`/api/v2/items/${currentItemId}/thumbnail`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-Token': window.f0ckSession?.csrf_token
|
||||
},
|
||||
body: fd
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
fileInput.value = '';
|
||||
if (data.success) {
|
||||
window.flashMessage(data.msg);
|
||||
if (window.refreshItemThumbnails) {
|
||||
window.refreshItemThumbnails(currentItemId);
|
||||
}
|
||||
} else {
|
||||
window.flashError(data.msg || 'Upload failed');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
fileInput.value = '';
|
||||
window.flashError('Upload error');
|
||||
console.error(err);
|
||||
});
|
||||
submitRethumb(file, currentItemId);
|
||||
fileInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
fileInput.dataset.itemId = itemId;
|
||||
fileInput.click();
|
||||
|
||||
} else if (target.closest('button#a_toggle')) {
|
||||
e.preventDefault();
|
||||
const toggleBtn = target.closest('button#a_toggle');
|
||||
@@ -9849,12 +10004,18 @@ document.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
mediaObj.classList.add('revealed');
|
||||
|
||||
|
||||
// Start audio/video playback cleanly on reveal
|
||||
const videoElem = mediaObj.querySelector('video') || mediaObj.querySelector('audio');
|
||||
if (videoElem) {
|
||||
videoElem.play().catch(() => {});
|
||||
}
|
||||
|
||||
// Start Ruffle playback if this is a Flash item — it was loaded with autoplay:"off"
|
||||
const rufflePlayer = mediaObj.querySelector('ruffle-player');
|
||||
if (rufflePlayer) {
|
||||
try { rufflePlayer.play(); } catch(err) {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -782,6 +782,7 @@ window.initUploadForm = (selector) => {
|
||||
// Basic validation (MIME/Extension/Size)
|
||||
const container = form.closest('.upload-container');
|
||||
const mimesSource = form.getAttribute('data-mimes') || (container ? container.getAttribute('data-mimes') : null);
|
||||
const swfEnabled = form.getAttribute('data-enable-swf') !== '0';
|
||||
let allowedMimes = [];
|
||||
let allowedExts = [];
|
||||
try {
|
||||
@@ -793,6 +794,19 @@ window.initUploadForm = (selector) => {
|
||||
}
|
||||
|
||||
const fileExt = file.name.split('.').pop().toLowerCase();
|
||||
const isSwfFile = fileExt === 'swf' ||
|
||||
file.type === 'application/x-shockwave-flash' ||
|
||||
file.type === 'application/vnd.adobe.flash.movie';
|
||||
|
||||
// Reject SWF when Flash uploads are disabled
|
||||
if (isSwfFile && !swfEnabled) {
|
||||
const errorMsg = 'Flash (.swf) uploads are disabled.';
|
||||
if (typeof window.flashMessage === 'function') window.flashMessage('✕ ' + errorMsg, 4000, 'error');
|
||||
else if (window.showFlash) window.showFlash(errorMsg, 'error');
|
||||
else if (statusDiv) { statusDiv.textContent = errorMsg; statusDiv.className = 'upload-status error'; }
|
||||
continue;
|
||||
}
|
||||
|
||||
const mimeOk = !file.type || allowedMimes.includes(file.type);
|
||||
const extOk = allowedExts.length > 0 && allowedExts.includes(fileExt);
|
||||
|
||||
@@ -952,8 +966,147 @@ window.initUploadForm = (selector) => {
|
||||
mediaElem.style.width = '100%';
|
||||
} else if (file.type === 'application/x-shockwave-flash' || file.type === 'application/vnd.adobe.flash.movie' || fileExt === 'swf') {
|
||||
mediaElem = document.createElement('div');
|
||||
mediaElem.className = 'generic-file-icon swf-preview-icon';
|
||||
mediaElem.innerHTML = '<span style="font-size:1.5em;">⚡</span>';
|
||||
mediaElem.className = 'swf-upload-preview';
|
||||
mediaElem.dataset.swfFile = 'pending';
|
||||
// Placeholder shown while Ruffle loads
|
||||
mediaElem.innerHTML = `<div class="swf-upload-placeholder"><span class="swf-upload-placeholder-icon">⚡</span><span class="swf-upload-placeholder-text">Loading Flash preview…</span></div>`;
|
||||
// Load Ruffle asynchronously once the element is in the DOM
|
||||
const swfObjectUrl = URL.createObjectURL(file);
|
||||
const ensureRuffleUpload = (cb) => {
|
||||
if (window.RufflePlayer && typeof window.RufflePlayer.newest === 'function') { cb(); return; }
|
||||
if (document.querySelector('script[src*="/s/ruffle/ruffle.js"]')) {
|
||||
// Script is loading, poll for it
|
||||
let attempts = 0;
|
||||
const poll = setInterval(() => {
|
||||
attempts++;
|
||||
if ((window.RufflePlayer && typeof window.RufflePlayer.newest === 'function') || attempts >= 80) {
|
||||
clearInterval(poll);
|
||||
cb();
|
||||
}
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
const s = document.createElement('script');
|
||||
s.src = '/s/ruffle/ruffle.js';
|
||||
s.onload = () => cb();
|
||||
s.onerror = () => cb(); // proceed even if fail
|
||||
document.head.appendChild(s);
|
||||
};
|
||||
// Defer init until next microtask so mediaElem is appended to DOM first
|
||||
Promise.resolve().then(() => {
|
||||
ensureRuffleUpload(() => {
|
||||
if (!mediaElem.isConnected) { URL.revokeObjectURL(swfObjectUrl); return; }
|
||||
const ruffle = window.RufflePlayer && window.RufflePlayer.newest ? window.RufflePlayer.newest() : null;
|
||||
if (!ruffle) {
|
||||
mediaElem.innerHTML = `<div class="swf-upload-placeholder"><span class="swf-upload-placeholder-icon">⚡</span><span class="swf-upload-placeholder-text">Flash preview unavailable</span></div>`;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Patch getContext BEFORE creating the player so that Ruffle's WebGL
|
||||
// context is created with preserveDrawingBuffer:true.
|
||||
// Without this, WebGL clears the drawing buffer after each frame
|
||||
// presentation, making canvas readback produce solid black.
|
||||
const _origGetCtx = HTMLCanvasElement.prototype.getContext;
|
||||
HTMLCanvasElement.prototype.getContext = function(type, attrs) {
|
||||
if (type === 'webgl' || type === 'webgl2' || type === 'experimental-webgl') {
|
||||
attrs = Object.assign({}, attrs || {}, { preserveDrawingBuffer: true });
|
||||
}
|
||||
return _origGetCtx.call(this, type, attrs);
|
||||
};
|
||||
const player = ruffle.createPlayer();
|
||||
player.style.cssText = 'width:100%;height:100%;display:block;border-radius:8px;';
|
||||
const placeholder = mediaElem.querySelector('.swf-upload-placeholder');
|
||||
if (placeholder) placeholder.remove();
|
||||
mediaElem.appendChild(player);
|
||||
player.load({ url: swfObjectUrl, config: { volume: 0.5 } });
|
||||
// Restore getContext after Ruffle's WASM finishes creating its GL context
|
||||
// (typically within ~2s of load; 6s is a safe upper bound)
|
||||
setTimeout(() => { HTMLCanvasElement.prototype.getContext = _origGetCtx; }, 6000);
|
||||
mediaElem._rufflePlayer = player;
|
||||
mediaElem._swfObjectUrl = swfObjectUrl;
|
||||
|
||||
// Inject snapshot button directly below the Ruffle player
|
||||
// (inside the .swf-upload-preview, so it travels with each file-preview-item)
|
||||
if (!mediaElem.querySelector('.btn-ruffle-snapshot')) {
|
||||
const snapBtn = document.createElement('button');
|
||||
snapBtn.type = 'button';
|
||||
snapBtn.className = 'btn-ruffle-snapshot';
|
||||
snapBtn.textContent = 'Capture Thumbnail';
|
||||
snapBtn.title = 'Capture the current frame of the Flash preview as the thumbnail';
|
||||
mediaElem.appendChild(snapBtn);
|
||||
}
|
||||
|
||||
// Wire snapshot button — scoped to this previewItem / mediaElem
|
||||
const wireSnapshot = () => {
|
||||
const snapBtn = mediaElem.querySelector('.btn-ruffle-snapshot');
|
||||
if (!snapBtn) return;
|
||||
snapBtn.onclick = async (e) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
// Try to find Ruffle's internal canvas via shadow DOM
|
||||
let canvas = null;
|
||||
const tryFindCanvas = (root) => {
|
||||
if (!root) return null;
|
||||
const c = root.querySelector('canvas');
|
||||
if (c) return c;
|
||||
for (const el of root.querySelectorAll('*')) {
|
||||
if (el.shadowRoot) {
|
||||
const found = tryFindCanvas(el.shadowRoot);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
canvas = tryFindCanvas(player.shadowRoot || player);
|
||||
if (!canvas) canvas = tryFindCanvas(player);
|
||||
if (canvas && canvas.width > 0 && canvas.height > 0) {
|
||||
const out = document.createElement('canvas');
|
||||
const MAX = 640;
|
||||
const w = canvas.width, h = canvas.height;
|
||||
if (w > MAX || h > MAX) {
|
||||
const ratio = Math.min(MAX / w, MAX / h);
|
||||
out.width = Math.round(w * ratio);
|
||||
out.height = Math.round(h * ratio);
|
||||
} else {
|
||||
out.width = w || 320;
|
||||
out.height = h || 240;
|
||||
}
|
||||
const ctx = out.getContext('2d');
|
||||
ctx.drawImage(canvas, 0, 0, out.width, out.height);
|
||||
out.toBlob((blob) => {
|
||||
if (!blob) { snapBtn.textContent = '❌ Capture failed'; return; }
|
||||
const snapFile = new File([blob], 'ruffle-snapshot.jpg', { type: 'image/jpeg' });
|
||||
const dt = new DataTransfer();
|
||||
dt.items.add(snapFile);
|
||||
if (thumbInput) {
|
||||
thumbInput.files = dt.files;
|
||||
thumbInput.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
// Replace snapshot preview in its grid row (between player and button)
|
||||
const existingPrev = mediaElem.querySelector('.ruffle-snapshot-preview');
|
||||
if (existingPrev) existingPrev.remove();
|
||||
const prevImg = document.createElement('img');
|
||||
prevImg.src = URL.createObjectURL(blob);
|
||||
prevImg.className = 'ruffle-snapshot-preview';
|
||||
mediaElem.insertBefore(prevImg, snapBtn);
|
||||
}, 'image/jpeg', 0.92);
|
||||
} else {
|
||||
snapBtn.textContent = 'Capture Thumbnail';
|
||||
}
|
||||
} catch(err) {
|
||||
console.warn('[Ruffle snapshot]', err);
|
||||
snapBtn.textContent = 'Capture Thumbnail';
|
||||
}
|
||||
};
|
||||
};
|
||||
// Wire after a short delay (Ruffle may not be fully ready)
|
||||
setTimeout(wireSnapshot, 200);
|
||||
} catch(err) {
|
||||
console.warn('[Ruffle upload preview]', err);
|
||||
mediaElem.innerHTML = `<div class="swf-upload-placeholder"><span class="swf-upload-placeholder-icon">⚡</span><span class="swf-upload-placeholder-text">Flash preview error</span></div>`;
|
||||
}
|
||||
});
|
||||
});
|
||||
} else if (file.type === 'application/pdf' || fileExt === 'pdf') {
|
||||
mediaElem = document.createElement('div');
|
||||
mediaElem.className = 'generic-file-icon pdf-preview-icon';
|
||||
@@ -1281,6 +1434,12 @@ window.initUploadForm = (selector) => {
|
||||
const idx = selectedFiles.indexOf(item);
|
||||
if (idx !== -1) selectedFiles.splice(idx, 1);
|
||||
item._rendered = false;
|
||||
// Clean up Ruffle player and blob URL if this was a SWF preview
|
||||
const swfPreview = previewItem.querySelector('.swf-upload-preview');
|
||||
if (swfPreview) {
|
||||
if (swfPreview._rufflePlayer) { try { swfPreview._rufflePlayer.pause(); swfPreview._rufflePlayer.remove(); } catch {} swfPreview._rufflePlayer = null; }
|
||||
if (swfPreview._swfObjectUrl) { URL.revokeObjectURL(swfPreview._swfObjectUrl); swfPreview._swfObjectUrl = null; }
|
||||
}
|
||||
previewItem.remove();
|
||||
|
||||
if (selectedFiles.length === 0) {
|
||||
@@ -1393,13 +1552,9 @@ window.initUploadForm = (selector) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle custom thumbnail for single SWF batch
|
||||
// Hide thumbSection for SWF (snapshot button now lives inside each file-preview-item)
|
||||
if (thumbSection) {
|
||||
const firstItem = selectedFiles[0];
|
||||
const firstFile = (firstItem && firstItem.file) || firstItem;
|
||||
const isSingleSwf = firstFile && selectedFiles.length === 1 &&
|
||||
(firstFile.type === 'application/x-shockwave-flash' || firstFile.type === 'application/vnd.adobe.flash.movie' || (firstFile.name && firstFile.name.endsWith('.swf')));
|
||||
thumbSection.style.display = isSingleSwf ? 'block' : 'none';
|
||||
thumbSection.style.display = 'none';
|
||||
}
|
||||
|
||||
updateSubmitButton();
|
||||
@@ -1447,6 +1602,11 @@ window.initUploadForm = (selector) => {
|
||||
removeFile.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// Clean up any Ruffle preview players and blob URLs
|
||||
filePreview?.querySelectorAll('.swf-upload-preview').forEach(el => {
|
||||
if (el._rufflePlayer) { try { el._rufflePlayer.pause(); el._rufflePlayer.remove(); } catch {} el._rufflePlayer = null; }
|
||||
if (el._swfObjectUrl) { URL.revokeObjectURL(el._swfObjectUrl); el._swfObjectUrl = null; }
|
||||
});
|
||||
selectedFiles = [];
|
||||
form.querySelector('.gps-privacy-warning')?.remove();
|
||||
if (fileInput) fileInput.value = '';
|
||||
@@ -1456,7 +1616,11 @@ window.initUploadForm = (selector) => {
|
||||
const media = filePreview?.querySelector('.preview-media');
|
||||
if (media) media.remove();
|
||||
|
||||
if (thumbSection) thumbSection.style.display = 'none';
|
||||
if (thumbSection) {
|
||||
thumbSection.style.display = 'none';
|
||||
thumbSection.querySelector('.btn-ruffle-snapshot')?.remove();
|
||||
thumbSection.querySelector('.ruffle-snapshot-preview')?.remove();
|
||||
}
|
||||
if (thumbInput) thumbInput.value = '';
|
||||
|
||||
updateSubmitButton();
|
||||
@@ -2108,8 +2272,19 @@ window.initUploadForm = (selector) => {
|
||||
localStorage.setItem('bustedThumbs', JSON.stringify(busted));
|
||||
} catch(e) {}
|
||||
}
|
||||
} else if (!isShitpost) {
|
||||
throw new Error(res.msg || 'Upload failed');
|
||||
} else {
|
||||
// Server returned an error — always surface it visibly
|
||||
const errMsg = res.msg || 'Upload failed';
|
||||
if (isShitpost) {
|
||||
// In shitpost mode there's no persistent statusDiv — use flash
|
||||
if (typeof window.flashMessage === 'function') {
|
||||
window.flashMessage(`✕ ${errMsg}`, 5000, 'error');
|
||||
} else if (typeof window.showFlash === 'function') {
|
||||
window.showFlash(errMsg, 'error');
|
||||
}
|
||||
} else {
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[UPLOAD ERROR]', err);
|
||||
@@ -2119,6 +2294,13 @@ window.initUploadForm = (selector) => {
|
||||
if (progressContainer) progressContainer.style.display = 'none';
|
||||
restoreBtn();
|
||||
return;
|
||||
} else {
|
||||
// Shitpost mode: show via flash toast
|
||||
if (typeof window.flashMessage === 'function') {
|
||||
window.flashMessage(`✕ ${err.message}`, 5000, 'error');
|
||||
} else if (typeof window.showFlash === 'function') {
|
||||
window.showFlash(err.message, 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user