#12 first commit

This commit is contained in:
2026-05-16 20:11:51 +02:00
parent fbd47636d1
commit 552c239677
16 changed files with 962 additions and 12 deletions

View File

@@ -1858,6 +1858,7 @@ class CommentSystem {
<a href="#c${comment.id}" class="comment-time timeago" title="${fullDate}" data-id="${comment.id}" data-username="${comment.username}" data-display="${this.escapeHtml(comment.display_name || '')}">${timeAgo}</a>
</div>
<div class="comment-content" data-raw="${this.escapeHtml(comment.content)}">${content}</div>
${this.renderCommentAttachments(comment.files)}
<div class="comment-footer">
<div class="comment-footer-right">
<div class="comment-actions">
@@ -1901,22 +1902,46 @@ class CommentSystem {
return div.innerHTML;
}
renderCommentAttachments(files) {
if (!files || files.length === 0) return '';
const items = files.map(f => {
const url = `/c/${f.dest}`;
if (f.mime.startsWith('image/')) {
return `<a href="${url}" target="_blank" class="cf-attachment cf-image"><img src="${url}" alt="${this.escapeHtml(f.original_filename || 'image')}" loading="lazy"></a>`;
} else if (f.mime.startsWith('video/')) {
return `<div class="cf-attachment cf-video"><video src="${url}" controls preload="metadata"></video></div>`;
} else if (f.mime.startsWith('audio/')) {
return `<div class="cf-attachment cf-audio"><audio src="${url}" controls preload="metadata"></audio></div>`;
}
return '';
}).join('');
return items ? `<div class="comment-attachments">${items}</div>` : '';
}
renderInput(parentId = null) {
const i18n = window.f0ckI18n || {};
const session = window.f0ckSession || {};
const placeholder = i18n.write_comment || 'Write a comment...';
const postLabel = i18n.post || 'Post';
const cancelLabel = i18n.cancel || 'Cancel';
const maxLen = window.f0ckSession?.comment_max_length;
const attachLabel = i18n.attach_file || 'Attach file';
const maxLen = session.comment_max_length;
const maxLenAttr = (maxLen !== null && maxLen !== undefined) ? ` maxlength="${maxLen}"` : '';
const counter = (maxLen !== null && maxLen !== undefined)
? `<span class="char-counter" data-max="${maxLen}">0 / ${maxLen}</span>`
: '';
const fileUploadEnabled = session.logged_in && session.allow_fileupload_comments;
const multiFile = session.fileupload_comments_multifile;
const attachBtn = fileUploadEnabled
? `<button class="comment-attach-btn" title="${attachLabel}" type="button"><i class="fa-solid fa-paperclip"></i></button><input type="file" class="comment-file-input" accept="image/*,video/*,audio/*" ${multiFile ? 'multiple' : ''} style="display:none;">`
: '';
return `
<div class="comment-input ${parentId ? 'reply-input' : 'main-input'}" ${parentId ? `data-parent="${parentId}"` : ''}>
<textarea placeholder="${placeholder}"${maxLenAttr}></textarea>
<div class="comment-file-preview"></div>
<div class="input-actions">
${counter}
${attachBtn}
${parentId ? `<button class="cancel-reply" title="${cancelLabel}"><i class="fa-solid fa-xmark"></i></button>` : ''}
<button class="submit-comment">${postLabel}</button>
</div>
@@ -2080,6 +2105,113 @@ class CommentSystem {
});
// Single Click Listener for Everything
this.container.addEventListener('change', async (e) => {
if (!e.target.matches('.comment-file-input')) return;
const fileInput = e.target;
const wrap = fileInput.closest('.comment-input');
if (!wrap) return;
const textarea = wrap.querySelector('textarea');
if (!textarea) return;
const previewArea = wrap.querySelector('.comment-file-preview');
const session = window.f0ckSession || {};
const maxSize = session.fileupload_comments_size || (10 * 1024 * 1024);
const i18n = window.f0ckI18n || {};
const removeLabel = i18n.remove_file || 'Remove file';
for (const file of fileInput.files) {
if (file.size > maxSize) {
alert((i18n.file_too_large || 'File too large') + `: ${file.name}`);
continue;
}
const fd = new FormData();
fd.append('file', file);
const csrf = session.csrf_token || '';
const uploadingText = i18n.uploading_file || 'Uploading...';
// Insert placeholder at cursor position
const cursorPos = textarea.selectionStart;
const before = textarea.value.substring(0, cursorPos);
const after = textarea.value.substring(textarea.selectionEnd);
const placeholder = `[${uploadingText} ${file.name}]`;
const sep = before.length > 0 && !/\s$/.test(before) ? ' ' : '';
textarea.value = before + sep + placeholder + after;
// Show uploading preview
let previewItem = null;
if (previewArea) {
previewItem = document.createElement('div');
previewItem.className = 'cf-preview-item cf-uploading';
const spinner = document.createElement('i');
spinner.className = 'fa-solid fa-spinner fa-spin';
previewItem.appendChild(spinner);
const nameEl = document.createElement('span');
nameEl.className = 'cf-filename';
nameEl.textContent = file.name;
previewItem.appendChild(nameEl);
previewArea.appendChild(previewItem);
}
try {
const res = await fetch('/api/v2/comments/upload', {
method: 'POST',
headers: { 'X-CSRF-Token': csrf },
body: fd
});
const json = await res.json();
if (json.success && json.files && json.files.length > 0) {
const fileData = json.files[0];
const url = `/c/${fileData.dest}`;
textarea.value = textarea.value.replace(placeholder, url);
// Update preview with actual thumbnail
if (previewItem) {
previewItem.classList.remove('cf-uploading');
previewItem.dataset.url = url;
previewItem.dataset.fileId = fileData.id;
previewItem.innerHTML = '';
if (fileData.mime.startsWith('image/')) {
const img = document.createElement('img');
img.src = url;
img.loading = 'lazy';
previewItem.appendChild(img);
} else if (fileData.mime.startsWith('video/')) {
const vid = document.createElement('video');
vid.src = url;
vid.muted = true;
vid.preload = 'metadata';
previewItem.appendChild(vid);
} else {
const icon = document.createElement('i');
icon.className = 'fa-solid fa-music';
previewItem.appendChild(icon);
}
const nameEl = document.createElement('span');
nameEl.className = 'cf-filename';
nameEl.textContent = file.name;
previewItem.appendChild(nameEl);
const removeBtn = document.createElement('button');
removeBtn.className = 'cf-remove-btn';
removeBtn.title = removeLabel;
removeBtn.innerHTML = '<i class="fa-solid fa-xmark"></i>';
previewItem.appendChild(removeBtn);
}
} else {
textarea.value = textarea.value.replace(placeholder, '');
if (previewItem) previewItem.remove();
alert('Upload error: ' + (json.msg || 'Unknown error'));
}
} catch (err) {
textarea.value = textarea.value.replace(placeholder, '');
if (previewItem) previewItem.remove();
alert('Upload failed: ' + err.message);
}
}
fileInput.value = '';
});
this.container.addEventListener('click', async (e) => {
_f0ckDebug('[DEBUG] Click on container:', e.target);
const target = e.target;
@@ -2106,6 +2238,32 @@ class CommentSystem {
return;
}
// Attach file button
if (target.matches('.comment-attach-btn') || target.closest('.comment-attach-btn')) {
const wrap = target.closest('.comment-input');
const fileInput = wrap?.querySelector('.comment-file-input');
if (fileInput) fileInput.click();
return;
}
// Remove file preview + strip URL from textarea
if (target.matches('.cf-remove-btn') || target.closest('.cf-remove-btn')) {
const previewItem = target.closest('.cf-preview-item');
if (previewItem) {
const url = previewItem.dataset.url;
if (url) {
const wrap = previewItem.closest('.comment-input');
const textarea = wrap?.querySelector('textarea');
if (textarea) {
// Remove the URL and any surrounding newline
textarea.value = textarea.value.replace(new RegExp('\\n?' + url.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&') + '\\n?'), '\n').replace(/^\n|\n$/g, '');
}
}
previewItem.remove();
}
return;
}
// Submit Comment
if (target.matches('.submit-comment')) {
this.handleSubmit(e);
@@ -2467,6 +2625,8 @@ class CommentSystem {
counter.textContent = `0 / ${counter.dataset.max}`;
counter.classList.remove('near-limit', 'at-limit');
}
const fpArea = wrap.querySelector('.comment-file-preview');
if (fpArea) fpArea.innerHTML = '';
}
// Notify the right sidebar that a new comment was posted (silent refresh)