This commit is contained in:
2026-09-13 20:15:48 +02:00
parent ffd32a6b63
commit 399f2be456
24 changed files with 2159 additions and 90 deletions
+526
View File
@@ -2725,14 +2725,526 @@ window.cancelAnimFrame = (function () {
}, delay);
};
const initAlbumGallery = () => {
const container = document.querySelector('.album-gallery-container');
if (!container) {
window._currentActiveAlbumGallery = null;
return;
}
if (container._f0ckAlbumInit) return;
container._f0ckAlbumInit = true;
let albumData = null;
const jsonScript = container.querySelector('.album-data-json, #album-data-json');
if (jsonScript && jsonScript.textContent) {
try {
albumData = JSON.parse(jsonScript.textContent);
} catch (e) {
console.warn('[ALBUM] Could not parse album script tag:', e);
}
}
if (!albumData) {
try {
let raw = container.getAttribute('data-album');
if (raw) {
if (raw.includes('"') || raw.includes('{') || raw.includes('&')) {
const txt = document.createElement('textarea');
txt.innerHTML = raw;
raw = txt.value;
if (raw.includes('"') || raw.includes('{')) {
txt.innerHTML = raw;
raw = txt.value;
}
}
albumData = JSON.parse(raw);
}
} catch (e) {
// Fallback to DOM elements below
}
}
// Fallback: build albumData directly from rendered thumbnail buttons
if (!Array.isArray(albumData) || albumData.length === 0) {
const thumbs = container.querySelectorAll('.album-thumb-item');
if (thumbs.length > 1) {
albumData = Array.from(thumbs).map((thumb, i) => {
const idx = parseInt(thumb.getAttribute('data-index'), 10);
const src = thumb.getAttribute('data-src') || thumb.querySelector('img')?.src || '';
const subSlug = thumb.getAttribute('data-subf0ck-slug') || thumb.getAttribute('data-subf0ck-id') || '';
return {
order_index: isNaN(idx) ? i : idx,
display_index: (isNaN(idx) ? i : idx) + 1,
src: src,
dest: src,
slug: subSlug,
subf0ck_id: subSlug,
id: subSlug
};
});
}
}
if (!Array.isArray(albumData) || albumData.length <= 1) return;
let currentIndex = 0;
const getHashSubf0ckId = () => {
return (window.location.hash || '').replace(/^#/, '').trim();
};
const initialHash = getHashSubf0ckId() || container.getAttribute('data-requested-subf0ck') || '';
if (initialHash) {
const foundIdx = albumData.findIndex(item =>
String(item.slug || '') === initialHash ||
String(item.subf0ck_id || '') === initialHash ||
String(item.id) === initialHash ||
String(item.order_index + 1) === initialHash
);
if (foundIdx !== -1) {
currentIndex = foundIdx;
}
}
const imgEl = container.querySelector('#f0ck-image');
const videoEl = container.querySelector('#f0ck-album-video');
const videoWrapper = container.querySelector('#f0ck-album-video-wrapper');
const audioWrapper = container.querySelector('#f0ck-album-audio-wrapper') || container.querySelector('#f0ck-album-audio-container');
const audioEl = container.querySelector('#f0ck-album-audio');
const linkEl = container.querySelector('#elfe, .album-stage-link');
const prevBtn = container.querySelector('.album-btn-prev');
const nextBtn = container.querySelector('.album-btn-next');
const currentIdxEl = container.querySelector('.album-current-idx');
const totalCountEl = container.querySelector('.album-total-count');
const thumbItems = container.querySelectorAll('.album-thumb-item');
const stripEl = container.querySelector('.album-thumbnails-strip');
if (totalCountEl) totalCountEl.textContent = albumData.length;
let albumVideoV0ck = null;
const initAlbumVideoV0ck = () => {
if (albumVideoV0ck) return albumVideoV0ck;
if (videoEl && typeof v0ck === 'function') {
try {
albumVideoV0ck = new v0ck(videoEl);
} catch (e) {
console.warn('[ALBUM] Could not init v0ck for album video:', e);
}
}
return albumVideoV0ck;
};
let albumAudioV0ck = null;
const initAlbumAudioV0ck = () => {
if (albumAudioV0ck) return albumAudioV0ck;
if (audioEl && typeof v0ck === 'function') {
try {
albumAudioV0ck = new v0ck(audioEl);
} catch (e) {
console.warn('[ALBUM] Could not init v0ck for album audio:', e);
}
}
return albumAudioV0ck;
};
const preloadedImages = new Set();
const preload = (idx) => {
if (idx < 0 || idx >= albumData.length) return;
const sub = albumData[idx];
if (!sub) return;
const src = sub.src || sub.dest;
const mime = (sub.mime || '').toLowerCase();
if (src && !mime.startsWith('video/') && !mime.startsWith('audio/') && !preloadedImages.has(src)) {
const i = new Image();
i.src = src;
preloadedImages.add(src);
}
};
preload(1);
if (albumData.length > 2) preload(albumData.length - 1);
const isBlurred = () => {
const mediaObj = container.closest('.media-object') || document.querySelector('.media-object');
if (mediaObj && localStorage.getItem('blurDetail') !== 'false') {
const mode = mediaObj.getAttribute('data-mode');
const blurNsfw = localStorage.getItem('blurNsfw') === 'true';
const blurNsfl = localStorage.getItem('blurNsfl') === 'true';
const blurSfw = localStorage.getItem('blurSfw') === 'true';
const blurUntagged = localStorage.getItem('blurUntagged') === 'true';
let shouldBlurThis = false;
if (mode === 'nsfw') shouldBlurThis = blurNsfw;
else if (mode === 'nsfl') shouldBlurThis = blurNsfl;
else if (mode === 'sfw') shouldBlurThis = blurSfw;
else if (mode === 'untagged') shouldBlurThis = blurUntagged;
if (shouldBlurThis && !mediaObj.classList.contains('revealed')) {
return true;
}
}
return false;
};
const isAutoplayAllowed = () => {
return !isBlurred() && window.f0ckSession?.disable_autoplay !== true;
};
const showImage = (index, direction = 'none', updateHash = true) => {
if (index < 0) index = albumData.length - 1;
if (index >= albumData.length) index = 0;
currentIndex = index;
const item = albumData[currentIndex];
if (!item) return;
const newSrc = item.src || item.dest;
const mime = (item.mime || '').toLowerCase();
// Update URL hash with subf0ck slug (or id fallback) so links are shareable directly to this subf0ck
const subKey = item.slug || item.subf0ck_id || item.id;
if (updateHash && subKey) {
history.replaceState(null, '', window.location.pathname + window.location.search + '#' + subKey);
}
// Pause any playing audio or video
if (videoEl && !videoEl.paused) {
try { videoEl.pause(); } catch {}
}
if (audioEl && !audioEl.paused) {
try { audioEl.pause(); } catch {}
}
if (mime.startsWith('video/')) {
container.classList.add('is-video-active');
container.classList.remove('is-audio-active');
if (imgEl) imgEl.style.display = 'none';
if (linkEl) linkEl.style.display = 'none';
if (audioWrapper) audioWrapper.style.display = 'none';
if (videoWrapper) videoWrapper.style.display = 'block';
if (videoEl) {
videoEl.style.display = 'block';
if (videoEl.src !== newSrc && !videoEl.src.endsWith(newSrc)) {
videoEl.src = newSrc;
videoEl.load();
}
initAlbumVideoV0ck();
video = videoEl;
if (item.size && videoWrapper) {
const dlBtn = videoWrapper.querySelector('#v0ck_download');
if (dlBtn) dlBtn.textContent = `Download (${item.size})`;
}
const playerWrap = videoWrapper.querySelector('.v0ck') || videoWrapper;
if (isAutoplayAllowed()) {
const playPromise = videoEl.play();
if (playPromise !== undefined) {
playPromise.catch(() => {
playerWrap.classList.add('v0ck_initial');
});
}
} else {
try { videoEl.pause(); } catch {}
playerWrap.classList.add('v0ck_initial');
}
}
} else if (mime.startsWith('audio/')) {
container.classList.remove('is-video-active');
container.classList.add('is-audio-active');
if (videoEl) {
try { videoEl.pause(); } catch {}
}
if (videoWrapper) videoWrapper.style.display = 'none';
if (imgEl) imgEl.style.display = 'none';
if (linkEl) linkEl.style.display = 'none';
if (audioWrapper) {
audioWrapper.style.display = 'block';
const coverUrl = item.coverart || item.thumb || '/s/img/audio.webp';
audioWrapper.style.backgroundImage = `url('${coverUrl}')`;
audioWrapper.style.backgroundRepeat = 'no-repeat';
audioWrapper.style.backgroundPosition = 'center';
audioWrapper.style.backgroundSize = 'contain';
audioWrapper.style.backgroundColor = 'black';
}
if (audioEl) {
audioEl.style.display = 'block';
if (audioEl.src !== newSrc && !audioEl.src.endsWith(newSrc)) {
audioEl.src = newSrc;
audioEl.load();
}
initAlbumAudioV0ck();
video = audioEl;
if (item.size && audioWrapper) {
const dlBtn = audioWrapper.querySelector('#v0ck_download');
if (dlBtn) dlBtn.textContent = `Download (${item.size})`;
}
const playerWrap = audioWrapper.querySelector('.v0ck') || audioWrapper;
if (isAutoplayAllowed()) {
const playPromise = audioEl.play();
if (playPromise !== undefined) {
playPromise.catch(() => {
playerWrap.classList.add('v0ck_initial');
});
}
} else {
try { audioEl.pause(); } catch {}
playerWrap.classList.add('v0ck_initial');
}
}
} else {
// Image
container.classList.remove('is-video-active', 'is-audio-active');
if (videoEl) {
try { videoEl.pause(); } catch {}
}
if (audioEl) {
try { audioEl.pause(); } catch {}
}
if (videoWrapper) videoWrapper.style.display = 'none';
if (audioWrapper) audioWrapper.style.display = 'none';
if (linkEl) {
linkEl.style.display = '';
linkEl.href = newSrc;
}
if (imgEl) {
imgEl.style.display = 'block';
imgEl.classList.remove('album-img-fade');
void imgEl.offsetWidth; // trigger reflow
imgEl.src = newSrc;
imgEl.classList.add('album-img-fade');
}
video = null;
}
if (linkEl && !mime.startsWith('video/')) {
linkEl.href = newSrc;
}
if (currentIdxEl) {
currentIdxEl.textContent = currentIndex + 1;
}
thumbItems.forEach((btn, idx) => {
const isActive = idx === currentIndex;
btn.classList.toggle('active', isActive);
if (isActive) {
btn.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
}
});
updateInfoModal();
preload(currentIndex + 1);
preload(currentIndex - 1);
};
const updateInfoModal = () => {
const modal = document.getElementById('info-modal');
if (!modal) return;
const sub = albumData[currentIndex];
if (!sub) return;
const subHeading = modal.querySelector('#info-modal-subheading');
if (subHeading) {
const parentId = subHeading.getAttribute('data-item-id') || container.getAttribute('data-album-id') || '';
const parentSlug = subHeading.getAttribute('data-item-slug') || '';
const subSlug = sub.slug || sub.subf0ck_id || sub.id;
const total = albumData.length;
const idx = currentIndex + 1;
subHeading.innerHTML = `Post ID: <code>${parentId}</code>${parentSlug ? ` <span class="text-muted">(${parentSlug})</span>` : ''} &bull; Subf0ck: <code>${subSlug}</code> <span class="text-muted">(${idx}/${total})</span>`;
}
const specsTitle = modal.querySelector('#info-specs-header-title');
if (specsTitle) {
specsTitle.textContent = `Technical Specifications (Subf0ck ${currentIndex + 1}/${albumData.length})`;
}
const fileSizeEl = modal.querySelector('#info-file-size');
if (fileSizeEl) {
fileSizeEl.textContent = sub.size || '';
}
const dimsCard = modal.querySelector('#info-card-dimensions');
const dimsEl = modal.querySelector('#info-file-dimensions');
if (dimsCard && dimsEl) {
if (sub.width && sub.height) {
dimsEl.textContent = `${sub.width} × ${sub.height} px`;
dimsCard.style.display = '';
} else {
dimsCard.style.display = 'none';
}
}
const mimeEl = modal.querySelector('#info-file-mime');
if (mimeEl) {
mimeEl.textContent = sub.mime || '';
}
const directLink = modal.querySelector('#info-file-direct-link');
if (directLink) {
directLink.href = sub.dest || sub.src || '#';
}
const sourceCard = modal.querySelector('#info-card-source');
if (sourceCard) {
sourceCard.style.display = 'none';
}
const hashCard = modal.querySelector('#info-card-hash');
const hashEl = modal.querySelector('#info-file-hash');
const copyHashBtn = modal.querySelector('#info-copy-hash-btn');
if (hashCard) {
const cleanHash = sub.checksum ? String(sub.checksum).split('_bypass_')[0] : '';
if (cleanHash) {
if (hashEl) hashEl.textContent = cleanHash;
if (copyHashBtn) copyHashBtn.setAttribute('data-hash', cleanHash);
hashCard.style.display = '';
} else {
hashCard.style.display = 'none';
}
}
};
if (imgEl) {
imgEl.addEventListener('load', () => {
const sub = albumData[currentIndex];
if (sub && (!sub.width || !sub.height) && imgEl.naturalWidth) {
sub.width = imgEl.naturalWidth;
sub.height = imgEl.naturalHeight;
updateInfoModal();
}
});
}
if (videoEl) {
videoEl.addEventListener('loadedmetadata', () => {
const sub = albumData[currentIndex];
if (sub && (!sub.width || !sub.height) && videoEl.videoWidth) {
sub.width = videoEl.videoWidth;
sub.height = videoEl.videoHeight;
updateInfoModal();
}
});
}
// Ensure proper initial media display (especially if initial item is video or subf0ck hash was requested)
showImage(currentIndex, 'none', !!initialHash);
if (prevBtn) {
prevBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
showImage(currentIndex - 1, 'prev');
});
}
if (nextBtn) {
nextBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
showImage(currentIndex + 1, 'next');
});
}
thumbItems.forEach((btn) => {
btn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const targetIdx = parseInt(btn.getAttribute('data-index'), 10);
if (!isNaN(targetIdx)) {
showImage(targetIdx);
}
});
});
// Touch tap on container reveals thumbnail strip briefly on touch devices
let stripTimeout = null;
container.addEventListener('click', (e) => {
if (stripEl && !e.target.closest('.album-thumbnails-strip, .v0ck_player_controls, .v0ck_settings_menu, .v0ck_hud, .album-btn')) {
stripEl.classList.add('is-visible');
clearTimeout(stripTimeout);
stripTimeout = setTimeout(() => {
stripEl.classList.remove('is-visible');
}, 3500);
}
});
// Hashchange listener for forward/backward browser navigation
const hashChangeHandler = () => {
const newHash = getHashSubf0ckId();
if (!newHash) return;
const targetIdx = albumData.findIndex(item =>
String(item.slug || '') === newHash ||
String(item.subf0ck_id || '') === newHash ||
String(item.id) === newHash ||
String(item.order_index + 1) === newHash
);
if (targetIdx !== -1 && targetIdx !== currentIndex) {
showImage(targetIdx, 'none', false);
}
};
window.addEventListener('hashchange', hashChangeHandler);
// Touch swipe support on album container
let touchStartX = 0;
let touchStartY = 0;
container.addEventListener('touchstart', (e) => {
if (e.target.closest('.v0ck_player_controls, .v0ck_settings_menu, input[type="range"]')) return;
if (e.touches && e.touches.length === 1) {
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
}
}, { passive: true });
container.addEventListener('touchend', (e) => {
if (e.target.closest('.v0ck_player_controls, .v0ck_settings_menu, input[type="range"]')) return;
if (e.changedTouches && e.changedTouches.length === 1) {
const diffX = e.changedTouches[0].clientX - touchStartX;
const diffY = e.changedTouches[0].clientY - touchStartY;
if (Math.abs(diffX) > 40 && Math.abs(diffX) > Math.abs(diffY) * 1.5) {
if (diffX > 0) {
showImage(currentIndex - 1, 'prev');
} else {
showImage(currentIndex + 1, 'next');
}
}
}
}, { passive: true });
window._currentActiveAlbumGallery = {
prev: () => showImage(currentIndex - 1, 'prev'),
next: () => showImage(currentIndex + 1, 'next'),
updateInfoModal: updateInfoModal,
getCurrentSubf0ck: () => albumData[currentIndex],
isHovered: false
};
container.addEventListener('mouseenter', () => {
if (window._currentActiveAlbumGallery) window._currentActiveAlbumGallery.isHovered = true;
});
container.addEventListener('mouseleave', () => {
if (window._currentActiveAlbumGallery) window._currentActiveAlbumGallery.isHovered = false;
});
};
const setupMedia = () => {
window._currentActiveAlbumGallery = null;
const elem = document.querySelector("#my-video") || document.querySelector("audio#my-video");
if (elem) {
video = new v0ck(elem);
} else {
video = null;
}
initAlbumGallery();
};
document.addEventListener('f0ck:contentLoaded', initAlbumGallery);
const initOnaraInitialState = () => {
if (!isOnaraActive()) return;
@@ -6219,6 +6731,15 @@ window.cancelAnimFrame = (function () {
// <keybindings>
const clickOnElementBinding = selector => () => (elem = document.querySelector(selector)) ? elem.click() : null;
const clickOnNavBinding = (directionOrSelector) => () => {
if (window._currentActiveAlbumGallery && window._currentActiveAlbumGallery.isHovered) {
if (directionOrSelector === 'prev' || directionOrSelector === '#prev') {
window._currentActiveAlbumGallery.prev();
return;
} else if (directionOrSelector === 'next' || directionOrSelector === '#next') {
window._currentActiveAlbumGallery.next();
return;
}
}
let el;
if (directionOrSelector === 'prev' || directionOrSelector === '#prev') {
el = document.querySelector(".steuerung .nav-prev:not([href='#']), .nav-prev:not([href='#']), #prev:not([href='#'])") || document.querySelector('.nav-prev') || document.getElementById('prev');
@@ -6252,6 +6773,8 @@ window.cancelAnimFrame = (function () {
"7": () => seekToPercentage(0.7),
"8": () => seekToPercentage(0.8),
"9": () => seekToPercentage(0.9),
"[": () => window._currentActiveAlbumGallery?.prev(),
"]": () => window._currentActiveAlbumGallery?.next(),
"ArrowLeft": clickOnNavBinding("prev"),
"a": clickOnNavBinding("prev"),
"ArrowRight": clickOnNavBinding("next"),
@@ -14018,6 +14541,9 @@ document.addEventListener('click', (e) => {
const infoBtn = e.target.closest('#a_info');
if (infoBtn) {
e.preventDefault();
if (window._currentActiveAlbumGallery && typeof window._currentActiveAlbumGallery.updateInfoModal === 'function') {
window._currentActiveAlbumGallery.updateInfoModal();
}
const modal = document.getElementById('info-modal');
if (modal) {
modal.style.display = 'flex';
+448 -30
View File
@@ -500,6 +500,34 @@ window.initUploadForm = (selector) => {
let autoTags = []; // Track tags suggested from metadata
let selectedFiles = []; // Array of files for shitpost_mode
let activeMode = 'file'; // 'file' or 'url'
// Album mode state and helpers
const albumChoiceContainer = form.querySelector('#album-choice-container');
let albumChoiceMode = 'album'; // 'album' | 'batch'
const isAlbumModeActive = () => {
if (activeMode === 'album') {
return selectedFiles.length > 0;
}
return activeMode === 'file' && selectedFiles.length > 1 && (albumChoiceMode === 'album' || !isShitpost);
};
if (albumChoiceContainer) {
const btns = albumChoiceContainer.querySelectorAll('.album-choice-btn');
btns.forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
const choice = btn.getAttribute('data-choice');
if (choice === albumChoiceMode) return;
albumChoiceMode = choice;
btns.forEach(b => b.classList.toggle('active', b === btn));
selectedFiles.forEach(item => { delete item._rendered; });
if (filePreview) filePreview.innerHTML = '';
handleFile();
updateSubmitButton();
});
});
}
// Shared emoji cache for per-item pickers (fetched once, reused by all items)
let _emojiCache = null;
let _emojiCachePromise = null;
@@ -649,14 +677,47 @@ window.initUploadForm = (selector) => {
tab.addEventListener('click', () => {
const mode = tab.dataset.mode;
if (mode === activeMode) return;
const prevMode = activeMode;
activeMode = mode;
modeTabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
if (modeFile) modeFile.style.display = mode === 'file' ? '' : 'none';
if (modeFile) modeFile.style.display = (mode === 'file' || mode === 'album') ? '' : 'none';
if (modeUrl) modeUrl.style.display = mode === 'url' ? '' : 'none';
if (mode === 'album') {
albumChoiceMode = 'album';
form.classList.add('album-mode-active');
if (albumChoiceContainer) albumChoiceContainer.style.display = 'none';
if (fileInput) {
try {
const mimesObj = JSON.parse(form.getAttribute('data-mimes') || '{}');
fileInput.accept = Object.keys(mimesObj).join(',');
} catch {}
}
if (selectedFiles.length > 0) {
renderAlbumStaging();
}
} else if (mode === 'file') {
if (fileInput) {
try {
const mimesObj = JSON.parse(form.getAttribute('data-mimes') || '{}');
fileInput.accept = Object.keys(mimesObj).join(',');
} catch {}
}
if (!isAlbumModeActive()) {
form.classList.remove('album-mode-active');
if (prevMode === 'album' && isShitpost && selectedFiles.length > 0) {
selectedFiles.forEach(item => { delete item._rendered; });
if (filePreview) filePreview.innerHTML = '';
handleFile();
}
}
} else {
form.classList.remove('album-mode-active');
}
// Reset status
if (statusDiv) {
statusDiv.textContent = '';
@@ -1013,11 +1074,17 @@ window.initUploadForm = (selector) => {
}
const isShitpost = !!window.f0ckShitpostMode;
const isAlbum = isAlbumModeActive();
const isAlbumTab = activeMode === 'album';
const isAlbumActive = isAlbum || isAlbumTab;
form.classList.toggle('album-mode-active', isAlbumActive);
const rating = form.querySelector('input[name="rating"]:checked');
// In Shitpost Mode, ratings are per-item. If require rating is true, every item must be rated.
// In Shitpost Mode, ratings are per-item unless album mode is active
let hasRating = true;
if (isShitpost && activeMode === 'file') {
if (isShitpost && !isAlbumActive && activeMode === 'file') {
if (shitpostRequireRating) {
hasRating = selectedFiles.length > 0 && selectedFiles.every(item => ['sfw', 'nsfw', 'nsfl'].includes(item.rating));
}
@@ -1026,7 +1093,7 @@ window.initUploadForm = (selector) => {
}
let hasTags = true;
if (!isShitpost) {
if (!isShitpost || isAlbumActive) {
hasTags = tags.length >= minTags;
} else if (shitpostMinTags > 0 && activeMode === 'file') {
// In shitpost file mode with min-tags enforced: every queued item must meet the threshold.
@@ -1038,17 +1105,21 @@ window.initUploadForm = (selector) => {
const commentSec = form.querySelector('.global-comment-section');
const tagsSec = form.querySelector('.global-tag-section');
const ocSec = form.querySelector('.global-oc-section');
const titleSec = form.querySelector('.global-title-section');
const formActions = form.querySelector('.form-actions');
if (isShitpost) {
if (formActions) {
formActions.style.display = activeMode === 'url' ? 'none' : 'block';
formActions.style.display = (activeMode === 'url' && !isAlbumActive) ? 'none' : 'block';
}
const hide = activeMode === 'file';
const hide = activeMode === 'file' && !isAlbumActive;
const disp = hide ? 'none' : 'block';
if (ratingSec) {
ratingSec.style.display = disp;
ratingSec.querySelectorAll('input').forEach(i => i.disabled = hide);
ratingSec.querySelectorAll('input').forEach(i => {
i.disabled = hide;
i.required = !hide;
});
}
if (commentSec) {
commentSec.style.display = disp;
@@ -1059,13 +1130,19 @@ window.initUploadForm = (selector) => {
tagsSec.querySelectorAll('input').forEach(i => i.disabled = hide);
}
if (ocSec) {
ocSec.style.display = 'none';
ocSec.querySelectorAll('input').forEach(i => i.disabled = true);
ocSec.style.display = isAlbumActive ? 'block' : 'none';
ocSec.querySelectorAll('input').forEach(i => i.disabled = !isAlbumActive);
}
if (titleSec) {
titleSec.style.display = isAlbumActive ? 'block' : 'none';
titleSec.querySelectorAll('input').forEach(i => i.disabled = !isAlbumActive);
}
}
let hasContent = false;
if (activeMode === 'file') {
if (activeMode === 'album') {
hasContent = selectedFiles.length >= 2;
} else if (activeMode === 'file') {
hasContent = selectedFiles.length > 0;
} else {
hasContent = urlInput && urlInput.value.trim().length > 0;
@@ -1077,13 +1154,19 @@ window.initUploadForm = (selector) => {
const btnText = submitBtn.querySelector('.btn-text');
if (btnText) {
const i18n = window.f0ckI18n || {};
if (!hasContent) {
if (activeMode === 'album' && selectedFiles.length === 0) {
btnText.textContent = i18n.album_select_pictures || 'Select files for album';
submitBtn.disabled = true;
} else if (activeMode === 'album' && selectedFiles.length === 1) {
btnText.textContent = i18n.album_min_pictures || 'Add at least 2 items for an album';
submitBtn.disabled = true;
} else if (!hasContent) {
btnText.textContent = activeMode === 'file'
? (ssrSelectFileText || i18n.select_file || 'Select a file')
: (i18n.enter_url || 'Enter a URL');
} else if (!hasTags) {
// non-shitpost or shitpost with min-tags
if (isShitpost && shitpostMinTags > 0) {
if (isShitpost && !isAlbumActive && shitpostMinTags > 0) {
const remaining = shitpostMinTags - Math.min(...selectedFiles.map(item => (item.tags || []).length));
btnText.textContent = `${remaining} more tag${remaining !== 1 ? 's' : ''} required per item`;
} else {
@@ -1095,7 +1178,7 @@ window.initUploadForm = (selector) => {
}
} else if (!hasRating) {
const nsflEnabled = !!form.querySelector('input[name="rating"][value="nsfl"]');
if (isShitpost && shitpostRequireRating) {
if (isShitpost && !isAlbumActive && shitpostRequireRating) {
btnText.textContent = 'Select a rating for each item';
} else {
if (nsflEnabled) {
@@ -1105,7 +1188,10 @@ window.initUploadForm = (selector) => {
}
}
} else {
if (activeMode === 'url' && urlInput && ytRegex.test(urlInput.value.trim()) && window.f0ckEnableYoutubeUpload !== false) {
if (isAlbumActive) {
const tpl = i18n.upload_album || 'Upload Album (%s subf0cks)';
btnText.textContent = tpl.replace('%s', selectedFiles.length);
} else if (activeMode === 'url' && urlInput && ytRegex.test(urlInput.value.trim()) && window.f0ckEnableYoutubeUpload !== false) {
btnText.textContent = i18n.embed_youtube || 'Embed YouTube Video';
} else if (activeMode === 'url') {
btnText.textContent = i18n.upload_from_url || 'Upload from URL';
@@ -1126,15 +1212,194 @@ window.initUploadForm = (selector) => {
}
};
const renderAlbumStaging = () => {
if (!filePreview) return;
filePreview.style.display = 'block';
filePreview.innerHTML = '';
const stagingCont = document.createElement('div');
stagingCont.className = 'album-staging-container';
const stagingHeader = document.createElement('div');
stagingHeader.className = 'album-staging-header';
stagingHeader.innerHTML = `
<div class="album-staging-title">
<i class="fa-solid fa-layer-group"></i>
<span>${(window.f0ckI18n && window.f0ckI18n.album_title) || 'Album'} (${selectedFiles.length} subf0cks)</span>
</div>
<button type="button" class="btn-add-album-pics">
<i class="fa-solid fa-plus"></i> ${(window.f0ckI18n && window.f0ckI18n.album_add_more) || 'Add subf0cks'}
</button>
`;
const addBtn = stagingHeader.querySelector('.btn-add-album-pics');
if (addBtn) {
addBtn.addEventListener('click', (e) => {
e.preventDefault();
if (fileInput) fileInput.click();
});
}
stagingCont.appendChild(stagingHeader);
const grid = document.createElement('div');
grid.className = 'album-staging-grid';
selectedFiles.forEach((item, index) => {
const file = item.file || item;
const card = document.createElement('div');
card.className = 'album-stage-card' + (index === 0 ? ' is-cover' : '');
const badge = document.createElement('div');
badge.className = 'album-stage-badge';
badge.innerHTML = index === 0 ? '<i class="fa-solid fa-star"></i> Cover' : `#${index + 1}`;
card.appendChild(badge);
const isVideo = (file.type && file.type.startsWith('video/')) || /\.(mp4|webm|mov|mkv)$/i.test(file.name || '');
const isAudio = (file.type && file.type.startsWith('audio/')) || /\.(mp3|ogg|wav|flac|m4a|aac)$/i.test(file.name || '');
if (isVideo) {
const video = document.createElement('video');
video.src = URL.createObjectURL(file);
video.muted = true;
video.playsInline = true;
video.autoplay = false;
video.preload = 'metadata';
card.appendChild(video);
const mimeBadge = document.createElement('span');
mimeBadge.className = 'album-stage-mime-badge';
mimeBadge.innerHTML = '<i class="fa-solid fa-play"></i>';
card.appendChild(mimeBadge);
} else if (isAudio) {
const audioPreview = document.createElement('div');
audioPreview.className = 'album-stage-audio-preview';
audioPreview.innerHTML = `
<i class="fa-solid fa-music"></i>
<span class="album-stage-audio-name" title="${file.name || 'Audio'}">${file.name || 'Audio'}</span>
`;
card.appendChild(audioPreview);
const mimeBadge = document.createElement('span');
mimeBadge.className = 'album-stage-mime-badge';
mimeBadge.innerHTML = '<i class="fa-solid fa-music"></i>';
card.appendChild(mimeBadge);
} else {
const img = document.createElement('img');
img.src = URL.createObjectURL(file);
img.alt = file.name || `Subf0ck ${index + 1}`;
card.appendChild(img);
}
const actions = document.createElement('div');
actions.className = 'album-stage-actions';
// Move Left
const btnLeft = document.createElement('button');
btnLeft.type = 'button';
btnLeft.className = 'album-action-btn btn-album-move-left';
btnLeft.title = window.f0ckI18n?.album_move_left || 'Move left';
btnLeft.innerHTML = '<i class="fa-solid fa-arrow-left"></i>';
if (index === 0) {
btnLeft.disabled = true;
btnLeft.style.opacity = '0.3';
btnLeft.style.cursor = 'not-allowed';
} else {
btnLeft.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const temp = selectedFiles[index];
selectedFiles[index] = selectedFiles[index - 1];
selectedFiles[index - 1] = temp;
renderAlbumStaging();
updateSubmitButton();
});
}
actions.appendChild(btnLeft);
// Move Right
const btnRight = document.createElement('button');
btnRight.type = 'button';
btnRight.className = 'album-action-btn btn-album-move-right';
btnRight.title = window.f0ckI18n?.album_move_right || 'Move right';
btnRight.innerHTML = '<i class="fa-solid fa-arrow-right"></i>';
if (index === selectedFiles.length - 1) {
btnRight.disabled = true;
btnRight.style.opacity = '0.3';
btnRight.style.cursor = 'not-allowed';
} else {
btnRight.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const temp = selectedFiles[index];
selectedFiles[index] = selectedFiles[index + 1];
selectedFiles[index + 1] = temp;
renderAlbumStaging();
updateSubmitButton();
});
}
actions.appendChild(btnRight);
// Remove
const btnRemove = document.createElement('button');
btnRemove.type = 'button';
btnRemove.className = 'album-action-btn btn-album-remove';
btnRemove.title = window.f0ckI18n?.album_remove || 'Remove subf0ck';
btnRemove.innerHTML = '<i class="fa-solid fa-xmark"></i>';
btnRemove.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
selectedFiles.splice(index, 1);
if (selectedFiles.length === 0) {
if (form._f0ckUploader && typeof form._f0ckUploader.reset === 'function') {
form._f0ckUploader.reset();
}
} else {
handleFile();
updateSubmitButton();
}
});
actions.appendChild(btnRemove);
card.appendChild(actions);
const caption = document.createElement('div');
caption.className = 'album-stage-caption';
caption.title = file.name;
caption.textContent = `${file.name} (${formatSize(file.size)})`;
card.appendChild(caption);
grid.appendChild(card);
});
// Add More Card in Grid if under 100 subf0cks
const maxAlbumItems = 100;
if (selectedFiles.length < maxAlbumItems) {
const addMoreCard = document.createElement('div');
addMoreCard.className = 'album-stage-card album-stage-add-more';
addMoreCard.innerHTML = `
<div class="album-add-icon"><i class="fa-solid fa-plus"></i></div>
<div class="album-add-text">${(window.f0ckI18n && window.f0ckI18n.album_add_more) || 'Add subf0cks'}</div>
<div class="album-add-sub">(${selectedFiles.length}/${maxAlbumItems})</div>
`;
addMoreCard.addEventListener('click', (e) => {
e.preventDefault();
if (fileInput) fileInput.click();
});
grid.appendChild(addMoreCard);
}
stagingCont.appendChild(grid);
filePreview.appendChild(stagingCont);
};
const handleFile = (files) => {
const isShitpost = !!window.f0ckShitpostMode;
// If files were provided, process them (append or replace)
if (files && files.length > 0) {
const filesToProcess = isShitpost ? Array.from(files) : [files[0]];
if (!isShitpost) {
selectedFiles = []; // Reset for normal mode — replace, not append
// Also wipe the preview DOM so the old card doesn't linger
const isMultiAllowed = isShitpost || activeMode === 'album' || files.length > 1 || selectedFiles.length > 0;
const filesToProcess = isMultiAllowed ? Array.from(files) : [files[0]];
if (!isMultiAllowed && selectedFiles.length === 0) {
selectedFiles = []; // Reset for normal mode single non-image file
if (filePreview) filePreview.innerHTML = '';
}
@@ -1219,18 +1484,32 @@ window.initUploadForm = (selector) => {
}
}
if (activeMode === 'album' && selectedFiles.length >= 100) {
const errorMsg = 'Album limit reached (maximum 100 subf0cks).';
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'; }
break;
}
if (!selectedFiles.some(f => (f.file || f).name === file.name && (f.file || f).size === file.size)) {
if (isShitpost) {
selectedFiles.push({ type: 'file', file: file, rating: '', visibility: '', tags: [], comment: '', title: '', is_oc: false });
} else {
selectedFiles.push(file); // Legacy single file mode uses raw File
}
selectedFiles.push({ type: 'file', file: file, rating: '', visibility: '', tags: [], comment: '', title: '', is_oc: false });
}
}
}
// Toggle album choice container
if (activeMode === 'album') {
albumChoiceMode = 'album';
}
const isAlbumCandidate = activeMode === 'file' && selectedFiles.length > 1;
if (albumChoiceContainer) {
albumChoiceContainer.style.display = isAlbumCandidate ? 'flex' : 'none';
}
// Rebuild UI state
if (selectedFiles.length === 0) {
if (albumChoiceContainer) albumChoiceContainer.style.display = 'none';
if (filePreview) {
filePreview.style.display = 'none';
filePreview.innerHTML = '';
@@ -1257,14 +1536,23 @@ window.initUploadForm = (selector) => {
statusDiv.className = 'upload-status';
}
// Force 'file' mode tab UI
if (activeMode !== 'file' && modeTabs.length > 0) {
// Force 'file' or 'album' mode tab UI if coming from URL mode
if (activeMode !== 'file' && activeMode !== 'album' && modeTabs.length > 0) {
modeTabs.forEach(t => t.classList.remove('active'));
const fileTab = form.querySelector('.upload-mode-tab[data-mode="file"]');
if (fileTab) fileTab.classList.add('active');
const targetMode = isAlbumModeActive() ? 'album' : 'file';
const targetTab = form.querySelector(`.upload-mode-tab[data-mode="${targetMode}"]`);
if (targetTab) targetTab.classList.add('active');
if (modeFile) modeFile.style.display = '';
if (modeUrl) modeUrl.style.display = 'none';
activeMode = 'file';
activeMode = targetMode;
}
// If Album Mode is active, render Album Staging
if (isAlbumModeActive()) {
renderAlbumStaging();
updateSubmitButton();
form.dispatchEvent(new CustomEvent('fileReady', { detail: { files: selectedFiles } }));
return true;
}
let lastNewPreviewItem = null;
@@ -1962,7 +2250,7 @@ window.initUploadForm = (selector) => {
// Legacy Global Meta Sync (Non-Shitpost Mode)
if (!isShitpost && selectedFiles.length > 0 && files && files.length > 0) {
const primaryFile = selectedFiles[0];
const primaryFile = selectedFiles[0].file || selectedFiles[0];
autoTags = [];
const metaCont = form.querySelector('.meta-suggestions-container');
const metaList = form.querySelector('.meta-suggestions-list');
@@ -2084,6 +2372,7 @@ window.initUploadForm = (selector) => {
if (el._swfObjectUrl) { URL.revokeObjectURL(el._swfObjectUrl); el._swfObjectUrl = null; }
});
selectedFiles = [];
if (albumChoiceContainer) albumChoiceContainer.style.display = 'none';
form.querySelector('.gps-privacy-warning')?.remove();
if (fileInput) fileInput.value = '';
if (dropZonePrompt) dropZonePrompt.style.display = 'block';
@@ -2475,17 +2764,22 @@ window.initUploadForm = (selector) => {
}
const isFileMode = activeMode === 'file';
const isAlbum = isAlbumModeActive() || activeMode === 'album';
const globalRatingEl = form.querySelector('input[name="rating"]:checked');
// Validation
if (isShitpost && isFileMode) {
if (isShitpost && isFileMode && !isAlbum) {
if (selectedFiles.length === 0) {
if (window.showFlash) window.showFlash('No files selected', 'error');
return;
}
// No tag or rating requirement in shitpost mode — untagged items are allowed
} else {
if (isAlbum && selectedFiles.length < 2) {
if (window.showFlash) window.showFlash('Add at least 2 pictures for an album', 'error');
return;
}
if (!globalRatingEl) {
if (window.showFlash) window.showFlash('Please select a rating', 'error');
return;
@@ -2661,6 +2955,122 @@ window.initUploadForm = (selector) => {
// --- File Upload ---
if (selectedFiles.length === 0) return;
const isAlbum = isAlbumModeActive() || activeMode === 'album';
if (isAlbum) {
const statusMsg = window.f0ckI18n?.uploading_album || `Uploading album (${selectedFiles.length} pictures)...`;
setBtnLoading(statusMsg);
if (progressContainer) progressContainer.style.display = 'flex';
if (statusDiv) {
statusDiv.textContent = '';
statusDiv.className = 'upload-status';
}
const globalRatingEl = form.querySelector('input[name="rating"]:checked');
const globalVisEl = form.querySelector('input[name="visibility"]:checked');
const globalExpiryEl = form.querySelector('select[name="expiry"], input[name="expiry"]');
const formData = new FormData();
formData.append('is_album', 'true');
formData.append('rating', globalRatingEl ? globalRatingEl.value : 'sfw');
formData.append('visibility', globalVisEl ? globalVisEl.value : '0');
formData.append('expiry', globalExpiryEl ? globalExpiryEl.value : 'permanent');
formData.append('tags', tags.join(','));
formData.append('is_oc', isOc ? 'true' : 'false');
if (titleVal) formData.append('title', titleVal);
if (comment) formData.append('comment', comment);
for (let i = 0; i < selectedFiles.length; i++) {
const f = selectedFiles[i].file || selectedFiles[i];
formData.append('files', f);
}
try {
const res = await new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100);
if (progressFill) progressFill.style.width = percent + '%';
if (progressText) progressText.textContent = percent + '%';
}
});
xhr.onload = () => {
try {
const data = JSON.parse(xhr.responseText);
resolve(data);
} catch(e) {
let msg = 'Server error';
if (xhr.status === 413) msg = 'File too large';
try {
const errData = JSON.parse(xhr.responseText);
if (errData.msg) msg = errData.msg;
} catch(e2) {}
reject(new Error(msg));
}
};
xhr.onerror = () => reject(new Error('Connection error'));
xhr.open('POST', '/api/v2/upload');
const csrf = window.f0ckSession?.csrf_token || document.querySelector('input[name="csrf_token"]')?.value || '';
xhr.setRequestHeader('X-CSRF-Token', csrf);
xhr.send(formData);
});
if (res.success) {
if (dragModal) dragModal.classList.remove('show');
const dropModal = document.getElementById('upload-drag-modal');
if (dropModal) dropModal.classList.remove('show');
form._f0ckUploader.reset();
const successMsg = res.msg || `Album (${res.album_count || selectedFiles.length} subf0cks) uploaded successfully!`;
if (typeof window.flashMessage === 'function') {
window.flashMessage(successMsg, 3000, 'success');
} else if (!dragModal && statusDiv) {
statusDiv.innerHTML = '✓ ' + successMsg;
statusDiv.className = 'upload-status success';
}
if (res.itemid && window.NotificationSystemInstance && typeof window.NotificationSystemInstance.handleNewItem === 'function') {
window.NotificationSystemInstance.handleNewItem({
id: res.itemid,
dest: res.dest,
mime: res.mime,
username: res.username || window.f0ckSession?.user || '',
display_name: res.display_name || window.f0ckSession?.display_name || null,
tag_id: res.tag_id ?? 0,
is_oc: !!res.is_oc,
slug: res.slug,
visibility: res.visibility || 0
});
}
const targetUrl = res.slug ? `/${res.slug}` : (res.itemid ? `/${res.itemid}` : '/');
if (typeof window.loadPageAjax === 'function') {
window.loadPageAjax(targetUrl, true, { bypassCache: true });
} else {
window.location.href = targetUrl;
}
return;
} else {
const errMsg = res.msg || 'Upload failed';
const err = new Error(errMsg);
if (res.repost) err.repost = res.repost;
throw err;
}
} catch (err) {
console.error('[ALBUM UPLOAD ERROR]', err);
if (err.repost) {
statusDiv.innerHTML = '✕ ' + window.escapeHtmlUpload(err.message) + ` (<a href="/${err.repost}" class="repost-link">view existing</a>)`;
} else {
statusDiv.textContent = '✕ ' + err.message;
}
statusDiv.className = 'upload-status error';
if (progressContainer) progressContainer.style.display = 'none';
restoreBtn();
return;
}
}
setBtnLoading(isShitpost ? `Uploading 1/${selectedFiles.length}...` : 'Uploading...');
if (progressContainer) progressContainer.style.display = 'flex';
if (statusDiv) {
@@ -2909,6 +3319,13 @@ window.initUploadForm = (selector) => {
form.reset();
tags = [];
selectedFiles = [];
if (albumChoiceContainer) albumChoiceContainer.style.display = 'none';
albumChoiceMode = 'album';
if (albumChoiceContainer) {
albumChoiceContainer.querySelectorAll('.album-choice-btn').forEach(b => {
b.classList.toggle('active', b.getAttribute('data-choice') === 'album');
});
}
if (tagsList) tagsList.innerHTML = '';
if (tagsHidden) tagsHidden.value = '';
if (fileInput) fileInput.style.display = 'inline-block';
@@ -2937,6 +3354,7 @@ window.initUploadForm = (selector) => {
// Reset mode to 'file'
activeMode = 'file';
form.classList.remove('album-mode-active');
if (modeTabs.length > 0) {
modeTabs.forEach(t => {
if (t.dataset.mode === 'file') t.classList.add('active');
+18 -7
View File
@@ -134,8 +134,8 @@ class v0ck {
}
if (tagName === "audio" && elem.hasAttribute('poster')) { // set cover
const player = document.querySelector('.v0ck');
player.style.backgroundImage = `url('${elem.getAttribute('poster')}')`;
const player = elem.closest('.v0ck') || document.querySelector('.v0ck');
if (player) player.style.backgroundImage = `url('${elem.getAttribute('poster')}')`;
}
}
else
@@ -144,7 +144,7 @@ class v0ck {
}
init(elem) {
const player = document.querySelector('.v0ck');
const player = elem.closest('.v0ck') || document.querySelector('.v0ck');
const video = elem;
video.removeAttribute('controls');
video.removeAttribute('autoplay');
@@ -200,10 +200,21 @@ class v0ck {
return video[video.paused ? 'play' : 'pause']();
}
function updatePlayIcon() {
toggle.classList.toggle('playing');
player.classList.toggle('paused');
toggle.setAttribute('title', toggle.classList.contains('playing') ? 'Pause' : 'Play');
[...toggle.querySelectorAll('use')].forEach(icon => icon.classList.toggle('v0ck_hidden'));
const isPlaying = !video.paused;
toggle.classList.toggle('playing', isPlaying);
player.classList.toggle('paused', !isPlaying);
toggle.setAttribute('title', isPlaying ? 'Pause' : 'Play');
const playIcon = toggle.querySelector('#v0ck_svg_play');
const pauseIcon = toggle.querySelector('#v0ck_svg_pause');
if (playIcon && pauseIcon) {
playIcon.classList.toggle('v0ck_hidden', isPlaying);
pauseIcon.classList.toggle('v0ck_hidden', !isPlaying);
} else {
[...toggle.querySelectorAll('use')].forEach(icon => {
const isPlaySvg = icon.id === 'v0ck_svg_play' || icon.getAttribute('href')?.includes('play');
icon.classList.toggle('v0ck_hidden', isPlaySvg ? isPlaying : !isPlaying);
});
}
}
function toggleMute(e) {
if (video.volume === 0)