add item titles

This commit is contained in:
2026-05-24 23:02:49 +02:00
parent 187f35227b
commit 613f099a8b
13 changed files with 334 additions and 16 deletions

View File

@@ -1725,7 +1725,7 @@ body.sidebar-right-hidden .global-sidebar-right {
grid-column: 2;
height: calc(100vh - var(--navbar-h, 50px));
display: grid;
grid-template-rows: auto auto 1fr;
grid-template-rows: auto auto auto 1fr;
justify-items: center;
overflow: hidden;
padding: 20px;
@@ -1754,6 +1754,10 @@ body.sidebar-right-hidden .global-sidebar-right {
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 {
align-self: start;
}
@@ -2165,6 +2169,7 @@ body.sidebar-right-hidden .global-sidebar-right {
/* Common sizing for the Legacy content area */
.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>.metadata,
.item-layout-container .item-main-content #comments-container {
@@ -4258,6 +4263,24 @@ span.placeholder {
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 {
padding-left: 5px;
}
@@ -14635,6 +14658,63 @@ body.scroller-active #gchat-reopen-bubble {
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
============================================= */

View File

@@ -551,6 +551,34 @@
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 {
display: flex;
gap: 1rem;
@@ -678,6 +706,29 @@
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 {
font-weight: normal;
font-size: 0.85rem;

View File

@@ -8991,6 +8991,81 @@ if (navigator.vibrate) {
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
const infoModal = document.getElementById('info-modal');
if (infoModal && e.target === infoModal) {

View File

@@ -590,7 +590,7 @@ window.initUploadForm = (selector) => {
}
lines.forEach(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 = '';
@@ -613,7 +613,7 @@ window.initUploadForm = (selector) => {
const val = urlInput.value.trim();
if (!val || !/^https?:\/\//i.test(val)) return;
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 = '';
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 (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 {
selectedFiles.push(file); // Legacy single file mode uses raw File
}
@@ -978,6 +978,7 @@ window.initUploadForm = (selector) => {
let tagsUI = '';
let ocUI = '';
let commentUI = '';
let titleUI = '';
if (isShitpost) {
const nsflEnabled = !!form.querySelector('input[name="rating"][value="nsfl"]');
// Build per-item rating HTML
@@ -1012,8 +1013,13 @@ window.initUploadForm = (selector) => {
</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}"` : '';
commentUI = `
<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-size-small">${fileSizeStr}</span>
</div>
${titleUI}
${ratingSwitch}
${tagsUI}
${commentUI}
@@ -1057,6 +1064,12 @@ window.initUploadForm = (selector) => {
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
const tagList = infoRow.querySelector('.item-tags-list');
const tagInput = infoRow.querySelector('.item-tag-input');
@@ -1794,6 +1807,15 @@ window.initUploadForm = (selector) => {
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 => {
radio.addEventListener('change', updateSubmitButton);
});
@@ -1831,6 +1853,7 @@ window.initUploadForm = (selector) => {
const dragModal = form.closest('#upload-drag-modal');
const comment = form.querySelector('.upload-comment')?.value.trim() || '';
const isOc = form.querySelector('#upload-oc-checkbox')?.checked || false;
const titleVal = form.querySelector('.upload-title-input')?.value.trim() || '';
const setBtnLoading = (text) => {
isUploading = true;
@@ -1893,7 +1916,8 @@ window.initUploadForm = (selector) => {
rating: globalRatingEl.value,
tags: tags.join(','),
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 fileTags = isShitpost ? item.tags : tags;
const fileComment = isShitpost ? item.comment : comment;
const fileTitle = isShitpost ? (item.title || '') : titleVal;
if (isShitpost) {
const statusMsg = window.f0ckI18n?.upload_shitposting_status || 'Shitposting';
@@ -1996,6 +2021,7 @@ window.initUploadForm = (selector) => {
formData.append('tags', fileTags.join(','));
formData.append('is_oc', (isShitpost ? item.is_oc : isOc) ? 'true' : 'false');
if (isShitpost) formData.append('is_shitpost', 'true');
if (fileTitle) formData.append('title', fileTitle);
// Add custom thumbnail if provided (only for single SWF files)
if (selectedFiles.length === 1 && thumbInput && thumbInput.files && thumbInput.files[0]) {
@@ -2046,7 +2072,8 @@ window.initUploadForm = (selector) => {
tags: fileTags.join(','),
is_oc: (isShitpost ? item.is_oc : isOc),
comment: fileComment,
is_shitpost: isShitpost ? true : undefined
is_shitpost: isShitpost ? true : undefined,
title: fileTitle || undefined
}));
} else {
xhr.send(formData);