360 lines
15 KiB
JavaScript
360 lines
15 KiB
JavaScript
(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 = '<i class="fa-solid fa-xmark"></i>';
|
|
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 have a fallback
|
|
const favoBtns = document.querySelectorAll("#a_favo");
|
|
const wasAlreadyFav = favoBtns.length > 0 && favoBtns[0].classList.contains('fa-solid');
|
|
|
|
try {
|
|
const res = await post('/api/v2/togglefav', {
|
|
postid: postid
|
|
});
|
|
if (res && res.success) {
|
|
if (window.invalidateItemCache) {
|
|
window.invalidateItemCache(postid);
|
|
}
|
|
// Authoritative server state takes priority; fallback to inverting previous UI state
|
|
const isNowFav = (res.favorited !== undefined) ? !!res.favorited : !wasAlreadyFav;
|
|
|
|
favoBtns.forEach(favoBtn => {
|
|
favoBtn.classList.toggle('fa-solid', isNowFav);
|
|
favoBtn.classList.toggle('fa-regular', !isNowFav);
|
|
});
|
|
|
|
// span#favs
|
|
const favcontainer = document.querySelector('#favs');
|
|
if (favcontainer) {
|
|
favcontainer.innerHTML = "";
|
|
if (res.favs && res.favs.length > 0) {
|
|
const isAnonymized = !!(window.f0ckSession?.is_anonymized ?? (window.f0ckSession?.guest_anonymize && !window.f0ckSession?.logged_in) ?? (window.f0ckSession?.is_anon && window.f0ckSession?.anon_anonymize));
|
|
const fragment = document.createDocumentFragment();
|
|
res.favs.forEach(f => {
|
|
const isSelf = window.f0ckSession && (
|
|
(window.f0ckSession.id && f.user_id && Number(window.f0ckSession.id) === Number(f.user_id)) ||
|
|
(window.f0ckSession.user && f.user && window.f0ckSession.user.toLowerCase() === f.user.toLowerCase()) ||
|
|
(window.f0ckSession.login && f.login && window.f0ckSession.login.toLowerCase() === f.login.toLowerCase())
|
|
);
|
|
const isFavAnon = f.is_anon || f.user === 'anonymous' || (typeof f.user === 'string' && f.user.startsWith('anon_'));
|
|
|
|
if (f.hide_fav_badge && !isSelf) {
|
|
const a = document.createElement('a');
|
|
a.className = 'ghost-fav';
|
|
a.setAttribute('tooltip', '?');
|
|
a.setAttribute('flow', 'up');
|
|
a.style.cursor = 'default';
|
|
|
|
const img = document.createElement('img');
|
|
img.src = '/s/img/ghost_fav.svg';
|
|
img.style.height = "32px";
|
|
img.style.width = "32px";
|
|
|
|
a.appendChild(img);
|
|
fragment.appendChild(a);
|
|
} else if (isAnonymized || isFavAnon) {
|
|
const a = document.createElement('a');
|
|
a.className = 'ghost-fav';
|
|
a.setAttribute('tooltip', 'anonymous');
|
|
a.setAttribute('flow', 'up');
|
|
a.style.cursor = 'default';
|
|
|
|
const img = document.createElement('img');
|
|
img.src = '/a/default.png';
|
|
img.style.height = "32px";
|
|
img.style.width = "32px";
|
|
|
|
a.appendChild(img);
|
|
fragment.appendChild(a);
|
|
} else {
|
|
const a = document.createElement('a');
|
|
a.href = `/user/${(f.user || '').toLowerCase()}`;
|
|
a.setAttribute('tooltip', f.display_name || f.user || 'anonymous');
|
|
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);
|
|
fragment.appendChild(a);
|
|
}
|
|
});
|
|
favcontainer.appendChild(fragment);
|
|
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);
|
|
}
|
|
});
|
|
|
|
|
|
})();
|