add item titles
This commit is contained in:
@@ -905,7 +905,8 @@ CREATE TABLE public.items (
|
|||||||
is_pinned boolean DEFAULT false,
|
is_pinned boolean DEFAULT false,
|
||||||
is_oc boolean DEFAULT false,
|
is_oc boolean DEFAULT false,
|
||||||
xd_score integer DEFAULT 0 NOT NULL,
|
xd_score integer DEFAULT 0 NOT NULL,
|
||||||
original_filename text
|
original_filename text,
|
||||||
|
title text
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1725,7 +1725,7 @@ body.sidebar-right-hidden .global-sidebar-right {
|
|||||||
grid-column: 2;
|
grid-column: 2;
|
||||||
height: calc(100vh - var(--navbar-h, 50px));
|
height: calc(100vh - var(--navbar-h, 50px));
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto auto 1fr;
|
grid-template-rows: auto auto auto 1fr;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
@@ -1754,6 +1754,10 @@ body.sidebar-right-hidden .global-sidebar-right {
|
|||||||
align-self: start;
|
align-self: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.layout-modern .item-layout-container .item-main-content .item_title {
|
||||||
|
align-self: start;
|
||||||
|
}
|
||||||
|
|
||||||
body.layout-modern .item-layout-container .item-main-content .content {
|
body.layout-modern .item-layout-container .item-main-content .content {
|
||||||
align-self: start;
|
align-self: start;
|
||||||
}
|
}
|
||||||
@@ -2165,6 +2169,7 @@ body.sidebar-right-hidden .global-sidebar-right {
|
|||||||
|
|
||||||
/* Common sizing for the Legacy content area */
|
/* Common sizing for the Legacy content area */
|
||||||
.item-layout-container .item-main-content>._204863,
|
.item-layout-container .item-main-content>._204863,
|
||||||
|
.item-layout-container .item-main-content>.item_title,
|
||||||
.item-layout-container .item-main-content>.content,
|
.item-layout-container .item-main-content>.content,
|
||||||
.item-layout-container .item-main-content>.metadata,
|
.item-layout-container .item-main-content>.metadata,
|
||||||
.item-layout-container .item-main-content #comments-container {
|
.item-layout-container .item-main-content #comments-container {
|
||||||
@@ -4258,6 +4263,24 @@ span.placeholder {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.item_title {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1100px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-top: none;
|
||||||
|
color: rgba(255, 255, 255, 0.72);
|
||||||
|
font-size: 0.82em;
|
||||||
|
font-family: inherit;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
line-height: 1.6;
|
||||||
|
box-sizing: border-box;
|
||||||
|
z-index: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.location {
|
.location {
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
}
|
}
|
||||||
@@ -14635,6 +14658,63 @@ body.scroller-active #gchat-reopen-bubble {
|
|||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Info Modal: Inline Title Editor ─────────────────────────────────────── */
|
||||||
|
|
||||||
|
.info-title-edit-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-title-input {
|
||||||
|
flex: 1;
|
||||||
|
background: rgba(255, 255, 255, 0.07);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
color: var(--white, #eee);
|
||||||
|
padding: 5px 8px;
|
||||||
|
font-size: 0.88em;
|
||||||
|
font-family: inherit;
|
||||||
|
outline: none;
|
||||||
|
border-radius: 0;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-title-input:focus {
|
||||||
|
border-color: var(--accent, #9f0);
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-title-save-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
background: var(--accent, #9f0);
|
||||||
|
border: none;
|
||||||
|
color: #111;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-title-save-btn:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-title-save-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: wait;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-title-status {
|
||||||
|
font-size: 0.8em;
|
||||||
|
margin-top: 3px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
/* =============================================
|
/* =============================================
|
||||||
NSFW / NSFL PREMIUM ZERO-LAG BACKGROUND BLUR
|
NSFW / NSFL PREMIUM ZERO-LAG BACKGROUND BLUR
|
||||||
============================================= */
|
============================================= */
|
||||||
|
|||||||
@@ -551,6 +551,34 @@
|
|||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.item-title-container {
|
||||||
|
margin-top: 6px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-title-input {
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(0,0,0,0.3);
|
||||||
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-family: inherit;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-title-input::placeholder {
|
||||||
|
color: rgba(255,255,255,0.3);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-title-input:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
.rating-options {
|
.rating-options {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
@@ -678,6 +706,29 @@
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Global title input (normal mode) — matches tag-input-container style */
|
||||||
|
.upload-title-input {
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 0.5rem;
|
||||||
|
color: inherit;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-title-input:focus {
|
||||||
|
border-color: var(--accent, #7c5cbf);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-title-input::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
.tag-count {
|
.tag-count {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
|
|||||||
@@ -8991,6 +8991,81 @@ if (navigator.vibrate) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Title save button
|
||||||
|
const saveBtn = e.target.closest('#info-title-save');
|
||||||
|
if (saveBtn) {
|
||||||
|
e.preventDefault();
|
||||||
|
const input = document.getElementById('info-title-input');
|
||||||
|
const status = document.getElementById('info-title-status');
|
||||||
|
if (!input) return;
|
||||||
|
|
||||||
|
const itemId = input.dataset.itemId;
|
||||||
|
const newTitle = input.value.trim();
|
||||||
|
const csrf = window.f0ckSession?.csrf_token;
|
||||||
|
|
||||||
|
saveBtn.disabled = true;
|
||||||
|
const origIcon = saveBtn.innerHTML;
|
||||||
|
saveBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i>';
|
||||||
|
|
||||||
|
fetch(`/api/v2/items/${itemId}/title`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': csrf
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ title: newTitle })
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
saveBtn.disabled = false;
|
||||||
|
saveBtn.innerHTML = origIcon;
|
||||||
|
if (data.success) {
|
||||||
|
// Update the live .item_title bar below the ID bar
|
||||||
|
let titleBar = document.querySelector('.item_title');
|
||||||
|
if (data.title) {
|
||||||
|
if (titleBar) {
|
||||||
|
titleBar.textContent = data.title;
|
||||||
|
titleBar.style.display = '';
|
||||||
|
} else {
|
||||||
|
// Create it if it doesn't exist yet
|
||||||
|
const idBar = document.querySelector('.item-main-content > ._204863');
|
||||||
|
if (idBar) {
|
||||||
|
titleBar = document.createElement('div');
|
||||||
|
titleBar.className = 'item_title';
|
||||||
|
titleBar.textContent = data.title;
|
||||||
|
idBar.insertAdjacentElement('afterend', titleBar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Title cleared
|
||||||
|
if (titleBar) titleBar.remove();
|
||||||
|
}
|
||||||
|
if (status) {
|
||||||
|
status.textContent = '✓ Saved';
|
||||||
|
status.style.color = 'var(--accent, #5cb85c)';
|
||||||
|
status.style.display = 'inline';
|
||||||
|
setTimeout(() => { status.style.display = 'none'; }, 2000);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (status) {
|
||||||
|
status.textContent = data.msg || 'Error saving title';
|
||||||
|
status.style.color = '#e84040';
|
||||||
|
status.style.display = 'inline';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
saveBtn.disabled = false;
|
||||||
|
saveBtn.innerHTML = origIcon;
|
||||||
|
if (status) {
|
||||||
|
status.textContent = 'Network error';
|
||||||
|
status.style.color = '#e84040';
|
||||||
|
status.style.display = 'inline';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Close when clicking outside modal content
|
// Close when clicking outside modal content
|
||||||
const infoModal = document.getElementById('info-modal');
|
const infoModal = document.getElementById('info-modal');
|
||||||
if (infoModal && e.target === infoModal) {
|
if (infoModal && e.target === infoModal) {
|
||||||
|
|||||||
@@ -590,7 +590,7 @@ window.initUploadForm = (selector) => {
|
|||||||
}
|
}
|
||||||
lines.forEach(url => {
|
lines.forEach(url => {
|
||||||
if (!selectedFiles.some(item => item.type === 'url' && item.url === url)) {
|
if (!selectedFiles.some(item => item.type === 'url' && item.url === url)) {
|
||||||
selectedFiles.push({ type: 'url', url, rating: '', tags: [], comment: '', is_oc: false });
|
selectedFiles.push({ type: 'url', url, rating: '', tags: [], comment: '', title: '', is_oc: false });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
urlInput.value = '';
|
urlInput.value = '';
|
||||||
@@ -613,7 +613,7 @@ window.initUploadForm = (selector) => {
|
|||||||
const val = urlInput.value.trim();
|
const val = urlInput.value.trim();
|
||||||
if (!val || !/^https?:\/\//i.test(val)) return;
|
if (!val || !/^https?:\/\//i.test(val)) return;
|
||||||
if (!selectedFiles.some(item => item.type === 'url' && item.url === val)) {
|
if (!selectedFiles.some(item => item.type === 'url' && item.url === val)) {
|
||||||
selectedFiles.push({ type: 'url', url: val, rating: '', tags: [], comment: '', is_oc: false });
|
selectedFiles.push({ type: 'url', url: val, rating: '', tags: [], comment: '', title: '', is_oc: false });
|
||||||
}
|
}
|
||||||
urlInput.value = '';
|
urlInput.value = '';
|
||||||
if (urlBadge) urlBadge.style.display = 'none';
|
if (urlBadge) urlBadge.style.display = 'none';
|
||||||
@@ -812,7 +812,7 @@ window.initUploadForm = (selector) => {
|
|||||||
|
|
||||||
if (!selectedFiles.some(f => (f.file || f).name === file.name && (f.file || f).size === file.size)) {
|
if (!selectedFiles.some(f => (f.file || f).name === file.name && (f.file || f).size === file.size)) {
|
||||||
if (isShitpost) {
|
if (isShitpost) {
|
||||||
selectedFiles.push({ type: 'file', file: file, rating: '', tags: [], comment: '', is_oc: false });
|
selectedFiles.push({ type: 'file', file: file, rating: '', tags: [], comment: '', title: '', is_oc: false });
|
||||||
} else {
|
} else {
|
||||||
selectedFiles.push(file); // Legacy single file mode uses raw File
|
selectedFiles.push(file); // Legacy single file mode uses raw File
|
||||||
}
|
}
|
||||||
@@ -978,6 +978,7 @@ window.initUploadForm = (selector) => {
|
|||||||
let tagsUI = '';
|
let tagsUI = '';
|
||||||
let ocUI = '';
|
let ocUI = '';
|
||||||
let commentUI = '';
|
let commentUI = '';
|
||||||
|
let titleUI = '';
|
||||||
if (isShitpost) {
|
if (isShitpost) {
|
||||||
const nsflEnabled = !!form.querySelector('input[name="rating"][value="nsfl"]');
|
const nsflEnabled = !!form.querySelector('input[name="rating"][value="nsfl"]');
|
||||||
// Build per-item rating HTML
|
// Build per-item rating HTML
|
||||||
@@ -1012,8 +1013,13 @@ window.initUploadForm = (selector) => {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
titleUI = `
|
||||||
|
<div class="item-title-container">
|
||||||
|
<input type="text" class="item-title-input" placeholder="Add Title..." maxlength="500" value="${window.escapeHtmlUpload(item.title || '')}">
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
const commentPlaceholder = window.f0ckI18n?.upload_comment_placeholder || 'Comment (optional)...';
|
const commentPlaceholder = window.f0ckI18n?.upload_comment_placeholder || 'AddComment...';
|
||||||
const maxLenHtml = (commentMaxLen !== null && !isNaN(commentMaxLen)) ? ` maxlength="${commentMaxLen}"` : '';
|
const maxLenHtml = (commentMaxLen !== null && !isNaN(commentMaxLen)) ? ` maxlength="${commentMaxLen}"` : '';
|
||||||
commentUI = `
|
commentUI = `
|
||||||
<div class="item-comment-container">
|
<div class="item-comment-container">
|
||||||
@@ -1035,6 +1041,7 @@ window.initUploadForm = (selector) => {
|
|||||||
<span class="file-name-small" title="${window.escapeHtmlUpload(fileNameStr)}">${window.escapeHtmlUpload(fileNameStr)}</span>
|
<span class="file-name-small" title="${window.escapeHtmlUpload(fileNameStr)}">${window.escapeHtmlUpload(fileNameStr)}</span>
|
||||||
<span class="file-size-small">${fileSizeStr}</span>
|
<span class="file-size-small">${fileSizeStr}</span>
|
||||||
</div>
|
</div>
|
||||||
|
${titleUI}
|
||||||
${ratingSwitch}
|
${ratingSwitch}
|
||||||
${tagsUI}
|
${tagsUI}
|
||||||
${commentUI}
|
${commentUI}
|
||||||
@@ -1057,6 +1064,12 @@ window.initUploadForm = (selector) => {
|
|||||||
if (emojiTrigger) setupItemEmojiPicker(commentInput, emojiTrigger);
|
if (emojiTrigger) setupItemEmojiPicker(commentInput, emojiTrigger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle Title
|
||||||
|
const titleInput = infoRow.querySelector('.item-title-input');
|
||||||
|
if (titleInput) {
|
||||||
|
titleInput.oninput = () => { item.title = titleInput.value.trim(); };
|
||||||
|
}
|
||||||
|
|
||||||
// Handle Tags
|
// Handle Tags
|
||||||
const tagList = infoRow.querySelector('.item-tags-list');
|
const tagList = infoRow.querySelector('.item-tags-list');
|
||||||
const tagInput = infoRow.querySelector('.item-tag-input');
|
const tagInput = infoRow.querySelector('.item-tag-input');
|
||||||
@@ -1794,6 +1807,15 @@ window.initUploadForm = (selector) => {
|
|||||||
window.f0ckInitTagAutocomplete(tagInput, tagSuggestions, addTag, () => tags);
|
window.f0ckInitTagAutocomplete(tagInput, tagSuggestions, addTag, () => tags);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prevent Enter in the title input from submitting the form and
|
||||||
|
// accidentally flushing whatever is currently typed in the tag input as a tag.
|
||||||
|
const titleInputEl = form.querySelector('.upload-title-input');
|
||||||
|
if (titleInputEl) {
|
||||||
|
titleInputEl.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter') e.preventDefault();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
form.querySelectorAll('input[name="rating"]').forEach(radio => {
|
form.querySelectorAll('input[name="rating"]').forEach(radio => {
|
||||||
radio.addEventListener('change', updateSubmitButton);
|
radio.addEventListener('change', updateSubmitButton);
|
||||||
});
|
});
|
||||||
@@ -1831,6 +1853,7 @@ window.initUploadForm = (selector) => {
|
|||||||
const dragModal = form.closest('#upload-drag-modal');
|
const dragModal = form.closest('#upload-drag-modal');
|
||||||
const comment = form.querySelector('.upload-comment')?.value.trim() || '';
|
const comment = form.querySelector('.upload-comment')?.value.trim() || '';
|
||||||
const isOc = form.querySelector('#upload-oc-checkbox')?.checked || false;
|
const isOc = form.querySelector('#upload-oc-checkbox')?.checked || false;
|
||||||
|
const titleVal = form.querySelector('.upload-title-input')?.value.trim() || '';
|
||||||
|
|
||||||
const setBtnLoading = (text) => {
|
const setBtnLoading = (text) => {
|
||||||
isUploading = true;
|
isUploading = true;
|
||||||
@@ -1893,7 +1916,8 @@ window.initUploadForm = (selector) => {
|
|||||||
rating: globalRatingEl.value,
|
rating: globalRatingEl.value,
|
||||||
tags: tags.join(','),
|
tags: tags.join(','),
|
||||||
comment: comment,
|
comment: comment,
|
||||||
is_oc: isOc
|
is_oc: isOc,
|
||||||
|
title: titleVal || undefined
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1980,6 +2004,7 @@ window.initUploadForm = (selector) => {
|
|||||||
const fileRating = isShitpost ? item.rating : (globalRatingEl ? globalRatingEl.value : 'sfw');
|
const fileRating = isShitpost ? item.rating : (globalRatingEl ? globalRatingEl.value : 'sfw');
|
||||||
const fileTags = isShitpost ? item.tags : tags;
|
const fileTags = isShitpost ? item.tags : tags;
|
||||||
const fileComment = isShitpost ? item.comment : comment;
|
const fileComment = isShitpost ? item.comment : comment;
|
||||||
|
const fileTitle = isShitpost ? (item.title || '') : titleVal;
|
||||||
|
|
||||||
if (isShitpost) {
|
if (isShitpost) {
|
||||||
const statusMsg = window.f0ckI18n?.upload_shitposting_status || 'Shitposting';
|
const statusMsg = window.f0ckI18n?.upload_shitposting_status || 'Shitposting';
|
||||||
@@ -1996,6 +2021,7 @@ window.initUploadForm = (selector) => {
|
|||||||
formData.append('tags', fileTags.join(','));
|
formData.append('tags', fileTags.join(','));
|
||||||
formData.append('is_oc', (isShitpost ? item.is_oc : isOc) ? 'true' : 'false');
|
formData.append('is_oc', (isShitpost ? item.is_oc : isOc) ? 'true' : 'false');
|
||||||
if (isShitpost) formData.append('is_shitpost', 'true');
|
if (isShitpost) formData.append('is_shitpost', 'true');
|
||||||
|
if (fileTitle) formData.append('title', fileTitle);
|
||||||
|
|
||||||
// Add custom thumbnail if provided (only for single SWF files)
|
// Add custom thumbnail if provided (only for single SWF files)
|
||||||
if (selectedFiles.length === 1 && thumbInput && thumbInput.files && thumbInput.files[0]) {
|
if (selectedFiles.length === 1 && thumbInput && thumbInput.files && thumbInput.files[0]) {
|
||||||
@@ -2046,7 +2072,8 @@ window.initUploadForm = (selector) => {
|
|||||||
tags: fileTags.join(','),
|
tags: fileTags.join(','),
|
||||||
is_oc: (isShitpost ? item.is_oc : isOc),
|
is_oc: (isShitpost ? item.is_oc : isOc),
|
||||||
comment: fileComment,
|
comment: fileComment,
|
||||||
is_shitpost: isShitpost ? true : undefined
|
is_shitpost: isShitpost ? true : undefined,
|
||||||
|
title: fileTitle || undefined
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
xhr.send(formData);
|
xhr.send(formData);
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
"cancel_upload": "Cancel Upload",
|
"cancel_upload": "Cancel Upload",
|
||||||
"shitpost_success": "Successfully shitposted {n} items!",
|
"shitpost_success": "Successfully shitposted {n} items!",
|
||||||
"shitposting_status": "Uploading",
|
"shitposting_status": "Uploading",
|
||||||
"item_comment_placeholder": "Comment (optional)...",
|
"item_comment_placeholder": "Write a Comment...",
|
||||||
"item_tags_placeholder": "Tags...",
|
"item_tags_placeholder": "Tags...",
|
||||||
"btn_add_urls": "Add URL(s)",
|
"btn_add_urls": "Add URL(s)",
|
||||||
"tags_required_shitpost": "All items need tags",
|
"tags_required_shitpost": "All items need tags",
|
||||||
|
|||||||
@@ -657,6 +657,7 @@ export default {
|
|||||||
author_avatar: actitem.author_avatar,
|
author_avatar: actitem.author_avatar,
|
||||||
author_avatar_file: actitem.author_avatar_file,
|
author_avatar_file: actitem.author_avatar_file,
|
||||||
author_description: actitem.author_description,
|
author_description: actitem.author_description,
|
||||||
|
title: actitem.title || null,
|
||||||
|
|
||||||
src: {
|
src: {
|
||||||
long: actitem.src,
|
long: actitem.src,
|
||||||
|
|||||||
@@ -817,6 +817,33 @@ export default router => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// PATCH /api/v2/items/:id/title — set or clear the title for an item
|
||||||
|
// Allowed by: item owner, moderators, admins
|
||||||
|
group.patch(/\/items\/(?<id>\d+)\/title$/, lib.loggedin, async (req, res) => {
|
||||||
|
const id = +req.params.id;
|
||||||
|
if (!id) return res.json({ success: false, msg: 'Invalid item id' }, 400);
|
||||||
|
|
||||||
|
// Fetch item to check ownership
|
||||||
|
const rows = await db`SELECT id, username FROM items WHERE id = ${id} AND active = true LIMIT 1`;
|
||||||
|
if (!rows.length) return res.json({ success: false, msg: 'Item not found' }, 404);
|
||||||
|
|
||||||
|
const item = rows[0];
|
||||||
|
const isOwner = req.session.user === item.username;
|
||||||
|
const isMod = !!(req.session.is_moderator || req.session.admin);
|
||||||
|
if (!isOwner && !isMod) return res.json({ success: false, msg: 'Forbidden' }, 403);
|
||||||
|
|
||||||
|
// Accept title from JSON or URL-encoded body
|
||||||
|
let rawTitle = req.post?.title ?? req.body?.title ?? null;
|
||||||
|
if (rawTitle !== null) rawTitle = String(rawTitle).trim();
|
||||||
|
// Empty string → null (clears the title)
|
||||||
|
const title = (rawTitle === '' || rawTitle === null) ? null : rawTitle.substring(0, 500);
|
||||||
|
|
||||||
|
await db`UPDATE items SET title = ${title} WHERE id = ${id}`;
|
||||||
|
|
||||||
|
return res.json({ success: true, title });
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
group.post(/\/admin\/deletepost$/, lib.modAuth, async (req, res) => {
|
group.post(/\/admin\/deletepost$/, lib.modAuth, async (req, res) => {
|
||||||
if (req.post.postid === undefined || req.post.postid === null) {
|
if (req.post.postid === undefined || req.post.postid === null) {
|
||||||
return res.json({
|
return res.json({
|
||||||
|
|||||||
@@ -204,7 +204,8 @@ export default router => {
|
|||||||
return res.json({ success: false, msg: 'URL uploads are disabled' }, 403);
|
return res.json({ success: false, msg: 'URL uploads are disabled' }, 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { url: inputUrl, rating, tags: tagsRaw, comment, is_oc, is_shitpost } = req.post || {};
|
const { url: inputUrl, rating, tags: tagsRaw, comment, is_oc, is_shitpost, title: rawTitle } = req.post || {};
|
||||||
|
const title = (rawTitle && typeof rawTitle === 'string' && rawTitle.trim()) ? rawTitle.trim().substring(0, 500) : null;
|
||||||
|
|
||||||
const maxLen = cfg.main.comment_max_length;
|
const maxLen = cfg.main.comment_max_length;
|
||||||
if (comment && maxLen !== null && maxLen !== undefined && comment.length > maxLen) {
|
if (comment && maxLen !== null && maxLen !== undefined && comment.length > maxLen) {
|
||||||
@@ -280,8 +281,9 @@ export default router => {
|
|||||||
usernetwork: 'web',
|
usernetwork: 'web',
|
||||||
stamp: ~~(Date.now() / 1000),
|
stamp: ~~(Date.now() / 1000),
|
||||||
active: !isApprovalRequired,
|
active: !isApprovalRequired,
|
||||||
is_oc: !!is_oc
|
is_oc: !!is_oc,
|
||||||
}, 'src', 'dest', 'mime', 'size', 'checksum', 'phash', 'username', 'userchannel', 'usernetwork', 'stamp', 'active', 'is_oc')}
|
title: title
|
||||||
|
}, 'src', 'dest', 'mime', 'size', 'checksum', 'phash', 'username', 'userchannel', 'usernetwork', 'stamp', 'active', 'is_oc', 'title')}
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -564,8 +566,9 @@ export default router => {
|
|||||||
usernetwork: 'web',
|
usernetwork: 'web',
|
||||||
stamp: ~~(Date.now() / 1000),
|
stamp: ~~(Date.now() / 1000),
|
||||||
active: !isApprovalRequired,
|
active: !isApprovalRequired,
|
||||||
is_oc: !!is_oc
|
is_oc: !!is_oc,
|
||||||
}, 'src', 'dest', 'mime', 'size', 'checksum', 'phash', 'username', 'userchannel', 'usernetwork', 'stamp', 'active', 'is_oc')}
|
title: title
|
||||||
|
}, 'src', 'dest', 'mime', 'size', 'checksum', 'phash', 'username', 'userchannel', 'usernetwork', 'stamp', 'active', 'is_oc', 'title')}
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ const sendJson = (res, data, code = 200) => {
|
|||||||
// One-time migration: add original_filename column if it doesn't exist
|
// One-time migration: add original_filename column if it doesn't exist
|
||||||
db`ALTER TABLE items ADD COLUMN IF NOT EXISTS original_filename text`.catch(() => {});
|
db`ALTER TABLE items ADD COLUMN IF NOT EXISTS original_filename text`.catch(() => {});
|
||||||
|
|
||||||
|
// One-time migration: restore title column for backwards compatibility with old databases
|
||||||
|
db`ALTER TABLE items ADD COLUMN IF NOT EXISTS title text`.catch(() => {});
|
||||||
|
|
||||||
export const handleUpload = async (req, res, self) => {
|
export const handleUpload = async (req, res, self) => {
|
||||||
// Manual session lookup is required here because this handler is called from a
|
// Manual session lookup is required here because this handler is called from a
|
||||||
// bypass middleware that runs in parallel with the main session middleware.
|
// bypass middleware that runs in parallel with the main session middleware.
|
||||||
@@ -106,6 +109,8 @@ export const handleUpload = async (req, res, self) => {
|
|||||||
const rating = parts.rating;
|
const rating = parts.rating;
|
||||||
const tagsRaw = parts.tags;
|
const tagsRaw = parts.tags;
|
||||||
const comment = parts.comment ? parts.comment.trim() : '';
|
const comment = parts.comment ? parts.comment.trim() : '';
|
||||||
|
const rawTitle = parts.title ? parts.title.trim() : '';
|
||||||
|
const title = rawTitle.length > 0 ? rawTitle.substring(0, 500) : null;
|
||||||
|
|
||||||
const is_oc = (parts.is_oc === 'true' || parts.is_oc === '1');
|
const is_oc = (parts.is_oc === 'true' || parts.is_oc === '1');
|
||||||
|
|
||||||
@@ -354,8 +359,9 @@ export const handleUpload = async (req, res, self) => {
|
|||||||
stamp: ~~(Date.now() / 1000),
|
stamp: ~~(Date.now() / 1000),
|
||||||
active: !manualApproval,
|
active: !manualApproval,
|
||||||
is_oc: is_oc,
|
is_oc: is_oc,
|
||||||
original_filename: originalFilename
|
original_filename: originalFilename,
|
||||||
}, 'src', 'dest', 'mime', 'size', 'checksum', 'phash', 'username', 'userchannel', 'usernetwork', 'stamp', 'active', 'is_oc', 'original_filename')
|
title: title
|
||||||
|
}, 'src', 'dest', 'mime', 'size', 'checksum', 'phash', 'username', 'userchannel', 'usernetwork', 'stamp', 'active', 'is_oc', 'original_filename', 'title')
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
<div class="location">{{ link.main }}{{ item.id }}{{ link.suffix }}</div>
|
<div class="location">{{ link.main }}{{ item.id }}{{ link.suffix }}</div>
|
||||||
<div class="gapLeft"></div>
|
<div class="gapLeft"></div>
|
||||||
</div>
|
</div>
|
||||||
|
@if(item.title)
|
||||||
|
<div class="item_title">{{ item.title }}</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="previous-post">
|
<div class="previous-post">
|
||||||
@@ -198,6 +201,23 @@
|
|||||||
<div class="modal-content" style="max-width: 500px;">
|
<div class="modal-content" style="max-width: 500px;">
|
||||||
<div class="modal-body" style="padding: 20px 0; text-align: left;">
|
<div class="modal-body" style="padding: 20px 0; text-align: left;">
|
||||||
<table class="info-table">
|
<table class="info-table">
|
||||||
|
@if(can_manage_item)
|
||||||
|
<tr class="info-title-row">
|
||||||
|
<th>Title</th>
|
||||||
|
<td>
|
||||||
|
<div class="info-title-edit-wrap">
|
||||||
|
<input type="text" id="info-title-input" class="info-title-input" value="{{ item.title || '' }}" placeholder="Add a title…" maxlength="500" data-item-id="{{ item.id }}" />
|
||||||
|
<button id="info-title-save" class="info-title-save-btn"><i class="fa-solid fa-check"></i></button>
|
||||||
|
</div>
|
||||||
|
<span id="info-title-status" class="info-title-status" style="display:none"></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@elseif(item.title)
|
||||||
|
<tr>
|
||||||
|
<th>Title</th>
|
||||||
|
<td>{{ item.title }}</td>
|
||||||
|
</tr>
|
||||||
|
@endif
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ t('info_modal.file_size') || 'File Size' }}</th>
|
<th>{{ t('info_modal.file_size') || 'File Size' }}</th>
|
||||||
<td>{{ item.size }}</td>
|
<td>{{ item.size }}</td>
|
||||||
|
|||||||
@@ -60,6 +60,9 @@
|
|||||||
<div class="location">{{ link.main }}{{ item.id }}{{ link.suffix }}</div>
|
<div class="location">{{ link.main }}{{ item.id }}{{ link.suffix }}</div>
|
||||||
<div class="gapLeft"></div>
|
<div class="gapLeft"></div>
|
||||||
</div>
|
</div>
|
||||||
|
@if(item.title)
|
||||||
|
<div class="item_title">{{ item.title }}</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="previous-post">
|
<div class="previous-post">
|
||||||
@@ -172,6 +175,23 @@
|
|||||||
<div class="modal-content" style="max-width: 500px;">
|
<div class="modal-content" style="max-width: 500px;">
|
||||||
<div class="modal-body" style="padding: 20px 0; text-align: left;">
|
<div class="modal-body" style="padding: 20px 0; text-align: left;">
|
||||||
<table class="info-table">
|
<table class="info-table">
|
||||||
|
@if(can_manage_item)
|
||||||
|
<tr class="info-title-row">
|
||||||
|
<th>Title</th>
|
||||||
|
<td>
|
||||||
|
<div class="info-title-edit-wrap">
|
||||||
|
<input type="text" id="info-title-input" class="info-title-input" value="{{ item.title || '' }}" placeholder="Add a title…" maxlength="500" data-item-id="{{ item.id }}" />
|
||||||
|
<button id="info-title-save" class="info-title-save-btn"><i class="fa-solid fa-check"></i></button>
|
||||||
|
</div>
|
||||||
|
<span id="info-title-status" class="info-title-status" style="display:none"></span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@elseif(item.title)
|
||||||
|
<tr>
|
||||||
|
<th>Title</th>
|
||||||
|
<td>{{ item.title }}</td>
|
||||||
|
</tr>
|
||||||
|
@endif
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ t('info_modal.file_size') || 'File Size' }}</th>
|
<th>{{ t('info_modal.file_size') || 'File Size' }}</th>
|
||||||
<td>{{ item.size }}</td>
|
<td>{{ item.size }}</td>
|
||||||
|
|||||||
@@ -58,6 +58,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if(!shitpost_mode)
|
||||||
|
<div class="form-section global-title-section">
|
||||||
|
<label>Title <span style="opacity: 0.5; font-weight: normal;">{{ t('upload.comment_optional') }}</span></label>
|
||||||
|
<input type="text" name="title" class="upload-title-input" placeholder="Add a title for this item…" maxlength="500" autocomplete="off">
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
<div class="form-section global-rating-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">
|
||||||
|
|||||||
Reference in New Issue
Block a user