Feature: Shitpost Mode -> upload multiple files at once
This commit is contained in:
@@ -65,6 +65,7 @@
|
|||||||
"enable_youtube_upload": true,
|
"enable_youtube_upload": true,
|
||||||
"web_meta_extraction": true,
|
"web_meta_extraction": true,
|
||||||
"bypass_duplicate_check": true,
|
"bypass_duplicate_check": true,
|
||||||
|
"shitpost_mode": false,
|
||||||
"protect_files": false,
|
"protect_files": false,
|
||||||
"allowed_comment_images": [
|
"allowed_comment_images": [
|
||||||
"i.imgur.com",
|
"i.imgur.com",
|
||||||
|
|||||||
@@ -5134,9 +5134,9 @@ div.posts>a[data-mode="nsfl"]>p::before {
|
|||||||
background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgba(227, 7, 7, 0) 85%, rgb(231, 3, 3) 100%);
|
background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgba(227, 7, 7, 0) 85%, rgb(231, 3, 3) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
div.posts>a[data-mode="null"]>p:before {
|
div.posts > a[data-mode="null"] > p::before {
|
||||||
background-color: #dcd512;
|
background: #000000;
|
||||||
/* untagged */
|
background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgba(63, 196, 61, 0) 85%, rgb(244, 222, 0) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
div#footbar {
|
div#footbar {
|
||||||
|
|||||||
@@ -178,23 +178,375 @@
|
|||||||
background: #fa5252;
|
background: #fa5252;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ratings */
|
/* Default/Non-Shitpost Mode: Centered Card */
|
||||||
@media(max-width: 700px) {
|
.upload-form:not(.shitpost-mode-active) .file-preview-item {
|
||||||
.rating-options {
|
flex-direction: column;
|
||||||
display: grid;
|
align-items: center;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
text-align: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Shitpost Mode: Two-Column Row */
|
||||||
|
.upload-form.shitpost-mode-active .file-preview-item {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-preview-item {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
position: relative;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
animation: previewItemIn 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-preview-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
border-color: rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes previewItemIn {
|
||||||
|
from { opacity: 0; transform: translateX(-15px); }
|
||||||
|
to { opacity: 1; transform: translateX(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-form:not(.shitpost-mode-active) .preview-media-small {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
min-height: auto;
|
||||||
|
height: auto;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-form.shitpost-mode-active .preview-media-small {
|
||||||
|
width: 120px;
|
||||||
|
height: 80px;
|
||||||
|
object-fit: cover;
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-media-small {
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #000;
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.upload-form:not(.shitpost-mode-active) .file-meta-row-small {
|
||||||
|
padding-right: 0;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-form.shitpost-mode-active .file-meta-row-small {
|
||||||
|
padding-right: 30px; /* Space for X button */
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-meta-row-small {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
overflow: hidden;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.rating-option:nth-child(3) {
|
.file-info-small {
|
||||||
grid-column: 1 / span 2;
|
display: flex;
|
||||||
}
|
flex-direction: column;
|
||||||
|
gap: 3px;
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
.tag-suggestions {
|
.file-name-small {
|
||||||
left: 0px;
|
font-size: 0.95rem;
|
||||||
right: 0px;
|
font-weight: 500;
|
||||||
}
|
overflow: hidden;
|
||||||
|
color: #fff;
|
||||||
|
max-width: 100%;
|
||||||
|
display: block;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-size-small {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
opacity: 0.4;
|
||||||
|
font-family: 'Outfit', sans-serif;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-remove-small {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
z-index: 10;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-remove-small:hover {
|
||||||
|
background: #ff6b6b;
|
||||||
|
color: white;
|
||||||
|
border-color: #ff6b6b;
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
.btn-add-urls {
|
||||||
|
margin-top: 10px;
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
color: #fff;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-add-urls:hover {
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-more-item {
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1.5px dashed rgba(255, 255, 255, 0.15);
|
||||||
|
background: rgba(255, 255, 255, 0.02);
|
||||||
|
cursor: pointer;
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
backdrop-filter: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.add-more-item i {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-more-item:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--accent);
|
||||||
|
background: rgba(var(--accent-rgb), 0.05);
|
||||||
|
border-style: solid;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-form.shitpost-mode-active .global-rating-section,
|
||||||
|
.upload-form.shitpost-mode-active .global-comment-section,
|
||||||
|
.upload-form.shitpost-mode-active .global-tag-section {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Per-item Rating Switch */
|
||||||
|
.item-rating-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-rating-option {
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-rating-option input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-rating-label {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 700;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
color: rgba(255, 255, 255, 0.4);
|
||||||
|
transition: all 0.2s;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-rating-option input:checked + .item-rating-label.sfw {
|
||||||
|
background: #40c057;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #40c057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-rating-option input:checked + .item-rating-label.nsfw {
|
||||||
|
background: #fd7e14;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #fd7e14;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-rating-option input:checked + .item-rating-label.nsfl {
|
||||||
|
background: #fa5252;
|
||||||
|
color: #fff;
|
||||||
|
border-color: #fa5252;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-rating-label:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bigger Previews in Shitpost Mode */
|
||||||
|
.file-preview-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 15px;
|
||||||
|
gap: 0;
|
||||||
|
width: 100%;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-media-small {
|
||||||
|
flex: 0 0 50%;
|
||||||
|
width: 50% !important;
|
||||||
|
height: auto !important;
|
||||||
|
min-height: 120px;
|
||||||
|
max-height: 350px;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Per-item Tags */
|
||||||
|
.item-tags-container {
|
||||||
|
margin-top: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-tags-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-tag-chip {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-tag-remove {
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-tag-remove:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-tag-input {
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(0,0,0,0.3);
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-tag-input:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-meta-suggestions {
|
||||||
|
margin-top: 5px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-meta-suggestion {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px dashed rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-meta-suggestion:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-meta-suggestion.selected {
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border-color: var(--accent);
|
||||||
|
border-style: solid;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-meta-suggestion.selected i {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.item-comment-input {
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(0,0,0,0.3);
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
margin-top: 10px;
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 40px;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-comment-input:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rating-options {
|
.rating-options {
|
||||||
@@ -398,6 +750,10 @@
|
|||||||
scrollbar-width: thin !important;
|
scrollbar-width: thin !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.upload-form.shitpost-mode-active .tag-suggestions {
|
||||||
|
position: relative !important;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes tagDropIn {
|
@keyframes tagDropIn {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -409,7 +765,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#upload-form .tag-suggestion-item {
|
.tag-suggestion-item {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
justify-content: space-between !important;
|
justify-content: space-between !important;
|
||||||
align-items: center !important;
|
align-items: center !important;
|
||||||
@@ -418,7 +774,7 @@
|
|||||||
cursor: pointer !important;
|
cursor: pointer !important;
|
||||||
transition: background 0.12s !important;
|
transition: background 0.12s !important;
|
||||||
box-sizing: border-box !important;
|
box-sizing: border-box !important;
|
||||||
user-select: none !important;
|
user-select: text !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#upload-form .tag-suggestion-item:not(:last-child) {
|
#upload-form .tag-suggestion-item:not(:last-child) {
|
||||||
@@ -446,7 +802,7 @@
|
|||||||
/* Submit Button */
|
/* Submit Button */
|
||||||
.btn-upload {
|
.btn-upload {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: #000;
|
color: var(--bg);
|
||||||
border: none;
|
border: none;
|
||||||
padding: 1rem 2rem;
|
padding: 1rem 2rem;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
@@ -464,8 +820,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-upload:not(:disabled):hover {
|
.btn-upload:not(:disabled):hover {
|
||||||
transform: translateY(-2px);
|
filter: brightness(1.1);
|
||||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Progress */
|
/* Progress */
|
||||||
@@ -747,12 +1102,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.meta-suggestion.selected {
|
.meta-suggestion.selected {
|
||||||
opacity: 0.4;
|
background: rgba(255, 255, 255, 0.08);
|
||||||
cursor: default;
|
border-color: var(--accent);
|
||||||
pointer-events: none;
|
color: rgba(255, 255, 255, 0.9);
|
||||||
background: rgba(255, 255, 255, 0.02);
|
|
||||||
border-color: rgba(255, 255, 255, 0.05);
|
|
||||||
color: rgba(255, 255, 255, 0.4) !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.meta-suggestion.selected i {
|
.meta-suggestion.selected i {
|
||||||
@@ -809,3 +1161,11 @@
|
|||||||
}
|
}
|
||||||
.gps-privacy-warning.gps-stripped i,
|
.gps-privacy-warning.gps-stripped i,
|
||||||
.gps-privacy-warning.gps-stripped span { color: #4caf50; }
|
.gps-privacy-warning.gps-stripped span { color: #4caf50; }
|
||||||
|
|
||||||
|
.item-meta-suggestion span {
|
||||||
|
user-select: text !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-comment-input {
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@
|
|||||||
const files = e.dataTransfer.files;
|
const files = e.dataTransfer.files;
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
if (uploader && uploader.handleFile) {
|
if (uploader && uploader.handleFile) {
|
||||||
const ok = uploader.handleFile(files[0]);
|
const ok = uploader.handleFile(files);
|
||||||
if (ok !== false) {
|
if (ok !== false) {
|
||||||
showModal();
|
showModal();
|
||||||
}
|
}
|
||||||
@@ -151,12 +151,12 @@
|
|||||||
|
|
||||||
if (targetUploader && targetUploader.handleFile) {
|
if (targetUploader && targetUploader.handleFile) {
|
||||||
if (isUploadPage || isModalOpen) {
|
if (isUploadPage || isModalOpen) {
|
||||||
targetUploader.handleFile(file);
|
targetUploader.handleFile([file]);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
} else if (!isTyping) {
|
} else if (!isTyping) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
showModal();
|
showModal();
|
||||||
targetUploader.handleFile(file);
|
targetUploader.handleFile([file]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -132,13 +132,36 @@ window.TagAutocomplete = (() => {
|
|||||||
row.appendChild(name);
|
row.appendChild(name);
|
||||||
row.appendChild(meta);
|
row.appendChild(meta);
|
||||||
|
|
||||||
// Desktop: mousedown fires before focusout, preventing premature close
|
// Partial Selection Support
|
||||||
row.addEventListener('mousedown', (e) => {
|
row.addEventListener('mouseup', (ev) => {
|
||||||
|
const sel = window.getSelection?.()?.toString().trim();
|
||||||
|
if (!sel || sel === entry.tag) return;
|
||||||
|
|
||||||
|
row.addEventListener('click', (e) => e.stopImmediatePropagation(), { once: true, capture: true });
|
||||||
|
ev.stopPropagation();
|
||||||
|
|
||||||
|
window._showSelTagPopover?.(sel, row, (confirmed) => {
|
||||||
|
window.getSelection?.()?.removeAllRanges();
|
||||||
|
input.value = confirmed;
|
||||||
|
dropdown.style.display = 'none';
|
||||||
|
if (form.requestSubmit) form.requestSubmit();
|
||||||
|
else form.submit();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Desktop: click listener handles the actual selection
|
||||||
|
row.addEventListener('click', (e) => {
|
||||||
|
const sel = window.getSelection?.()?.toString().trim();
|
||||||
|
if (sel && sel !== entry.tag) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.stopPropagation();
|
||||||
input.value = entry.tag;
|
input.value = entry.tag;
|
||||||
dropdown.style.display = 'none';
|
dropdown.style.display = 'none';
|
||||||
form.requestSubmit();
|
if (form.requestSubmit) form.requestSubmit();
|
||||||
|
else form.submit();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mobile: distinguish tap from scroll using touch distance
|
// Mobile: distinguish tap from scroll using touch distance
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ window.F0ckUpload = class {
|
|||||||
const tagInput = this.form.querySelector('.tag-input');
|
const tagInput = this.form.querySelector('.tag-input');
|
||||||
if (tagInput) {
|
if (tagInput) {
|
||||||
tagInput.addEventListener('keydown', (e) => {
|
tagInput.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter' && !e.ctrlKey && !e.metaKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.addTag(tagInput.value);
|
this.addTag(tagInput.value);
|
||||||
tagInput.value = '';
|
tagInput.value = '';
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -61,7 +61,13 @@
|
|||||||
"uploading": "Wird hochgeladen...",
|
"uploading": "Wird hochgeladen...",
|
||||||
"pending_approval_patient": "Upload wartet auf Freigabe, bitte haben Sie etwas Geduld",
|
"pending_approval_patient": "Upload wartet auf Freigabe, bitte haben Sie etwas Geduld",
|
||||||
"remove_file": "Datei entfernen",
|
"remove_file": "Datei entfernen",
|
||||||
"cancel_upload": "Upload abbrechen"
|
"cancel_upload": "Upload abbrechen",
|
||||||
|
"shitpost_success": "{n} Beiträge erfolgreich gepostet!",
|
||||||
|
"shitposting_status": "Wird gepostet",
|
||||||
|
"item_comment_placeholder": "Kommentar (optional)...",
|
||||||
|
"item_tags_placeholder": "Tags...",
|
||||||
|
"btn_add_urls": "URL(s) hinzufügen",
|
||||||
|
"tags_required_shitpost": "Alle Beiträge benötigen Tags"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"registering": "Wird registriert...",
|
"registering": "Wird registriert...",
|
||||||
@@ -305,6 +311,7 @@
|
|||||||
"enter_url": "URL eingeben",
|
"enter_url": "URL eingeben",
|
||||||
"tags_required": "{n} Tag(s) noch erforderlich",
|
"tags_required": "{n} Tag(s) noch erforderlich",
|
||||||
"select_rating": "SFW oder NSFW auswählen",
|
"select_rating": "SFW oder NSFW auswählen",
|
||||||
|
"select_rating_nsfl": "SFW, NSFW oder NSFL auswählen",
|
||||||
"embed_youtube": "YouTube-Video einbetten",
|
"embed_youtube": "YouTube-Video einbetten",
|
||||||
"upload_from_url": "Von URL hochladen",
|
"upload_from_url": "Von URL hochladen",
|
||||||
"upload": "Hochladen"
|
"upload": "Hochladen"
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
"url_tab_yt": "URL / YouTube",
|
"url_tab_yt": "URL / YouTube",
|
||||||
"url_placeholder": "Paste a URL to download...",
|
"url_placeholder": "Paste a URL to download...",
|
||||||
"url_placeholder_yt": "Paste a URL or YouTube link...",
|
"url_placeholder_yt": "Paste a URL or YouTube link...",
|
||||||
|
"url_placeholder_shitpost": "Paste multiple URLs here (one per line)...",
|
||||||
"drop_here": "Drop your file here",
|
"drop_here": "Drop your file here",
|
||||||
"admin_boost": "Admin Boost",
|
"admin_boost": "Admin Boost",
|
||||||
"custom_thumbnail": "Custom Thumbnail",
|
"custom_thumbnail": "Custom Thumbnail",
|
||||||
@@ -61,7 +62,13 @@
|
|||||||
"uploading": "Uploading...",
|
"uploading": "Uploading...",
|
||||||
"pending_approval_patient": "Upload awaits approval, please be patient",
|
"pending_approval_patient": "Upload awaits approval, please be patient",
|
||||||
"remove_file": "Remove File",
|
"remove_file": "Remove File",
|
||||||
"cancel_upload": "Cancel Upload"
|
"cancel_upload": "Cancel Upload",
|
||||||
|
"shitpost_success": "Successfully shitposted {n} items!",
|
||||||
|
"shitposting_status": "Shitposting",
|
||||||
|
"item_comment_placeholder": "Comment (optional)...",
|
||||||
|
"item_tags_placeholder": "Tags...",
|
||||||
|
"btn_add_urls": "Add URL(s)",
|
||||||
|
"tags_required_shitpost": "All items need tags"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"registering": "Registering...",
|
"registering": "Registering...",
|
||||||
@@ -305,6 +312,7 @@
|
|||||||
"enter_url": "Enter a URL",
|
"enter_url": "Enter a URL",
|
||||||
"tags_required": "{n} more tag{s} required",
|
"tags_required": "{n} more tag{s} required",
|
||||||
"select_rating": "Select SFW or NSFW",
|
"select_rating": "Select SFW or NSFW",
|
||||||
|
"select_rating_nsfl": "Select SFW, NSFW or NSFL",
|
||||||
"embed_youtube": "Embed YouTube Video",
|
"embed_youtube": "Embed YouTube Video",
|
||||||
"upload_from_url": "Upload from URL",
|
"upload_from_url": "Upload from URL",
|
||||||
"upload": "Upload"
|
"upload": "Upload"
|
||||||
|
|||||||
@@ -61,7 +61,13 @@
|
|||||||
"uploading": "Uploaden...",
|
"uploading": "Uploaden...",
|
||||||
"pending_approval_patient": "Upload wacht op goedkeuring, even geduld alstublieft",
|
"pending_approval_patient": "Upload wacht op goedkeuring, even geduld alstublieft",
|
||||||
"remove_file": "Bestand Verwijderen",
|
"remove_file": "Bestand Verwijderen",
|
||||||
"cancel_upload": "Upload Annuleren"
|
"cancel_upload": "Upload Annuleren",
|
||||||
|
"shitpost_success": "{n} items succesvol gepost!",
|
||||||
|
"shitposting_status": "Lekker shitposten",
|
||||||
|
"item_comment_placeholder": "Opmerking (optioneel)...",
|
||||||
|
"item_tags_placeholder": "Etiketten...",
|
||||||
|
"btn_add_urls": "URL(s) toevoegen",
|
||||||
|
"tags_required_shitpost": "Alle items hebben tags nodig"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"registering": "Registreren...",
|
"registering": "Registreren...",
|
||||||
@@ -305,6 +311,7 @@
|
|||||||
"enter_url": "Voer een URL in",
|
"enter_url": "Voer een URL in",
|
||||||
"tags_required": "{n} extra tag{s} vereist",
|
"tags_required": "{n} extra tag{s} vereist",
|
||||||
"select_rating": "Selecteer SFW of NSFW",
|
"select_rating": "Selecteer SFW of NSFW",
|
||||||
|
"select_rating_nsfl": "Selecteer SFW, NSFW of NSFL",
|
||||||
"embed_youtube": "YouTube-video Insluiten",
|
"embed_youtube": "YouTube-video Insluiten",
|
||||||
"upload_from_url": "Uploaden via URL",
|
"upload_from_url": "Uploaden via URL",
|
||||||
"upload": "Uploaden"
|
"upload": "Uploaden"
|
||||||
|
|||||||
@@ -61,7 +61,11 @@
|
|||||||
"uploading": "Wird aufladiert...",
|
"uploading": "Wird aufladiert...",
|
||||||
"pending_approval_patient": "Die Ladung harrt der Absegnung, bitte haben Sie Geduld",
|
"pending_approval_patient": "Die Ladung harrt der Absegnung, bitte haben Sie Geduld",
|
||||||
"remove_file": "Datei entfernen",
|
"remove_file": "Datei entfernen",
|
||||||
"cancel_upload": "Aufladierung abbrechen"
|
"cancel_upload": "Aufladierung abbrechen",
|
||||||
|
"shitpost_success": "{n} Fetzen erfolgreich gepfeffert!",
|
||||||
|
"shitposting_status": "Wird gepfeffert",
|
||||||
|
"item_comment_placeholder": "Senf dazugeben (optional)...",
|
||||||
|
"item_tags_placeholder": "Etiketten..."
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"registering": "Registrierung wird in die Wege geleitet...",
|
"registering": "Registrierung wird in die Wege geleitet...",
|
||||||
|
|||||||
@@ -253,6 +253,16 @@ export default new class queue {
|
|||||||
const tmpFile = path.join(os.tmpdir(), itemid + '.png');
|
const tmpFile = path.join(os.tmpdir(), itemid + '.png');
|
||||||
const tmpJpg = path.join(os.tmpdir(), itemid + '.jpg');
|
const tmpJpg = path.join(os.tmpdir(), itemid + '.jpg');
|
||||||
|
|
||||||
|
// Resolve real path if it's a symlink (important for reposts)
|
||||||
|
let sourcePath = path.join(bDir, filename);
|
||||||
|
try {
|
||||||
|
const lstat = await fs.promises.lstat(sourcePath);
|
||||||
|
if (lstat.isSymbolicLink()) {
|
||||||
|
sourcePath = await fs.promises.realpath(sourcePath);
|
||||||
|
console.log(`[QUEUE] Resolved symlink for thumbnailing: ${filename} -> ${sourcePath}`);
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
if (mime === 'video/youtube') {
|
if (mime === 'video/youtube') {
|
||||||
const videoId = filename.startsWith('yt:') ? filename.split(':')[1] : null;
|
const videoId = filename.startsWith('yt:') ? filename.split(':')[1] : null;
|
||||||
if (videoId) {
|
if (videoId) {
|
||||||
@@ -271,7 +281,7 @@ export default new class queue {
|
|||||||
else if (mime.startsWith('video/') || mime == 'image/gif') {
|
else if (mime.startsWith('video/') || mime == 'image/gif') {
|
||||||
const seeks = ['20%', '40%', '60%', '80%'];
|
const seeks = ['20%', '40%', '60%', '80%'];
|
||||||
for (const seek of seeks) {
|
for (const seek of seeks) {
|
||||||
await this.spawn('ffmpegthumbnailer', ['-i', path.join(bDir, filename), '-s', '1024', '-t', seek, '-o', tmpFile]);
|
await this.spawn('ffmpegthumbnailer', ['-i', sourcePath, '-s', '1024', '-t', seek, '-o', tmpFile]);
|
||||||
try {
|
try {
|
||||||
const { stdout } = await this.spawn('magick', [tmpFile, '-colorspace', 'Gray', '-format', '%[fx:mean]', 'info:']);
|
const { stdout } = await this.spawn('magick', [tmpFile, '-colorspace', 'Gray', '-format', '%[fx:mean]', 'info:']);
|
||||||
if (parseFloat(stdout.trim()) > 0.05) break;
|
if (parseFloat(stdout.trim()) > 0.05) break;
|
||||||
@@ -279,9 +289,10 @@ export default new class queue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (mime.startsWith('image/') && mime != 'image/gif')
|
else if (mime.startsWith('image/') && mime != 'image/gif')
|
||||||
await this.spawn('magick', [path.join(bDir, filename) + '[0]', tmpFile]);
|
await this.spawn('magick', [sourcePath + '[0]', tmpFile]);
|
||||||
else if (mime.startsWith('audio/')) {
|
else if (mime.startsWith('audio/')) {
|
||||||
let coverExtracted = false;
|
let coverExtracted = false;
|
||||||
|
this._lastCoverExtracted = false; // Reset state for this call
|
||||||
if (link.match(/soundcloud/)) {
|
if (link.match(/soundcloud/)) {
|
||||||
const proxyArgs = (cfg.main.socks && cfg.main.socks !== 'undefined' && cfg.main.socks !== '') ? ['--proxy', cfg.main.socks.includes('://') ? cfg.main.socks : `socks5h://${cfg.main.socks}`] : [];
|
const proxyArgs = (cfg.main.socks && cfg.main.socks !== 'undefined' && cfg.main.socks !== '') ? ['--proxy', cfg.main.socks.includes('://') ? cfg.main.socks : `socks5h://${cfg.main.socks}`] : [];
|
||||||
let cover = (await this.spawn('yt-dlp', [...proxyArgs, '-f', 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w', '--get-thumbnail', link])).stdout.trim().split('\n').map(l => l.trim()).filter(l => l.length > 0).pop();
|
let cover = (await this.spawn('yt-dlp', [...proxyArgs, '-f', 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w', '--get-thumbnail', link])).stdout.trim().split('\n').map(l => l.trim()).filter(l => l.length > 0).pop();
|
||||||
@@ -301,7 +312,7 @@ export default new class queue {
|
|||||||
}
|
}
|
||||||
if (!coverExtracted) {
|
if (!coverExtracted) {
|
||||||
try {
|
try {
|
||||||
await this.spawn('ffmpeg', ['-i', path.join(bDir, filename), '-an', '-vcodec', 'copy', '-frames:v', '1', '-update', '1', tmpJpg]);
|
await this.spawn('ffmpeg', ['-i', sourcePath, '-an', '-vcodec', 'copy', '-frames:v', '1', '-update', '1', tmpJpg]);
|
||||||
const size = (await fs.promises.stat(tmpJpg)).size;
|
const size = (await fs.promises.stat(tmpJpg)).size;
|
||||||
if (size > 0) {
|
if (size > 0) {
|
||||||
await this.spawn('magick', [tmpJpg, tmpFile]);
|
await this.spawn('magick', [tmpJpg, tmpFile]);
|
||||||
@@ -313,7 +324,7 @@ export default new class queue {
|
|||||||
} else {
|
} else {
|
||||||
// Try extracting embedded cover art (video stream in audio file)
|
// Try extracting embedded cover art (video stream in audio file)
|
||||||
try {
|
try {
|
||||||
await this.spawn('ffmpeg', ['-i', path.join(bDir, filename), '-an', '-vcodec', 'copy', '-frames:v', '1', '-update', '1', tmpJpg]);
|
await this.spawn('ffmpeg', ['-i', sourcePath, '-an', '-vcodec', 'copy', '-frames:v', '1', '-update', '1', tmpJpg]);
|
||||||
const size = (await fs.promises.stat(tmpJpg)).size;
|
const size = (await fs.promises.stat(tmpJpg)).size;
|
||||||
if (size > 0) {
|
if (size > 0) {
|
||||||
await this.spawn('magick', [tmpJpg, tmpFile]);
|
await this.spawn('magick', [tmpJpg, tmpFile]);
|
||||||
@@ -367,7 +378,7 @@ export default new class queue {
|
|||||||
'-dTextAlphaBits=4', '-dGraphicsAlphaBits=4',
|
'-dTextAlphaBits=4', '-dGraphicsAlphaBits=4',
|
||||||
'-dLastPage=1',
|
'-dLastPage=1',
|
||||||
'-sOutputFile=' + tmpFile,
|
'-sOutputFile=' + tmpFile,
|
||||||
path.join(bDir, filename)
|
sourcePath
|
||||||
]);
|
]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(`[QUEUE] PDF extraction failed for ${itemid}, using fallback icon.`);
|
console.warn(`[QUEUE] PDF extraction failed for ${itemid}, using fallback icon.`);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import cfg from "../config.mjs";
|
|||||||
import security from "../security.mjs";
|
import security from "../security.mjs";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { getManualApproval, setManualApproval, getMinTags, setMinTags, getRegistrationOpen, setRegistrationOpen, getTrustedUploads, setTrustedUploads, getEnablePdf, setEnablePdf, getLogUserIps, setLogUserIps, getHashUserIps, setHashUserIps, getEnableCleanup, setEnableCleanup, getCleanupStartDate, setCleanupStartDate, getCleanupEndDate, setCleanupEndDate } from "../settings.mjs";
|
import { getManualApproval, setManualApproval, getMinTags, setMinTags, getRegistrationOpen, setRegistrationOpen, getTrustedUploads, setTrustedUploads, getEnablePdf, setEnablePdf, getLogUserIps, setLogUserIps, getHashUserIps, setHashUserIps, getEnableCleanup, setEnableCleanup, getCleanupStartDate, setCleanupStartDate, getCleanupEndDate, setCleanupEndDate, getShitpostMode } from "../settings.mjs";
|
||||||
|
|
||||||
export default (router, tpl) => {
|
export default (router, tpl) => {
|
||||||
router.get(/^\/login(\/)?$/, async (req, res) => {
|
router.get(/^\/login(\/)?$/, async (req, res) => {
|
||||||
@@ -285,6 +285,7 @@ export default (router, tpl) => {
|
|||||||
log_user_ips: getLogUserIps(),
|
log_user_ips: getLogUserIps(),
|
||||||
hash_user_ips: getHashUserIps(),
|
hash_user_ips: getHashUserIps(),
|
||||||
enable_cleanup: getEnableCleanup(),
|
enable_cleanup: getEnableCleanup(),
|
||||||
|
shitpost_mode: getShitpostMode(),
|
||||||
enable_cleanup_config: cfg.websrv.enable_cleanup !== false,
|
enable_cleanup_config: cfg.websrv.enable_cleanup !== false,
|
||||||
tmp: null
|
tmp: null
|
||||||
}, req)
|
}, req)
|
||||||
@@ -625,6 +626,7 @@ export default (router, tpl) => {
|
|||||||
setRegistrationOpen(registration_open === 'true');
|
setRegistrationOpen(registration_open === 'true');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
await db`INSERT INTO site_settings (key, value) VALUES ('min_tags', ${min_tags.toString()}) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`;
|
await db`INSERT INTO site_settings (key, value) VALUES ('min_tags', ${min_tags.toString()}) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`;
|
||||||
await db`INSERT INTO site_settings (key, value) VALUES ('trusted_uploads', ${trusted_uploads.toString()}) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`;
|
await db`INSERT INTO site_settings (key, value) VALUES ('trusted_uploads', ${trusted_uploads.toString()}) ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value`;
|
||||||
|
|
||||||
|
|||||||
@@ -145,6 +145,57 @@ export default router => {
|
|||||||
return [...new Set(tags)];
|
return [...new Set(tags)];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
group.get(/\/meta\/extract-url$/, lib.loggedin, async (req, res) => {
|
||||||
|
const url = req.url.qs?.url;
|
||||||
|
if (!url) return res.json({ success: false, msg: 'URL required' }, 400);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const results = [];
|
||||||
|
const seen = new Set();
|
||||||
|
const addResult = (val) => {
|
||||||
|
if (!val) return;
|
||||||
|
const clean = String(val).replace(/<[^>]*>/g, '').replace(/[\x00-\x1F\x7F]/g, '').trim();
|
||||||
|
if (clean && clean.length > 1 && clean.length <= 255 && !seen.has(clean.toLowerCase())) {
|
||||||
|
seen.add(clean.toLowerCase());
|
||||||
|
results.push(clean);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add domain and auto-tags
|
||||||
|
const auto = autoTagsFromUrl(url);
|
||||||
|
auto.forEach(t => addResult(t));
|
||||||
|
|
||||||
|
// Try to get title via yt-dlp for supported sites
|
||||||
|
try {
|
||||||
|
const proxyArgs = (cfg.main.socks && cfg.main.socks !== 'undefined') ? ['--proxy', cfg.main.socks] : [];
|
||||||
|
const { stdout } = await queue.spawn('yt-dlp', [
|
||||||
|
...proxyArgs,
|
||||||
|
'--get-title',
|
||||||
|
'--get-description',
|
||||||
|
'--no-playlist',
|
||||||
|
'--skip-download',
|
||||||
|
url
|
||||||
|
], { quiet: true, timeout: 5000 });
|
||||||
|
|
||||||
|
if (stdout) {
|
||||||
|
const lines = stdout.split('\n').map(l => l.trim()).filter(l => l.length > 0);
|
||||||
|
if (lines[0]) addResult(lines[0]); // Title
|
||||||
|
if (lines[1]) {
|
||||||
|
// Description often has garbage, take only first line or short snippet
|
||||||
|
const desc = lines[1].split(/[.\n]/)[0].trim();
|
||||||
|
if (desc.length > 3) addResult(desc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback or ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json({ success: true, fields: results });
|
||||||
|
} catch (err) {
|
||||||
|
return res.json({ success: false, msg: err.message }, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
group.post(/\/upload-url$/, lib.loggedin, async (req, res) => {
|
group.post(/\/upload-url$/, lib.loggedin, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
if (!cfg.websrv.web_url_upload) {
|
if (!cfg.websrv.web_url_upload) {
|
||||||
@@ -515,7 +566,7 @@ export default router => {
|
|||||||
if (rating === 'nsfw' || rating === 'nsfl') await queue.genBlurredThumbnail(itemid, isApprovalRequired);
|
if (rating === 'nsfw' || rating === 'nsfl') await queue.genBlurredThumbnail(itemid, isApprovalRequired);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const tDir = isApprovalRequired ? path.join(cfg.paths.pending, 't') : cfg.paths.t;
|
const tDir = isApprovalRequired ? path.join(cfg.paths.pending, 't') : cfg.paths.t;
|
||||||
await queue.spawn('magick', ['./mugge.png', path.join(tDir, `${itemid}.webp`)]).catch(() => {});
|
await queue.spawn('magick', ['-size', '128x128', 'xc:#1a1a1a', path.join(tDir, `${itemid}.webp`)]).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
const ratingTagId = rating === 'sfw' ? 1 : (rating === 'nsfw' ? 2 : (cfg.nsfl_tag_id || 3));
|
const ratingTagId = rating === 'sfw' ? 1 : (rating === 'nsfw' ? 2 : (cfg.nsfl_tag_id || 3));
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ let enable_pdf = false;
|
|||||||
let enable_cleanup = false;
|
let enable_cleanup = false;
|
||||||
let cleanup_start_date = '';
|
let cleanup_start_date = '';
|
||||||
let cleanup_end_date = '';
|
let cleanup_end_date = '';
|
||||||
|
export const getShitpostMode = () => !!cfg.websrv.shitpost_mode;
|
||||||
|
export const setShitpostMode = (val) => {}; // No-op, strictly config-based
|
||||||
|
|
||||||
export const getEnableCleanup = () => {
|
export const getEnableCleanup = () => {
|
||||||
if (cfg.websrv.enable_cleanup === false) return false;
|
if (cfg.websrv.enable_cleanup === false) return false;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { handleEmojiUpload } from "./emoji_upload_handler.mjs";
|
|||||||
import { handleHallImageUpload, handleHallImageDelete, handleHallDelete, handleHallUpdate, handleHallCreate } from "./hall_image_handler.mjs";
|
import { handleHallImageUpload, handleHallImageDelete, handleHallDelete, handleHallUpdate, handleHallCreate } from "./hall_image_handler.mjs";
|
||||||
import { handleMetaExtract } from "./meta_extract_handler.mjs";
|
import { handleMetaExtract } from "./meta_extract_handler.mjs";
|
||||||
import { handleMetaStrip } from "./meta_strip_handler.mjs";
|
import { handleMetaStrip } from "./meta_strip_handler.mjs";
|
||||||
import { getManualApproval, setManualApproval, getMinTags, setMinTags, getRegistrationOpen, setRegistrationOpen, getTrustedUploads, setTrustedUploads, getBypassDuplicateCheck, setBypassDuplicateCheck, getProtectFiles, setProtectFiles, getPrivateMessages, setPrivateMessages, getDefaultLayout, setDefaultLayout, getEnablePdf, setEnablePdf, getEnableCleanup, setEnableCleanup, getCleanupStartDate, setCleanupStartDate, getCleanupEndDate, setCleanupEndDate, getLogUserIps, setLogUserIps, getHashUserIps, setHashUserIps } from "./inc/settings.mjs";
|
import { getManualApproval, setManualApproval, getMinTags, setMinTags, getRegistrationOpen, setRegistrationOpen, getTrustedUploads, setTrustedUploads, getBypassDuplicateCheck, setBypassDuplicateCheck, getProtectFiles, setProtectFiles, getPrivateMessages, setPrivateMessages, getDefaultLayout, setDefaultLayout, getEnablePdf, setEnablePdf, getEnableCleanup, setEnableCleanup, getCleanupStartDate, setCleanupStartDate, getCleanupEndDate, setCleanupEndDate, getLogUserIps, setLogUserIps, getHashUserIps, setHashUserIps, getShitpostMode, setShitpostMode } from "./inc/settings.mjs";
|
||||||
import { updateHallsCache, getHalls } from "./inc/halls_cache.mjs";
|
import { updateHallsCache, getHalls } from "./inc/halls_cache.mjs";
|
||||||
import { createI18n } from "./inc/i18n.mjs";
|
import { createI18n } from "./inc/i18n.mjs";
|
||||||
import security from "./inc/security.mjs";
|
import security from "./inc/security.mjs";
|
||||||
@@ -660,6 +660,7 @@ process.on('uncaughtException', err => {
|
|||||||
console.warn(`[BOOT] Trusted Uploads fetch failed:`, e.message);
|
console.warn(`[BOOT] Trusted Uploads fetch failed:`, e.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Set enable_pdf from config (pure config setting)
|
// Set enable_pdf from config (pure config setting)
|
||||||
setEnablePdf(!!cfg.enable_pdf);
|
setEnablePdf(!!cfg.enable_pdf);
|
||||||
console.log(`[BOOT] Enable PDF setting: ${getEnablePdf()}`);
|
console.log(`[BOOT] Enable PDF setting: ${getEnablePdf()}`);
|
||||||
@@ -766,6 +767,7 @@ process.on('uncaughtException', err => {
|
|||||||
get registration_open() { return getRegistrationOpen(); },
|
get registration_open() { return getRegistrationOpen(); },
|
||||||
registration_web_toggle_enabled: cfg.websrv.open_registration_web_toggle !== false,
|
registration_web_toggle_enabled: cfg.websrv.open_registration_web_toggle !== false,
|
||||||
get trusted_uploads() { return getTrustedUploads(); },
|
get trusted_uploads() { return getTrustedUploads(); },
|
||||||
|
get shitpost_mode() { return getShitpostMode(); },
|
||||||
get about_text() { return getAboutText(); },
|
get about_text() { return getAboutText(); },
|
||||||
get rules_text() { return getRulesText(); },
|
get rules_text() { return getRulesText(); },
|
||||||
get terms_text() { return getTermsText(); },
|
get terms_text() { return getTermsText(); },
|
||||||
|
|||||||
@@ -79,21 +79,27 @@ export const handleUpload = async (req, res, self) => {
|
|||||||
|
|
||||||
const is_oc = (parts.is_oc === 'true' || parts.is_oc === '1');
|
const is_oc = (parts.is_oc === 'true' || parts.is_oc === '1');
|
||||||
|
|
||||||
|
const is_shitpost = (parts.is_shitpost === 'true' || parts.is_shitpost === '1');
|
||||||
|
|
||||||
if (!file || !file.data) {
|
if (!file || !file.data) {
|
||||||
return sendJson(res, { success: false, msg: 'No file provided' }, 400);
|
return sendJson(res, { success: false, msg: 'No file provided' }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!rating || !['sfw', 'nsfw', 'nsfl'].includes(rating)) {
|
// 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);
|
||||||
|
|
||||||
|
if (!is_shitpost && !effectiveRating) {
|
||||||
return sendJson(res, { success: false, msg: 'Rating (sfw/nsfw/nsfl) is required' }, 400);
|
return sendJson(res, { success: false, msg: 'Rating (sfw/nsfw/nsfl) is required' }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rating === 'nsfl' && !cfg.enable_nsfl) {
|
if (effectiveRating === 'nsfl' && !cfg.enable_nsfl) {
|
||||||
return sendJson(res, { success: false, msg: 'NSFL mode is currently disabled' }, 400);
|
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 tags = tagsRaw ? tagsRaw.split(',').map(t => t.trim()).filter(t => t.length > 0 && !['sfw', 'nsfw', 'nsfl'].includes(t.toLowerCase())) : [];
|
||||||
const minTags = getMinTags();
|
const minTags = getMinTags();
|
||||||
if (tags.length < minTags) {
|
// In shitpost mode, tags are optional — items without tags enter as untagged
|
||||||
|
if (!is_shitpost && tags.length < minTags) {
|
||||||
return sendJson(res, { success: false, msg: `At least ${minTags} tags are required` }, 400);
|
return sendJson(res, { success: false, msg: `At least ${minTags} tags are required` }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,7 +369,7 @@ export const handleUpload = async (req, res, self) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate blurred thumbnail for NSFW/NSFL
|
// Generate blurred thumbnail for NSFW/NSFL
|
||||||
if (rating === 'nsfw' || rating === 'nsfl') {
|
if (effectiveRating === 'nsfw' || effectiveRating === 'nsfl') {
|
||||||
await queue.genBlurredThumbnail(itemid, isPending);
|
await queue.genBlurredThumbnail(itemid, isPending);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,11 +388,13 @@ export const handleUpload = async (req, res, self) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tags
|
// Tags — rating tag only assigned if a rating was selected
|
||||||
const ratingTagId = rating === 'sfw' ? 1 : (rating === 'nsfw' ? 2 : (cfg.nsfl_tag_id || 3));
|
if (effectiveRating) {
|
||||||
|
const ratingTagId = effectiveRating === 'sfw' ? 1 : (effectiveRating === 'nsfw' ? 2 : (cfg.nsfl_tag_id || 3));
|
||||||
await db`
|
await db`
|
||||||
insert into tags_assign ${db({ item_id: itemid, tag_id: ratingTagId, user_id: req.session.id })}
|
insert into tags_assign ${db({ item_id: itemid, tag_id: ratingTagId, user_id: req.session.id })}
|
||||||
`;
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
for (const tagName of tags) {
|
for (const tagName of tags) {
|
||||||
let tagRow = await db`
|
let tagRow = await db`
|
||||||
@@ -476,7 +484,7 @@ export const handleUpload = async (req, res, self) => {
|
|||||||
if (actualMime.startsWith('audio')) {
|
if (actualMime.startsWith('audio')) {
|
||||||
await moveSafe(path.join(cfg.paths.pending, 'ca', `${itemid}.webp`), coverDest);
|
await moveSafe(path.join(cfg.paths.pending, 'ca', `${itemid}.webp`), coverDest);
|
||||||
}
|
}
|
||||||
if (rating === 'nsfw' || rating === 'nsfl') {
|
if (effectiveRating === 'nsfw' || effectiveRating === 'nsfl') {
|
||||||
await moveSafe(path.join(cfg.paths.pending, 't', `${itemid}_blur.webp`), blurDest);
|
await moveSafe(path.join(cfg.paths.pending, 't', `${itemid}_blur.webp`), blurDest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -508,7 +516,7 @@ export const handleUpload = async (req, res, self) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ensure blurred thumbnail exists if needed
|
// Ensure blurred thumbnail exists if needed
|
||||||
if (rating === 'nsfw' || rating === 'nsfl') {
|
if (effectiveRating === 'nsfw' || effectiveRating === 'nsfl') {
|
||||||
const tDir = isPending ? path.join(cfg.paths.pending, 't') : cfg.paths.t;
|
const tDir = isPending ? path.join(cfg.paths.pending, 't') : cfg.paths.t;
|
||||||
const blurPath = path.join(tDir, `${itemid}_blur.webp`);
|
const blurPath = path.join(tDir, `${itemid}_blur.webp`);
|
||||||
const blurExists = await fs.access(blurPath).then(() => true).catch(() => false);
|
const blurExists = await fs.access(blurPath).then(() => true).catch(() => false);
|
||||||
@@ -555,7 +563,7 @@ export const handleUpload = async (req, res, self) => {
|
|||||||
mime: actualMime,
|
mime: actualMime,
|
||||||
username: req.session.user,
|
username: req.session.user,
|
||||||
display_name: req.session.display_name || null,
|
display_name: req.session.display_name || null,
|
||||||
tag_id: rating === 'sfw' ? 1 : (rating === 'nsfw' ? 2 : (cfg.nsfl_tag_id || 3)),
|
tag_id: effectiveRating ? (effectiveRating === 'sfw' ? 1 : (effectiveRating === 'nsfw' ? 2 : (cfg.nsfl_tag_id || 3))) : 0,
|
||||||
is_oc: !!is_oc
|
is_oc: !!is_oc
|
||||||
})})`;
|
})})`;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -59,6 +59,7 @@
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
|
||||||
<div class="settings-item" style="background: rgba(0,0,0,0.2); padding: 15px; border-radius: 4px; display: flex; align-items: center; justify-content: space-between; margin-top: 10px;">
|
<div class="settings-item" style="background: rgba(0,0,0,0.2); padding: 15px; border-radius: 4px; display: flex; align-items: center; justify-content: space-between; margin-top: 10px;">
|
||||||
<div>
|
<div>
|
||||||
<label style="display: block; font-weight: bold; color: var(--accent);">Minimum Tags</label>
|
<label style="display: block; font-weight: bold; color: var(--accent);">Minimum Tags</label>
|
||||||
|
|||||||
@@ -411,6 +411,10 @@
|
|||||||
uploading: "{{ t('upload.uploading') }}",
|
uploading: "{{ t('upload.uploading') }}",
|
||||||
processing: "{{ t('toast.processing') }}",
|
processing: "{{ t('toast.processing') }}",
|
||||||
upload_await_approval: "{{ t('upload.pending_approval_patient') }}",
|
upload_await_approval: "{{ t('upload.pending_approval_patient') }}",
|
||||||
|
upload_shitpost_success: "{{ t('upload.shitpost_success') || 'Successfully shitposted {n} items!' }}",
|
||||||
|
upload_shitposting_status: "{{ t('upload.shitposting_status') || 'Shitposting' }}",
|
||||||
|
upload_comment_placeholder: "{{ t('upload.item_comment_placeholder') || 'Comment (optional)...' }}",
|
||||||
|
upload_tags_placeholder: "{{ t('upload.item_tags_placeholder') || 'Tags...' }}",
|
||||||
// timeago
|
// timeago
|
||||||
timeago_just_now: "{{ t('timeago.just_now') }}",
|
timeago_just_now: "{{ t('timeago.just_now') }}",
|
||||||
timeago_year: "{{ t('timeago.year') }}",
|
timeago_year: "{{ t('timeago.year') }}",
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
@endif
|
@endif
|
||||||
<link rel="stylesheet" href="/s/css/upload.css?v={{ ts }}">
|
<link rel="stylesheet" href="/s/css/upload.css?v={{ ts }}">
|
||||||
@endif
|
@endif
|
||||||
<script>window.f0ckThemes = {{ themes_json }}; window.f0ckDefaultTheme = "{{ default_theme }}"; window.f0ckDomain = "{{ domain }}"; window.f0ckGitHash = "{{ git_hash }}"; window.f0ckAllowedImages = {{ allowed_comment_images_json }}; window.f0ckEmbedYoutubeInComments = {{ embed_youtube_in_comments ? 'true' : 'false' }}; window.f0ckEnableYoutubeUpload = {{ enable_youtube_upload ? 'true' : 'false' }}; window.f0ckBrandImages = {{ custom_brand_images_json }}; window.f0ckMediaBase = "{{ paths_images }}";</script>
|
<script>window.f0ckThemes = {{ themes_json }}; window.f0ckDefaultTheme = "{{ default_theme }}"; window.f0ckDomain = "{{ domain }}"; window.f0ckGitHash = "{{ git_hash }}"; window.f0ckAllowedImages = {{ allowed_comment_images_json }}; window.f0ckEmbedYoutubeInComments = {{ embed_youtube_in_comments ? 'true' : 'false' }}; window.f0ckEnableYoutubeUpload = {{ enable_youtube_upload ? 'true' : 'false' }}; window.f0ckBrandImages = {{ custom_brand_images_json }}; window.f0ckMediaBase = "{{ paths_images }}"; window.f0ckShitpostMode = {{ shitpost_mode ? 'true' : 'false' }};</script>
|
||||||
@if(!private_society || session)
|
@if(!private_society || session)
|
||||||
<script src="/s/js/marked.min.js" defer></script>
|
<script src="/s/js/marked.min.js" defer></script>
|
||||||
<script src="/s/js/comments.js?v={{ ts }}" defer></script>
|
<script src="/s/js/comments.js?v={{ ts }}" defer></script>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<form id="upload-form" class="upload-form" enctype="multipart/form-data" data-mimes='{!! mimes_json !!}' data-max-bytes="{{ max_file_size_bytes }}" data-min-tags="{{ min_tags }}">
|
<form id="upload-form" class="upload-form {{ shitpost_mode ? 'shitpost-mode-active' : '' }}" enctype="multipart/form-data" data-mimes='{!! mimes_json !!}' data-max-bytes="{{ max_file_size_bytes }}" data-min-tags="{{ min_tags }}">
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
@if(web_url_upload)
|
@if(web_url_upload)
|
||||||
<div class="upload-mode-tabs">
|
<div class="upload-mode-tabs">
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
<!-- File input area -->
|
<!-- File input area -->
|
||||||
<div class="upload-mode-content" id="mode-file">
|
<div class="upload-mode-content" id="mode-file">
|
||||||
<div class="drop-zone" id="upload-form-drop-zone">
|
<div class="drop-zone" id="upload-form-drop-zone">
|
||||||
<input type="file" class="file-input" name="file" accept="{{ allowed_mimes }}">
|
<input type="file" class="file-input" name="file" accept="{{ allowed_mimes }}" {{ shitpost_mode ? 'multiple' : '' }}>
|
||||||
<div class="drop-zone-prompt">
|
<div class="drop-zone-prompt">
|
||||||
<p style="font-size: 1.1rem; font-weight: 500;">{{ t('upload.drop_here') }}</p>
|
<p style="font-size: 1.1rem; font-weight: 500;">{{ t('upload.drop_here') }}</p>
|
||||||
<p style="font-size: 0.9rem; opacity: 0.6;">(max {{ max_file_size }})@if(session.admin) <span style="color: var(--accent);">{{ t('upload.admin_boost') }}</span>@endif</p>
|
<p style="font-size: 0.9rem; opacity: 0.6;">(max {{ max_file_size }})@if(session.admin) <span style="color: var(--accent);">{{ t('upload.admin_boost') }}</span>@endif</p>
|
||||||
@@ -39,7 +39,16 @@
|
|||||||
<!-- URL input area -->
|
<!-- URL input area -->
|
||||||
<div class="upload-mode-content" id="mode-url" style="display: none;">
|
<div class="upload-mode-content" id="mode-url" style="display: none;">
|
||||||
<div class="url-input-container">
|
<div class="url-input-container">
|
||||||
|
@if(shitpost_mode)
|
||||||
|
<div class="url-shitpost-container">
|
||||||
|
<textarea id="url-upload-input" name="url" placeholder="{{ t('upload.url_placeholder_shitpost') || 'Paste multiple URLs here (one per line)...' }}" autocomplete="off" style="width: 100%; min-height: 100px; background: rgba(0,0,0,0.2); border: 1px solid rgba(255,255,255,0.1); color: #fff; padding: 10px; border-radius: 4px; resize: vertical;"></textarea>
|
||||||
|
<button type="button" class="btn-add-urls">
|
||||||
|
<i class="fa-solid fa-plus-circle"></i> {{ t('upload.btn_add_urls') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
<input type="url" id="url-upload-input" name="url" placeholder="@if(enable_youtube_upload){{ t('upload.url_placeholder_yt') }}@else {{ t('upload.url_placeholder') }}@endif" autocomplete="off">
|
<input type="url" id="url-upload-input" name="url" placeholder="@if(enable_youtube_upload){{ t('upload.url_placeholder_yt') }}@else {{ t('upload.url_placeholder') }}@endif" autocomplete="off">
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
<div class="url-type-badge" id="url-type-badge" style="display: none;"></div>
|
<div class="url-type-badge" id="url-type-badge" style="display: none;"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -55,7 +64,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-section">
|
<div class="form-section global-rating-section">
|
||||||
<label>{{ t('upload.rating') }} <span class="required">*</span></label>
|
<label>{{ t('upload.rating') }} <span class="required">*</span></label>
|
||||||
<div class="rating-options">
|
<div class="rating-options">
|
||||||
<label class="rating-option">
|
<label class="rating-option">
|
||||||
@@ -75,14 +84,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-section">
|
|
||||||
<div class="oc-option">
|
|
||||||
<input type="checkbox" name="is_oc" id="upload-oc-checkbox">
|
|
||||||
<span class="oc-label">{{ t('upload.original_content') }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-section">
|
<div class="form-section global-tag-section">
|
||||||
<label>
|
<label>
|
||||||
{{ t('upload.tags') }}
|
{{ t('upload.tags') }}
|
||||||
@if(min_tags > 0)
|
@if(min_tags > 0)
|
||||||
@@ -109,7 +112,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-section">
|
<div class="form-section global-comment-section">
|
||||||
<label>{{ t('upload.comment') }} <span style="opacity: 0.5; font-weight: normal;">{{ t('upload.comment_optional') }}</span></label>
|
<label>{{ t('upload.comment') }} <span style="opacity: 0.5; font-weight: normal;">{{ t('upload.comment_optional') }}</span></label>
|
||||||
<div class="upload-comment-input comment-input">
|
<div class="upload-comment-input comment-input">
|
||||||
<textarea class="upload-comment" placeholder="{{ t('upload.comment_placeholder') }}" maxlength="2000"></textarea>
|
<textarea class="upload-comment" placeholder="{{ t('upload.comment_placeholder') }}" maxlength="2000"></textarea>
|
||||||
|
|||||||
Reference in New Issue
Block a user