From 9a9b787fd715fd52d7bc6e15686af046b7eefd53 Mon Sep 17 00:00:00 2001 From: Kibi Kelburton Date: Sat, 23 May 2026 22:38:28 +0200 Subject: [PATCH] add shitpost mode config options --- config_example.json | 2 ++ public/s/js/upload.js | 61 +++++++++++++++++++++++++++----------- src/index.mjs | 2 ++ src/upload_handler.mjs | 15 ++++++++-- views/snippets/header.html | 2 +- 5 files changed, 61 insertions(+), 21 deletions(-) diff --git a/config_example.json b/config_example.json index 445093f..3fc91b6 100644 --- a/config_example.json +++ b/config_example.json @@ -86,6 +86,8 @@ "web_meta_extraction": true, "bypass_duplicate_check": true, "shitpost_mode": false, + "shitpost_require_rating": false, + "shitpost_min_tags": 0, "protect_files": false, "enable_dynamic_thumbs": false, "allowed_comment_images": [ diff --git a/public/s/js/upload.js b/public/s/js/upload.js index de8d5bf..406f2f7 100644 --- a/public/s/js/upload.js +++ b/public/s/js/upload.js @@ -74,6 +74,9 @@ window.initUploadForm = (selector) => { const commentMaxLen = (commentMaxLenAttr && commentMaxLenAttr !== 'null') ? parseInt(commentMaxLenAttr) : null; const isShitpost = form.classList.contains('shitpost-mode-active') || !!window.f0ckShitpostMode; + // Config-driven shitpost overrides + const shitpostRequireRating = isShitpost && !!window.f0ckShitpostRequireRating; + const shitpostMinTags = isShitpost ? (parseInt(window.f0ckShitpostMinTags) || 0) : 0; let tags = []; let autoTags = []; // Track tags suggested from metadata let selectedFiles = []; // Array of files for shitpost_mode @@ -548,14 +551,23 @@ window.initUploadForm = (selector) => { const isShitpost = !!window.f0ckShitpostMode; const rating = form.querySelector('input[name="rating"]:checked'); - // In Shitpost Mode, ratings are per-item (optional) and tags are optional — just need files - const hasRating = (isShitpost && activeMode === 'file') ? true : (rating !== null); + // In Shitpost Mode, ratings are per-item. If require rating is true, every item must be rated. + let hasRating = true; + if (isShitpost && activeMode === 'file') { + if (shitpostRequireRating) { + hasRating = selectedFiles.length > 0 && selectedFiles.every(item => ['sfw', 'nsfw', 'nsfl'].includes(item.rating)); + } + } else { + hasRating = (rating !== null); + } let hasTags = true; if (!isShitpost) { 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. + hasTags = selectedFiles.length === 0 || selectedFiles.every(item => (item.tags || []).length >= shitpostMinTags); } - // In shitpost file mode: hasTags is always true (untagged is allowed) // Toggle visibility of global rating/comment/tag sections const ratingSec = form.querySelector('.global-rating-section'); @@ -606,18 +618,27 @@ window.initUploadForm = (selector) => { ? (ssrSelectFileText || i18n.select_file || 'Select a file') : (i18n.enter_url || 'Enter a URL'); } else if (!hasTags) { - // non-shitpost only - const remaining = minTags - tags.length; - const tpl = i18n.tags_required || '{n} more tag{s} required'; - btnText.textContent = tpl - .replace('{n}', remaining) - .replace('{s}', remaining !== 1 ? 's' : ''); + // non-shitpost or shitpost with min-tags + if (isShitpost && 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 { + const remaining = minTags - tags.length; + const tpl = i18n.tags_required || '{n} more tag{s} required'; + btnText.textContent = tpl + .replace('{n}', remaining) + .replace('{s}', remaining !== 1 ? 's' : ''); + } } else if (!hasRating) { const nsflEnabled = !!form.querySelector('input[name="rating"][value="nsfl"]'); - if (nsflEnabled) { - btnText.textContent = i18n.select_rating_nsfl || 'Select SFW, NSFW or NSFL'; + if (isShitpost && shitpostRequireRating) { + btnText.textContent = 'Select a rating for each item'; } else { - btnText.textContent = i18n.select_rating || 'Select SFW or NSFW'; + if (nsflEnabled) { + btnText.textContent = i18n.select_rating_nsfl || 'Select SFW, NSFW or NSFL'; + } else { + btnText.textContent = i18n.select_rating || 'Select SFW or NSFW'; + } } } else { if (activeMode === 'url' && urlInput && ytRegex.test(urlInput.value.trim()) && window.f0ckEnableYoutubeUpload !== false) { @@ -841,19 +862,21 @@ window.initUploadForm = (selector) => { let commentUI = ''; if (isShitpost) { const nsflEnabled = !!form.querySelector('input[name="rating"][value="nsfl"]'); + // Build per-item rating HTML + const ratingValue = item.rating; ratingSwitch = `
${nsflEnabled ? ` ` : ''} @@ -861,10 +884,11 @@ window.initUploadForm = (selector) => { `; const tagsPlaceholder = window.f0ckI18n?.upload_tags_placeholder || 'Tags...'; + const minTagsHint = shitpostMinTags > 0 ? ` (min ${shitpostMinTags})` : ''; tagsUI = `
- +
@@ -901,7 +925,10 @@ window.initUploadForm = (selector) => { if (isShitpost) { // Handle Rating infoRow.querySelectorAll('.item-rating-option input').forEach(radio => { - radio.onchange = () => { item.rating = radio.value; }; + radio.onchange = () => { + item.rating = radio.value; + updateSubmitButton(); + }; }); // Handle Comment diff --git a/src/index.mjs b/src/index.mjs index 68bcdde..b2737b4 100644 --- a/src/index.mjs +++ b/src/index.mjs @@ -1077,6 +1077,8 @@ process.on('uncaughtException', err => { registration_web_toggle_enabled: cfg.websrv.open_registration_web_toggle !== false, get trusted_uploads() { return getTrustedUploads(); }, get shitpost_mode() { return getShitpostMode(); }, + shitpost_require_rating: !!cfg.websrv.shitpost_require_rating, + shitpost_min_tags: parseInt(cfg.websrv.shitpost_min_tags) || 0, get about_text() { return getAboutText(); }, get rules_text() { return getRulesText(); }, get terms_text() { return getTermsText(); }, diff --git a/src/upload_handler.mjs b/src/upload_handler.mjs index 0b3fc0f..04f60fd 100644 --- a/src/upload_handler.mjs +++ b/src/upload_handler.mjs @@ -119,23 +119,32 @@ export const handleUpload = async (req, res, self) => { return sendJson(res, { success: false, msg: 'No file provided' }, 400); } - // In shitpost mode, rating is optional — null means no rating tag is assigned (truly untagged) - const effectiveRating = (rating && ['sfw', 'nsfw', 'nsfl'].includes(rating)) ? rating : (is_shitpost ? null : null); + // In shitpost mode, rating is optional — null means no rating tag is assigned (truly untagged). + // If shitpost_require_rating is configured to true, a rating is strictly required. + const effectiveRating = (rating && ['sfw', 'nsfw', 'nsfl'].includes(rating)) ? rating : null; if (!is_shitpost && !effectiveRating) { return sendJson(res, { success: false, msg: 'Rating (sfw/nsfw/nsfl) is required' }, 400); } + if (is_shitpost && cfg.websrv.shitpost_require_rating === true && !effectiveRating) { + return sendJson(res, { success: false, msg: 'Rating (sfw/nsfw/nsfl) is required for each item' }, 400); + } + if (effectiveRating === 'nsfl' && !cfg.enable_nsfl) { return sendJson(res, { success: false, msg: 'NSFL mode is currently disabled' }, 400); } const tags = tagsRaw ? tagsRaw.split(',').map(t => t.trim()).filter(t => t.length > 0 && !['sfw', 'nsfw', 'nsfl'].includes(t.toLowerCase())) : []; const minTags = getMinTags(); - // In shitpost mode, tags are optional — items without tags enter as untagged + // In shitpost mode, tags are optional by default — unless shitpost_min_tags is configured. + const shitpostMinTags = is_shitpost ? (parseInt(cfg.websrv.shitpost_min_tags) || 0) : 0; if (!is_shitpost && tags.length < minTags) { return sendJson(res, { success: false, msg: `At least ${minTags} tags are required` }, 400); } + if (is_shitpost && shitpostMinTags > 0 && tags.length < shitpostMinTags) { + return sendJson(res, { success: false, msg: `At least ${shitpostMinTags} tag${shitpostMinTags !== 1 ? 's' : ''} are required` }, 400); + } // Validate MIME type const allowedMimes = Object.keys(cfg.mimes); diff --git a/views/snippets/header.html b/views/snippets/header.html index b8cf00a..3322114 100644 --- a/views/snippets/header.html +++ b/views/snippets/header.html @@ -46,7 +46,7 @@ @endif @endif - + @if(!private_society || session)