gsdfgsdf
This commit is contained in:
+292
-231
@@ -38,7 +38,7 @@ window.cancelAnimFrame = (function () {
|
||||
};
|
||||
|
||||
window.getCurrentItemId = () => {
|
||||
const idEl = document.querySelector('a.id-link[data-item-id], #a_toggle[data-item-id], #a_favo[data-item-id], #comments-container[data-item-id], [data-item-id]');
|
||||
const idEl = document.querySelector('a.id-link[data-item-id], .rating-tag[data-item-id], #a_toggle[data-item-id], #a_favo[data-item-id], #comments-container[data-item-id], [data-item-id]');
|
||||
if (idEl && idEl.dataset && idEl.dataset.itemId) {
|
||||
const parsed = parseInt(idEl.dataset.itemId, 10);
|
||||
if (!isNaN(parsed)) return String(parsed);
|
||||
@@ -1299,6 +1299,10 @@ window.cancelAnimFrame = (function () {
|
||||
'#comments-container',
|
||||
'.tag-controls',
|
||||
'.sidebar-tags-container',
|
||||
'#tags',
|
||||
'.rating-tag',
|
||||
'.rating-label',
|
||||
'.item-sidebar-left',
|
||||
'.xd-score-wrapper',
|
||||
'.user-infobox-block',
|
||||
'a',
|
||||
@@ -1762,6 +1766,15 @@ window.cancelAnimFrame = (function () {
|
||||
document.body.classList.remove('modal-open');
|
||||
return;
|
||||
}
|
||||
const tagAcWrapper = document.querySelector('.tag-ac-wrapper');
|
||||
if (tagAcWrapper || (window.TagAutocomplete && typeof window.TagAutocomplete.isOpen === 'function' && window.TagAutocomplete.isOpen())) {
|
||||
if (window.TagAutocomplete && typeof window.TagAutocomplete.destroy === 'function') {
|
||||
window.TagAutocomplete.destroy();
|
||||
} else if (tagAcWrapper) {
|
||||
tagAcWrapper.remove();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (document.body.classList.contains('onara-modal-open')) {
|
||||
closeOnaraModal();
|
||||
}
|
||||
@@ -4530,6 +4543,20 @@ window.cancelAnimFrame = (function () {
|
||||
document.addEventListener('click', (e) => {
|
||||
const target = e.target.nodeType === 3 ? e.target.parentElement : e.target;
|
||||
|
||||
// Intercept rating cycling immediately before any link/navigation handler can catch it
|
||||
const ratingTag = target.closest('.rating-tag') || target.closest('button#a_toggle');
|
||||
if (ratingTag && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey && e.button === 0) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
|
||||
const canCycle = ratingTag.classList.contains('can-cycle') || ratingTag.id === 'a_toggle' || !!document.querySelector('#tags[data-can-manage="true"]');
|
||||
if (canCycle && window.cycleRating) {
|
||||
window.cycleRating(ratingTag);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for mode selection (only applies to <a href="/mode/..."> links, not plain buttons)
|
||||
const modeBtn = target.closest('.mode-btn');
|
||||
if (modeBtn && modeBtn.href && modeBtn.href.includes('/mode/') && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) {
|
||||
@@ -4714,10 +4741,11 @@ window.cancelAnimFrame = (function () {
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success && data.items && (data.items.slug || data.items.id)) {
|
||||
const targetKey = data.items.slug || data.items.id;
|
||||
const targetId = data.items.id;
|
||||
const targetKey = data.items.slug || data.items.id;
|
||||
const targetId = data.items.id;
|
||||
const targetPage = data.items.page || 1;
|
||||
|
||||
// Navigate immediately — don't wait for grid sync
|
||||
// Navigate immediately
|
||||
if (wUserHall && wUserHallOwner) {
|
||||
loadItemAjax(`/user/${encodeURIComponent(wUserHallOwner)}/hall/${encodeURIComponent(wUserHall)}/${targetKey}`, true, { transition: 'fade-zoom' });
|
||||
} else if (wFavsUser) {
|
||||
@@ -4726,68 +4754,40 @@ window.cancelAnimFrame = (function () {
|
||||
loadItemAjax(`/${targetKey}`, true, { transition: 'fade-zoom' });
|
||||
}
|
||||
|
||||
// Background grid sync for Onara — fire and forget, user never waits for this
|
||||
// Background grid sync — page number already in response, no extra fetch needed
|
||||
if (isOnaraActive() && targetId) {
|
||||
const gridThumbs = Array.from(document.querySelectorAll('.posts > a:not(.notif-item)'));
|
||||
const alreadyInGrid = gridThumbs.some(a => {
|
||||
const k = a.pathname.replace(/\/$/, '').split('/').pop();
|
||||
return k == targetId || (data.items.slug && k === data.items.slug);
|
||||
});
|
||||
console.log('[random-gridsync] isOnaraActive:', true, 'alreadyInGrid:', alreadyInGrid, 'targetId:', targetId, '_onaraCurrentGridUrl:', window._onaraCurrentGridUrl);
|
||||
const postsEl = document.querySelector('.posts[data-current-page]');
|
||||
const currentPage = postsEl ? parseInt(postsEl.dataset.currentPage, 10) : 1;
|
||||
|
||||
if (!alreadyInGrid) {
|
||||
const pageParams = new URLSearchParams(params);
|
||||
pageParams.set('id', targetId);
|
||||
console.log('[random-gridsync] fetching item-page:', `/api/v2/item-page?${pageParams}`);
|
||||
fetch(`/api/v2/item-page?${pageParams}`)
|
||||
.then(r => r.json())
|
||||
.then(pd => {
|
||||
console.log('[random-gridsync] item-page response:', pd);
|
||||
if (!pd.success || !pd.page) return;
|
||||
const targetPage = pd.page;
|
||||
const postsEl = document.querySelector('.posts[data-current-page]');
|
||||
const currentPage = postsEl ? parseInt(postsEl.dataset.currentPage, 10) : 1;
|
||||
console.log('[random-gridsync] targetPage:', targetPage, 'currentPage:', currentPage, '_onaraCurrentGridUrl:', window._onaraCurrentGridUrl);
|
||||
if (targetPage === currentPage) return;
|
||||
if (targetPage !== currentPage) {
|
||||
// Build page URL from current grid base
|
||||
let gridBase = window._onaraCurrentGridUrl
|
||||
? window._onaraCurrentGridUrl.replace(/\/p\/\d+/, '').replace(/\/$/, '')
|
||||
: window.location.pathname
|
||||
.replace(/\/[a-zA-Z0-9_-]{11}$/, '')
|
||||
.replace(/\/\d+$/, '')
|
||||
.replace(/\/p\/\d+/, '')
|
||||
.replace(/\/$/, '');
|
||||
const pageUrl = targetPage === 1 ? (gridBase || '/') : `${gridBase}/p/${targetPage}`;
|
||||
|
||||
let gridBase;
|
||||
if (window._onaraCurrentGridUrl) {
|
||||
// Strip trailing slash — root '/' becomes '' so '/p/N' builds correctly
|
||||
gridBase = window._onaraCurrentGridUrl.replace(/\/p\/\d+/, '').replace(/\/$/, '');
|
||||
} else {
|
||||
gridBase = window.location.pathname
|
||||
.replace(/\/[a-zA-Z0-9_-]{11}$/, '')
|
||||
.replace(/\/\d+$/, '')
|
||||
.replace(/\/p\/\d+/, '')
|
||||
.replace(/\/$/, '');
|
||||
fetch(pageUrl)
|
||||
.then(r => r.text())
|
||||
.then(html => {
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
const newPosts = doc.querySelector('.posts');
|
||||
const oldPosts = document.querySelector('.posts');
|
||||
if (newPosts && oldPosts) {
|
||||
oldPosts.innerHTML = newPosts.innerHTML;
|
||||
oldPosts.dataset.currentPage = targetPage;
|
||||
if (typeof window.initLazyLoading === 'function') window.initLazyLoading();
|
||||
}
|
||||
window._onaraCurrentGridUrl = pageUrl;
|
||||
if (typeof updateOnaraActiveItem === 'function') {
|
||||
updateOnaraActiveItem(targetId, `/${targetKey}`);
|
||||
}
|
||||
const pageUrl = targetPage === 1 ? (gridBase || '/') : `${gridBase}/p/${targetPage}`;
|
||||
console.log('[random-gridsync] fetching grid page:', pageUrl);
|
||||
|
||||
fetch(pageUrl)
|
||||
.then(r => r.text())
|
||||
.then(html => {
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
const newPosts = doc.querySelector('.posts');
|
||||
const oldPosts = document.querySelector('.posts');
|
||||
console.log('[random-gridsync] grid swap: newPosts found:', !!newPosts, 'oldPosts found:', !!oldPosts);
|
||||
if (newPosts && oldPosts) {
|
||||
oldPosts.innerHTML = newPosts.innerHTML;
|
||||
oldPosts.dataset.currentPage = targetPage;
|
||||
if (typeof window.initLazyLoading === 'function') window.initLazyLoading();
|
||||
}
|
||||
window._onaraCurrentGridUrl = pageUrl;
|
||||
// Highlight after grid swapped
|
||||
if (typeof updateOnaraActiveItem === 'function') {
|
||||
updateOnaraActiveItem(targetId, `/${targetKey}`);
|
||||
}
|
||||
})
|
||||
.catch((e) => { console.warn('[random-gridsync] grid fetch failed', e); });
|
||||
})
|
||||
.catch((e) => { console.warn('[random-gridsync] item-page fetch failed', e); });
|
||||
.catch(() => {});
|
||||
}
|
||||
} else {
|
||||
console.log('[random-gridsync] skipped: isOnaraActive=', isOnaraActive(), 'targetId=', targetId);
|
||||
}
|
||||
} else {
|
||||
window.location.href = link.href;
|
||||
@@ -4827,7 +4827,7 @@ window.cancelAnimFrame = (function () {
|
||||
const anyLink = target.closest('a');
|
||||
if (anyLink && anyLink.hostname === window.location.hostname && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey && anyLink.target !== '_blank') {
|
||||
const pathname = anyLink.pathname;
|
||||
const isSpecialLink = anyLink.classList.contains('removetag') || anyLink.classList.contains('admin-deltag') || anyLink.classList.contains('mode-btn') || anyLink.classList.contains('btn-approve-async') || anyLink.classList.contains('btn-deny-async') || anyLink.getAttribute('href') === '#';
|
||||
const isSpecialLink = anyLink.classList.contains('removetag') || anyLink.classList.contains('admin-deltag') || anyLink.classList.contains('mode-btn') || anyLink.classList.contains('btn-approve-async') || anyLink.classList.contains('btn-deny-async') || anyLink.getAttribute('href') === '#' || !!anyLink.closest('.rating-tag');
|
||||
|
||||
const targetUrl = anyLink.href;
|
||||
const currentUrl = window.location.href.split('#')[0];
|
||||
@@ -5151,168 +5151,175 @@ window.cancelAnimFrame = (function () {
|
||||
|
||||
fileInput.dataset.itemId = itemId;
|
||||
fileInput.click();
|
||||
|
||||
} else if (target.closest('button#a_toggle')) {
|
||||
e.preventDefault();
|
||||
const toggleBtn = target.closest('button#a_toggle');
|
||||
const itemIdStr = toggleBtn.dataset.itemId || window.getCurrentItemId();
|
||||
if (!itemIdStr) return;
|
||||
const postid = parseInt(itemIdStr, 10);
|
||||
if (isNaN(postid)) return;
|
||||
|
||||
// Determine current rating
|
||||
let currentRating = 'sfw';
|
||||
if (toggleBtn.classList.contains('is-sfw')) currentRating = 'sfw';
|
||||
else if (toggleBtn.classList.contains('is-nsfw')) currentRating = 'nsfw';
|
||||
else if (toggleBtn.classList.contains('is-nsfl')) currentRating = 'nsfl';
|
||||
else if (toggleBtn.classList.contains('is-untagged')) currentRating = 'untagged';
|
||||
|
||||
// Cycle SFW -> NSFW -> NSFL (if enabled) -> SFW
|
||||
let nextRating = 'sfw';
|
||||
if (currentRating === 'sfw') {
|
||||
nextRating = 'nsfw';
|
||||
} else if (currentRating === 'nsfw') {
|
||||
nextRating = (window.f0ckSession && window.f0ckSession.enable_nsfl) ? 'nsfl' : 'sfw';
|
||||
}
|
||||
|
||||
const labels = { sfw: 'SFW', nsfw: 'NSFW', nsfl: 'NSFL', untagged: '?' };
|
||||
const nextLabel = labels[nextRating] || nextRating.toUpperCase();
|
||||
|
||||
// Backup current state
|
||||
const oldClasses = [...toggleBtn.classList];
|
||||
const oldInnerHTML = toggleBtn.innerHTML;
|
||||
|
||||
// Track active request ID to ignore out-of-order race conditions on rapid keypresses
|
||||
const reqId = (toggleBtn._lastCycleReqId || 0) + 1;
|
||||
toggleBtn._lastCycleReqId = reqId;
|
||||
|
||||
// Increment active requests count to block incoming live SSE tag updates during cycle
|
||||
toggleBtn._activeRequestsCount = (toggleBtn._activeRequestsCount || 0) + 1;
|
||||
|
||||
// Optimistically apply new state
|
||||
toggleBtn.classList.remove('is-sfw', 'is-nsfw', 'is-nsfl', 'is-untagged');
|
||||
toggleBtn.classList.add(`is-${nextRating}`);
|
||||
// (icon stays — only CSS class color changes)
|
||||
|
||||
// Optimistically update the sidebar tag list
|
||||
let originalTags = [];
|
||||
if (window.renderTags) {
|
||||
const tagsContainer = document.querySelector("#tags");
|
||||
const inner = tagsContainer ? (tagsContainer.querySelector(".tags-inner") || tagsContainer) : null;
|
||||
if (inner) {
|
||||
originalTags = [...inner.querySelectorAll(".badge")].filter(badge => {
|
||||
return !badge.querySelector('#a_addtag') && !badge.querySelector('#a_toggle') && !badge.classList.contains('tag-ac-wrapper');
|
||||
}).map(badge => {
|
||||
const a = badge.querySelector('a[href*="/tag/"]');
|
||||
const tagText = a ? a.innerText.trim() : '';
|
||||
const normalized = a ? a.getAttribute('href').split('/').pop() : '';
|
||||
const badgeClasses = [...badge.classList].filter(c => c !== 'badge' && c !== 'mr-2').join(' ');
|
||||
return {
|
||||
tag: tagText,
|
||||
normalized: normalized,
|
||||
badge: badgeClasses
|
||||
};
|
||||
});
|
||||
|
||||
// Create the optimistic rating tag object
|
||||
const userStr = (window.f0ckSession && window.f0ckSession.user) || '';
|
||||
const dispName = (window.f0ckSession && window.f0ckSession.display_name) || userStr;
|
||||
let newRatingTag = null;
|
||||
if (nextRating === 'sfw') {
|
||||
newRatingTag = { id: 1, tag: 'sfw', normalized: 'sfw', badge: 'badge-success', user: userStr, display_name: dispName };
|
||||
} else if (nextRating === 'nsfw') {
|
||||
newRatingTag = { id: 2, tag: 'nsfw', normalized: 'nsfw', badge: 'badge-danger', user: userStr, display_name: dispName };
|
||||
} else if (nextRating === 'nsfl') {
|
||||
const nsfl_id = (window.f0ckSession && window.f0ckSession.nsfl_tag_id) || 3;
|
||||
newRatingTag = { id: nsfl_id, tag: 'nsfl', normalized: 'nsfl', badge: 'badge-nsfl', user: userStr, display_name: dispName };
|
||||
}
|
||||
|
||||
// Combine tags: filter out old rating tags and place the new one at the front
|
||||
const optimisticTags = [];
|
||||
if (newRatingTag) {
|
||||
optimisticTags.push(newRatingTag);
|
||||
}
|
||||
originalTags.filter(t => t.normalized !== 'sfw' && t.normalized !== 'nsfw' && t.normalized !== 'nsfl').forEach(t => {
|
||||
optimisticTags.push(t);
|
||||
});
|
||||
|
||||
window.renderTags(optimisticTags);
|
||||
}
|
||||
}
|
||||
|
||||
const flashMsg = (window.f0ckI18n && window.f0ckI18n.mode_activated && window.f0ckI18n.mode_activated.replace('{mode}', nextLabel)) || ('RATING UPDATED: ' + nextLabel);
|
||||
window.flashMessage(flashMsg);
|
||||
|
||||
// Enqueue the request to ensure sequential server execution in correct order
|
||||
if (!toggleBtn._requestQueue) {
|
||||
toggleBtn._requestQueue = Promise.resolve();
|
||||
}
|
||||
|
||||
toggleBtn._requestQueue = toggleBtn._requestQueue.then(() => {
|
||||
return fetch(`/api/v2/item/${postid}/rating`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.f0ckSession?.csrf_token
|
||||
},
|
||||
body: JSON.stringify({ rating: nextRating })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
toggleBtn._activeRequestsCount = Math.max(0, (toggleBtn._activeRequestsCount || 1) - 1);
|
||||
|
||||
// Evict cached item HTML immediately on success so navigating back fetches fresh content,
|
||||
// even if the user already navigated to a different item while this request was in flight.
|
||||
if (res && res.success && window.invalidateItemCache) {
|
||||
window.invalidateItemCache(postid);
|
||||
}
|
||||
|
||||
// Verify we are still on the same post (prevent dynamic/PJAX page leaks)
|
||||
const currentIdStr = window.getCurrentItemId();
|
||||
const currentPostId = currentIdStr ? parseInt(currentIdStr, 10) : null;
|
||||
if (currentPostId !== postid) return;
|
||||
|
||||
if (toggleBtn._lastCycleReqId !== reqId) return; // ignore stale responses
|
||||
if (res.success) {
|
||||
// Verify visual state and sync tags
|
||||
toggleBtn.classList.remove('is-sfw', 'is-nsfw', 'is-nsfl', 'is-untagged');
|
||||
toggleBtn.classList.add(`is-${res.rating}`);
|
||||
// icon stays constant — class drives the color
|
||||
|
||||
if (window.renderTags) {
|
||||
window.renderTags(res.tags);
|
||||
}
|
||||
} else {
|
||||
revert();
|
||||
window.flashMessage('Error: ' + (res.msg || 'Failed to update rating'), 3000, 'error');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
toggleBtn._activeRequestsCount = Math.max(0, (toggleBtn._activeRequestsCount || 1) - 1);
|
||||
|
||||
// Verify we are still on the same post (prevent dynamic/PJAX page leaks)
|
||||
const currentIdStr = window.getCurrentItemId();
|
||||
const currentPostId = currentIdStr ? parseInt(currentIdStr, 10) : null;
|
||||
if (currentPostId !== postid) return;
|
||||
|
||||
if (toggleBtn._lastCycleReqId !== reqId) return;
|
||||
console.error('[RATING_TOGGLE_ERROR]', err);
|
||||
revert();
|
||||
window.flashMessage('Failed to toggle rating', 3000, 'error');
|
||||
});
|
||||
});
|
||||
|
||||
function revert() {
|
||||
toggleBtn.className = '';
|
||||
oldClasses.forEach(cls => toggleBtn.classList.add(cls));
|
||||
toggleBtn.innerHTML = oldInnerHTML;
|
||||
if (window.renderTags && originalTags.length > 0) {
|
||||
window.renderTags(originalTags);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window._activeRatingCycleRequestsCount = 0;
|
||||
|
||||
window.cycleRating = function(targetEl, explicitPostId) {
|
||||
const ratingEl = targetEl ? (targetEl.closest('.rating-tag') || targetEl.closest('button#a_toggle') || targetEl) : document.querySelector('.rating-tag.can-cycle, button#a_toggle');
|
||||
if (!ratingEl) return;
|
||||
|
||||
const itemIdStr = explicitPostId || ratingEl.dataset.itemId || window.getCurrentItemId();
|
||||
if (!itemIdStr) return;
|
||||
const postid = parseInt(itemIdStr, 10);
|
||||
if (isNaN(postid)) return;
|
||||
|
||||
// Determine current rating
|
||||
let currentRating = ratingEl.dataset.rating || '';
|
||||
if (!currentRating) {
|
||||
if (ratingEl.classList.contains('is-sfw') || ratingEl.classList.contains('badge-success')) currentRating = 'sfw';
|
||||
else if (ratingEl.classList.contains('is-nsfw') || ratingEl.classList.contains('badge-danger')) currentRating = 'nsfw';
|
||||
else if (ratingEl.classList.contains('is-nsfl') || ratingEl.classList.contains('badge-nsfl')) currentRating = 'nsfl';
|
||||
else if (ratingEl.classList.contains('is-untagged') || ratingEl.classList.contains('badge-untagged')) currentRating = 'untagged';
|
||||
else {
|
||||
const txt = ratingEl.innerText.trim().toLowerCase();
|
||||
if (txt === 'sfw') currentRating = 'sfw';
|
||||
else if (txt === 'nsfw') currentRating = 'nsfw';
|
||||
else if (txt === 'nsfl') currentRating = 'nsfl';
|
||||
else currentRating = 'untagged';
|
||||
}
|
||||
}
|
||||
|
||||
// Cycle: SFW -> NSFW -> NSFL (if enabled) -> SFW; untagged -> SFW
|
||||
let nextRating = 'sfw';
|
||||
if (currentRating === 'sfw') {
|
||||
nextRating = 'nsfw';
|
||||
} else if (currentRating === 'nsfw') {
|
||||
nextRating = (window.f0ckSession && window.f0ckSession.enable_nsfl !== false) ? 'nsfl' : 'sfw';
|
||||
} else if (currentRating === 'nsfl') {
|
||||
nextRating = 'sfw';
|
||||
}
|
||||
|
||||
const mapping = {
|
||||
sfw: { label: 'SFW', text: 'sfw', badgeCls: 'badge-success', isCls: 'is-sfw', toast: '🛡 SFW' },
|
||||
nsfw: { label: 'NSFW', text: 'nsfw', badgeCls: 'badge-danger', isCls: 'is-nsfw', toast: '🔥 NSFW' },
|
||||
nsfl: { label: 'NSFL', text: 'nsfl', badgeCls: 'badge-nsfl', isCls: 'is-nsfl', toast: '💀 NSFL' },
|
||||
untagged: { label: '?', text: 'untagged', badgeCls: 'badge-untagged', isCls: 'is-untagged', toast: '❓ Untagged' }
|
||||
};
|
||||
const info = mapping[nextRating];
|
||||
const nextLabel = info.label;
|
||||
|
||||
// Backup current state
|
||||
const oldClasses = [...ratingEl.classList];
|
||||
const oldInnerHTML = ratingEl.innerHTML;
|
||||
const oldDatasetRating = ratingEl.dataset.rating;
|
||||
|
||||
// Track active request ID to ignore out-of-order race conditions on rapid keypresses
|
||||
const reqId = (ratingEl._lastCycleReqId || 0) + 1;
|
||||
ratingEl._lastCycleReqId = reqId;
|
||||
|
||||
// Increment active requests count to block incoming live SSE tag updates during cycle
|
||||
window._activeRatingCycleRequestsCount = (window._activeRatingCycleRequestsCount || 0) + 1;
|
||||
ratingEl._activeRequestsCount = (ratingEl._activeRequestsCount || 0) + 1;
|
||||
|
||||
// Optimistically apply new state to rating tag in-place (no full-DOM recreation)
|
||||
ratingEl.className = `badge ${info.badgeCls} rating-tag ${info.isCls} can-cycle`;
|
||||
ratingEl.dataset.rating = nextRating;
|
||||
ratingEl.removeAttribute('title');
|
||||
ratingEl.removeAttribute('tooltip');
|
||||
ratingEl.removeAttribute('flow');
|
||||
|
||||
const labelEl = ratingEl.querySelector('.rating-label') || ratingEl;
|
||||
labelEl.textContent = info.text;
|
||||
|
||||
// Immediately remove any duplicate rating tags from #tags container
|
||||
const tagsContainer = document.querySelector("#tags");
|
||||
if (tagsContainer) {
|
||||
const allRatingBadges = tagsContainer.querySelectorAll('.rating-tag, .badge-success, .badge-danger, .badge-nsfl, .badge-untagged, [data-rating]');
|
||||
allRatingBadges.forEach(b => {
|
||||
if (b !== ratingEl) b.remove();
|
||||
});
|
||||
}
|
||||
|
||||
// Also update any legacy a_toggle if present
|
||||
const toggleBtn = document.querySelector('button#a_toggle');
|
||||
if (toggleBtn && toggleBtn !== ratingEl) {
|
||||
toggleBtn.classList.remove('is-sfw', 'is-nsfw', 'is-nsfl', 'is-untagged');
|
||||
toggleBtn.classList.add(`is-${nextRating}`);
|
||||
}
|
||||
|
||||
// Optimistically update media-object data-mode for instant visual blur reaction
|
||||
const mediaObj = document.querySelector('.media-object');
|
||||
if (mediaObj) {
|
||||
mediaObj.dataset.mode = nextRating;
|
||||
}
|
||||
|
||||
const flashMsg = (window.f0ckI18n && window.f0ckI18n.mode_activated && window.f0ckI18n.mode_activated.replace('{mode}', nextLabel)) || ('RATING UPDATED: ' + nextLabel);
|
||||
window.flashMessage(flashMsg);
|
||||
|
||||
// Debounce the network request by 250ms so rapid clicks only dispatch the final state to the server
|
||||
window._ratingDebounceTimers = window._ratingDebounceTimers || {};
|
||||
if (window._ratingDebounceTimers[postid]) {
|
||||
clearTimeout(window._ratingDebounceTimers[postid]);
|
||||
}
|
||||
window._activeRatingCycleRequestsCount = 1;
|
||||
ratingEl._activeRequestsCount = 1;
|
||||
|
||||
window._ratingDebounceTimers[postid] = setTimeout(() => {
|
||||
delete window._ratingDebounceTimers[postid];
|
||||
const targetRating = ratingEl.dataset.rating || nextRating;
|
||||
|
||||
fetch(`/api/v2/item/${postid}/rating`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": window.f0ckSession?.csrf_token
|
||||
},
|
||||
body: JSON.stringify({ rating: targetRating })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
if (!window._ratingDebounceTimers || !window._ratingDebounceTimers[postid]) {
|
||||
window._activeRatingCycleRequestsCount = 0;
|
||||
ratingEl._activeRequestsCount = 0;
|
||||
}
|
||||
|
||||
if (res && res.success && window.invalidateItemCache) {
|
||||
window.invalidateItemCache(postid);
|
||||
}
|
||||
|
||||
const currentIdStr = window.getCurrentItemId();
|
||||
const currentPostId = currentIdStr ? parseInt(currentIdStr, 10) : null;
|
||||
if (currentPostId !== postid) return;
|
||||
|
||||
if (ratingEl._lastCycleReqId !== reqId) return; // ignore stale responses
|
||||
if (res.success) {
|
||||
if (res.tags && window.renderTags) {
|
||||
window.renderTags(res.tags);
|
||||
}
|
||||
const curMedia = document.querySelector('.media-object');
|
||||
if (curMedia && res.rating) {
|
||||
curMedia.dataset.mode = res.rating;
|
||||
}
|
||||
} else {
|
||||
revert();
|
||||
window.flashMessage('Error: ' + (res.msg || 'Failed to update rating'), 3000, 'error');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
window._activeRatingCycleRequestsCount = Math.max(0, (window._activeRatingCycleRequestsCount || 1) - 1);
|
||||
ratingEl._activeRequestsCount = Math.max(0, (ratingEl._activeRequestsCount || 1) - 1);
|
||||
|
||||
const currentIdStr = window.getCurrentItemId();
|
||||
const currentPostId = currentIdStr ? parseInt(currentIdStr, 10) : null;
|
||||
if (currentPostId !== postid) return;
|
||||
|
||||
if (ratingEl._lastCycleReqId !== reqId) return;
|
||||
console.error('[RATING_TOGGLE_ERROR]', err);
|
||||
revert();
|
||||
window.flashMessage('Failed to toggle rating', 3000, 'error');
|
||||
});
|
||||
}, 200);
|
||||
|
||||
function revert() {
|
||||
ratingEl.className = '';
|
||||
oldClasses.forEach(cls => ratingEl.classList.add(cls));
|
||||
ratingEl.innerHTML = oldInnerHTML;
|
||||
if (oldDatasetRating !== undefined) ratingEl.dataset.rating = oldDatasetRating;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('popstate', (e) => {
|
||||
// Swallow popstate events fired during Onara close (history.go(-N) unwinding)
|
||||
if (window._onaraSuppressPopstate) return;
|
||||
@@ -5415,7 +5422,13 @@ window.cancelAnimFrame = (function () {
|
||||
"ArrowRight": clickOnNavBinding("#next"),
|
||||
"d": clickOnNavBinding("#next"),
|
||||
"r": clickOnElementBinding("#random, #nav-random"),
|
||||
"p": clickOnElementBinding("button#a_toggle"),
|
||||
"p": (e) => {
|
||||
if (e && e.preventDefault) e.preventDefault();
|
||||
const ratingEl = document.querySelector('.rating-tag.can-cycle, button#a_toggle');
|
||||
if (ratingEl && window.cycleRating) {
|
||||
window.cycleRating(ratingEl);
|
||||
}
|
||||
},
|
||||
"f": () => {
|
||||
if (localStorage.getItem('fForFullscreen') === 'true') {
|
||||
const fsBtn = document.querySelector('.v0ck_fs_btn');
|
||||
@@ -9616,8 +9629,9 @@ class NotificationSystem {
|
||||
}
|
||||
|
||||
// Ignore incoming live SSE tag updates if an optimistic rating cycle is currently active on the page
|
||||
const toggleBtn = document.querySelector('button#a_toggle');
|
||||
if (toggleBtn && toggleBtn._activeRequestsCount > 0) {
|
||||
const toggleBtn = document.querySelector('button#a_toggle, .rating-tag');
|
||||
const hasPendingDebounce = !!(window._ratingDebounceTimers && window._ratingDebounceTimers[data.item_id]);
|
||||
if (hasPendingDebounce || (window._activeRatingCycleRequestsCount > 0) || (toggleBtn && toggleBtn._activeRequestsCount > 0)) {
|
||||
window.f0ckDebug("[NotificationSystem] Live Tag Update ignored - Optimistic rating cycle in progress.");
|
||||
return;
|
||||
}
|
||||
@@ -9653,21 +9667,60 @@ class NotificationSystem {
|
||||
// Cast to boolean to handle potentially numeric truthy values (1/0)
|
||||
const isAdminBySession = !!(window.f0ckSession?.is_admin || window.f0ckSession?.is_moderator);
|
||||
const hasSession = !!window.f0ckSession;
|
||||
const canManageItem = !!(
|
||||
document.querySelector('#tags[data-can-manage="true"]') ||
|
||||
isAdminBySession ||
|
||||
(window.f0ckSession && window.f0ckSession.user && document.querySelector('#a_username[data-username]')?.dataset?.username?.toLowerCase() === window.f0ckSession.user.toLowerCase())
|
||||
);
|
||||
|
||||
window.f0ckDebug(`[NotificationSystem] Rendering ${data.tags.length} tags. isAdmin: ${isAdminBySession}, hasSession: ${hasSession}`);
|
||||
// Deduplicate rating tags: guarantee at most one rating tag is kept
|
||||
const ratingTags = data.tags.filter(t => ['sfw', 'nsfw', 'nsfl'].includes(t.normalized));
|
||||
const lastRatingTag = ratingTags.length ? ratingTags[ratingTags.length - 1] : null;
|
||||
const cleanTags = data.tags.filter(t => !['sfw', 'nsfw', 'nsfl'].includes(t.normalized) || t === lastRatingTag);
|
||||
|
||||
window.f0ckDebug(`[NotificationSystem] Rendering ${cleanTags.length} tags. isAdmin: ${isAdminBySession}, hasSession: ${hasSession}`);
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
data.tags.forEach(tag => {
|
||||
|
||||
const hasRatingTag = !!lastRatingTag;
|
||||
if (!hasRatingTag) {
|
||||
const untaggedSpan = document.createElement('span');
|
||||
untaggedSpan.className = `badge badge-untagged rating-tag is-untagged${canManageItem ? ' can-cycle' : ''}`;
|
||||
untaggedSpan.dataset.rating = 'untagged';
|
||||
const lbl = document.createElement('span');
|
||||
lbl.className = 'rating-label';
|
||||
lbl.textContent = 'untagged';
|
||||
untaggedSpan.appendChild(lbl);
|
||||
fragment.appendChild(untaggedSpan);
|
||||
}
|
||||
|
||||
cleanTags.forEach(tag => {
|
||||
const span = document.createElement('span');
|
||||
span.className = `badge ${tag.badge}`;
|
||||
if (hasSession) span.setAttribute('tooltip', tag.display_name || tag.user);
|
||||
const isRating = ['sfw', 'nsfw', 'nsfl'].includes(tag.normalized);
|
||||
if (isRating) {
|
||||
span.classList.add('rating-tag', `is-${tag.normalized}`);
|
||||
span.dataset.rating = tag.normalized;
|
||||
if (canManageItem) {
|
||||
span.classList.add('can-cycle');
|
||||
}
|
||||
} else if (hasSession) {
|
||||
span.setAttribute('tooltip', tag.display_name || tag.user);
|
||||
}
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = `/tag/${tag.normalized}`;
|
||||
a.textContent = tag.tag;
|
||||
span.appendChild(a);
|
||||
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.textContent = tag.tag;
|
||||
}
|
||||
span.appendChild(contentEl);
|
||||
|
||||
if (isAdminBySession) {
|
||||
if (isAdminBySession && !isRating) {
|
||||
// Match template exactly: <a class="removetag admin-deltag" href="#"><i class="fa-solid fa-xmark"></i></a>
|
||||
span.insertAdjacentHTML('beforeend', ' <a class="removetag admin-deltag" href="#"><i class="fa-solid fa-xmark"></i></a>');
|
||||
}
|
||||
@@ -9681,6 +9734,14 @@ class NotificationSystem {
|
||||
} else {
|
||||
inner.appendChild(fragment);
|
||||
}
|
||||
|
||||
// Safeguard against duplicate rating tags in DOM
|
||||
const allRatingBadges = inner.querySelectorAll('.rating-tag, .badge-success, .badge-danger, .badge-nsfl, .badge-untagged, [data-rating]');
|
||||
if (allRatingBadges.length > 1) {
|
||||
for (let i = 1; i < allRatingBadges.length; i++) {
|
||||
allRatingBadges[i].remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user