overhaul rethumbing flashs

This commit is contained in:
2026-06-08 16:37:15 +02:00
parent c615676465
commit 69e90f8d2d
6 changed files with 540 additions and 60 deletions

View File

@@ -10017,7 +10017,7 @@ body.modal-open {
.modal-actions {
display: flex;
justify-content: flex-end;
justify-content: center;
gap: 10px;
margin-top: 15px;
}

View File

@@ -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); }
}

View File

@@ -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;
}
}

View File

@@ -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');
}
}
}
}