(async () => { // Helper to get dynamic context from the DOM const getContext = () => { const commentsEl = document.querySelector("#comments-container"); const favoEl = document.querySelector("#a_favo"); const infoEl = document.querySelector("#a_info"); const idLinkEl = document.querySelector("a.id-link"); const rawId = commentsEl?.dataset?.itemId || favoEl?.dataset?.itemId || infoEl?.dataset?.itemId || idLinkEl?.dataset?.itemId || idLinkEl?.innerText; if (!rawId) return null; const tagsContainer = document.querySelector("#tags"); const inner = tagsContainer ? (tagsContainer.querySelector(".tags-inner") || tagsContainer) : null; const currentSub = (window.albumGallery && typeof window.albumGallery.getCurrentSubf0ck === 'function') ? window.albumGallery.getCurrentSubf0ck() : null; const subf0ck_id = currentSub ? (currentSub.slug || currentSub.id) : (tagsContainer?.dataset?.subf0ckSlug || tagsContainer?.dataset?.subf0ckId || null); return { postid: /^\d+$/.test(String(rawId).trim()) ? parseInt(rawId, 10) : rawId.trim(), subf0ck_id, poster: document.querySelector("a#a_username")?.innerText, tags: inner ? [...inner.querySelectorAll(".badge")].map(t => t.innerText.slice(0, -2)) : [] }; }; const queryapi = async (url, data, method = 'GET') => { let req; if (method == 'POST') { req = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", "X-CSRF-Token": window.f0ckSession?.csrf_token }, body: JSON.stringify(data) }); } else { let s = []; for (const [key, val] of Object.entries(data)) s.push(encodeURIComponent(key) + "=" + encodeURIComponent(val)); req = await fetch(url + '?' + s.join('&')); } return await req.json(); }; const get = async (url, data) => queryapi(url, data, 'GET'); const post = async (url, data) => queryapi(url, data, 'POST'); const renderTags = (_tags, highlightTag = null) => { const tagsContainer = document.querySelector("#tags"); if (!tagsContainer) return; const inner = tagsContainer.querySelector(".tags-inner") || tagsContainer; const activePostId = tagsContainer.dataset.itemId || (typeof window.getCurrentItemId === 'function' ? window.getCurrentItemId() : null); const canManage = !!( document.querySelector('#tags[data-can-manage="true"]') || (window.f0ckSession && (window.f0ckSession.is_admin || window.f0ckSession.is_moderator)) || (window.f0ckSession && window.f0ckSession.user && document.querySelector('#a_username[data-username]')?.dataset?.username?.toLowerCase() === window.f0ckSession.user.toLowerCase()) ); // Only remove existing dynamically generated tags [...inner.querySelectorAll(".badge")].forEach(tag => { // Don't remove the one containing the add/toggle buttons, and don't remove the autocomplete input itself if (!tag.querySelector('#a_addtag') && !tag.querySelector('#a_toggle') && !tag.classList.contains('tag-ac-wrapper')) { tag.parentElement.removeChild(tag); } }); // Deduplicate: ensure only at most ONE rating tag exists in the tags list const ratingTags = _tags.filter(t => ['sfw', 'nsfw', 'nsfl'].includes(t.normalized)); const lastRatingTag = ratingTags.length ? ratingTags[ratingTags.length - 1] : null; const cleanTags = _tags.filter(t => !['sfw', 'nsfw', 'nsfl'].includes(t.normalized) || t === lastRatingTag); const tagsCopy = [...cleanTags]; tagsCopy.reverse().forEach(tag => { const isRating = ['sfw', 'nsfw', 'nsfl'].includes(tag.normalized); let contentEl; if (isRating) { contentEl = document.createElement("span"); contentEl.className = "rating-label"; contentEl.textContent = tag.tag; } else { contentEl = document.createElement("a"); contentEl.href = "/tag/" + tag.normalized; contentEl.style = "color: inherit !important"; contentEl.textContent = tag.tag; } const span = document.createElement("span"); span.classList.add("badge"); if (highlightTag && (tag.tag === highlightTag || tag.normalized === highlightTag)) { span.classList.add('new-tag-glow'); } tag.badge.split(" ").forEach(b => span.classList.add(b)); if (isRating) { span.classList.add('rating-tag', `is-${tag.normalized}`); span.dataset.rating = tag.normalized; if (activePostId) span.dataset.itemId = activePostId; if (canManage) { span.classList.add('can-cycle'); } } else if (!window.f0ckSession?.is_anonymized && window.f0ckSession?.logged_in && !window.f0ckSession?.is_anon && (tag.display_name || tag.user)) { span.setAttribute('tooltip', tag.display_name || tag.user); } span.insertAdjacentElement("beforeend", contentEl); if (window.f0ckSession && (window.f0ckSession.is_admin || window.f0ckSession.is_moderator) && !isRating) { const space = document.createTextNode('\u00A0'); //   span.appendChild(space); const del = document.createElement("a"); del.className = "removetag admin-deltag"; del.href = "javascript:void(0)"; del.innerHTML = ''; span.insertAdjacentElement("beforeend", del); } inner.insertAdjacentElement("afterbegin", span); }); const hasRating = !!lastRatingTag; if (!hasRating) { const untaggedSpan = document.createElement("span"); untaggedSpan.className = `badge badge-untagged rating-tag is-untagged${canManage ? ' can-cycle' : ''}`; untaggedSpan.dataset.rating = 'untagged'; if (activePostId) untaggedSpan.dataset.itemId = activePostId; const lbl = document.createElement("span"); lbl.className = "rating-label"; lbl.textContent = "untagged"; untaggedSpan.appendChild(lbl); inner.insertAdjacentElement("afterbegin", untaggedSpan); } // Safeguard: remove any extra rating tags in DOM const allRatingEls = inner.querySelectorAll('.rating-tag, .badge-success, .badge-danger, .badge-nsfl, .badge-untagged, [data-rating]'); if (allRatingEls.length > 1) { for (let i = 1; i < allRatingEls.length; i++) { allRatingEls[i].remove(); } } // Update thumbnail data-mode in background grid (ensures div.posts > a > p::before updates in Onara) if (activePostId) { const modeAttr = lastRatingTag ? lastRatingTag.normalized : 'null'; document.querySelectorAll(`.posts > a.thumb[data-item-id="${activePostId}"], .posts a[data-item-id="${activePostId}"], .posts a[href$="/${activePostId}"]`).forEach(th => { th.setAttribute('data-mode', modeAttr); }); if (document.body.classList.contains('onara-modal-open')) { const onaraThumb = document.querySelector('.posts > a.thumb.onara-active'); if (onaraThumb) onaraThumb.setAttribute('data-mode', modeAttr); } } // Handle show more/less toggle visibility and count const allBadges = [...inner.querySelectorAll(".badge")]; const realTags = allBadges.filter(b => !b.querySelector('#a_addtag') && !b.querySelector('#a_toggle') && !b.classList.contains('tag-ac-wrapper')); let toggle = tagsContainer.querySelector(".show-tags-toggle"); if (realTags.length > 10) { if (!toggle) { toggle = document.createElement("a"); toggle.href = "#"; toggle.className = "show-tags-toggle"; tagsContainer.appendChild(toggle); } const hiddenCount = realTags.length - 10; toggle.dataset.count = hiddenCount; // Auto-expand when rendering new tags (e.g. after adding one) as requested tagsContainer.classList.add('tags-expanded'); toggle.textContent = "show less"; } else if (toggle) { toggle.remove(); tagsContainer.classList.remove('tags-expanded'); } }; window.renderTags = renderTags; const addtagClick = (e) => { if (e) e.preventDefault(); const ctx = getContext(); if (!ctx) return; const { postid, subf0ck_id, tags } = ctx; const anchor = document.querySelector("a#a_addtag"); if (!anchor) return; TagAutocomplete.open({ postid, existingTags: tags, anchorEl: anchor, onSubmit: async (tag) => { const payload = { tagname: tag }; if (subf0ck_id) payload.subf0ck_id = subf0ck_id; const res = await post("/api/v2/tags/" + postid, payload); if (res.success) { if (window.albumGallery && typeof window.albumGallery.getCurrentSubf0ck === 'function') { const cur = window.albumGallery.getCurrentSubf0ck(); if (cur && res.tags) cur.tags = res.tags; } if (window.invalidateItemCache) { window.invalidateItemCache(postid); } } return res; }, renderTags: (newTags, hl) => { if (window.albumGallery && typeof window.albumGallery.getCurrentSubf0ck === 'function') { const cur = window.albumGallery.getCurrentSubf0ck(); if (cur) cur.tags = newTags; } renderTags(newTags, hl); } }); }; const toggleEvent = async (e) => { if (e) e.preventDefault(); const ratingEl = document.querySelector('.rating-tag.can-cycle, button#a_toggle'); if (window.cycleRating) { window.cycleRating(ratingEl); } }; const toggleFavEvent = async (e) => { // e is the click event or undefined if (e && typeof e.preventDefault === 'function') e.preventDefault(); if (window.f0ckSession?.is_anon && window.f0ckSession?.anon_permissions && window.f0ckSession.anon_permissions.favorite === false) { if (typeof window.flashMessage === 'function') { window.flashMessage('Anonymous favoriting is disabled.', 3000, 'error'); } return; } const ctx = getContext(); if (!ctx) return; const { postid } = ctx; // Read state BEFORE the API call so we know which direction to toggle const favoBtn = document.querySelector("#a_favo"); const wasAlreadyFav = favoBtn && favoBtn.classList.contains('fa-solid'); try { const res = await post('/api/v2/togglefav', { postid: postid }); if (res && res.success) { if (window.invalidateItemCache) { window.invalidateItemCache(postid); } // New state is the logical opposite of what it was before the API call const isNowFav = !wasAlreadyFav; if (favoBtn) { favoBtn.classList.toggle('fa-solid', isNowFav); favoBtn.classList.toggle('fa-regular', !isNowFav); } // span#favs const favcontainer = document.querySelector('#favs'); favcontainer.innerHTML = ""; if (res.favs && res.favs.length > 0) { res.favs.forEach(f => { const a = document.createElement('a'); a.href = `/user/${f.user}`; a.setAttribute('tooltip', f.display_name || f.user); a.setAttribute('flow', 'up'); const img = document.createElement('img'); img.src = f.avatar_file ? `/a/${f.avatar_file}` : (f.avatar ? `/t/${f.avatar}.webp` : '/a/default.png'); img.style.height = "32px"; img.style.width = "32px"; if (f.username_color) img.style.borderColor = f.username_color; a.appendChild(img); favcontainer.appendChild(a); }); favcontainer.hidden = false; } else { favcontainer.hidden = true; } window.flashMessage((window.f0ckI18n && (isNowFav ? window.f0ckI18n.fav_added : window.f0ckI18n.fav_removed)) || (isNowFav ? 'ADDED TO FAVORITES' : 'REMOVED FROM FAVORITES')); if (navigator.vibrate) navigator.vibrate(50); } else { const errMsg = (res && (res.msg || res.error)) || 'Anonymous favoriting is disabled.'; if (typeof window.flashMessage === 'function') { window.flashMessage(errMsg, 3000, 'error'); } } } catch (err) { if (typeof window.flashMessage === 'function') { window.flashMessage('Failed to update favorite.', 3000, 'error'); } } }; // Event Delegation document.addEventListener("click", e => { if (document.querySelector('script[src*="admin.js"]')) return; const target = e.target.nodeType === 3 ? e.target.parentElement : e.target; if (target.closest("a#a_addtag")) { addtagClick(e); } else if (target.closest("#a_favo")) { toggleFavEvent(e); } }); })();