This commit is contained in:
2026-09-26 15:23:36 +02:00
parent 990f463534
commit 27f5e3563f
18 changed files with 1226 additions and 559 deletions
+186 -3
View File
@@ -10277,6 +10277,16 @@ table img {
border-color: rgba(255, 107, 157, 0.4) !important; border-color: rgba(255, 107, 157, 0.4) !important;
} }
@keyframes fav-pop {
0% { transform: scale(1); }
45% { transform: scale(1.32); }
100% { transform: scale(1); }
}
.fav-pop {
animation: fav-pop 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) !important;
}
/* pin icon */ /* pin icon */
.iconset#a_pin { .iconset#a_pin {
color: inherit; color: inherit;
@@ -17155,6 +17165,26 @@ body.layout-legacy .gapRight>.iconset {
transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease, opacity 0.15s ease; transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease, opacity 0.15s ease;
} }
.gapRight i.iconset.fa-regular,
.gapRight svg.iconset.fa-regular,
.gapRight i.iconset.far,
.gapRight i.fa-regular,
.gapRight i.far,
body.layout-legacy .gapRight>i.iconset.fa-regular,
body.layout-modern .gapRight>i.iconset.fa-regular {
font-weight: 400 !important;
}
.gapRight i.iconset.fa-regular::before,
.gapRight svg.iconset.fa-regular::before,
.gapRight i.iconset.far::before,
.gapRight i.fa-regular::before,
.gapRight i.far::before,
body.layout-legacy .gapRight>i.iconset.fa-regular::before,
body.layout-modern .gapRight>i.iconset.fa-regular::before {
font-weight: 400 !important;
}
.gapRight>svg.iconset:hover, .gapRight>svg.iconset:hover,
.gapRight>i.iconset:hover, .gapRight>i.iconset:hover,
.gapRight>button:hover, .gapRight>button:hover,
@@ -17618,6 +17648,33 @@ textarea#profile_description {
transform: scale(1.1); transform: scale(1.1);
} }
.user-infobox-block #a_favo.fa-regular,
.user-infobox-block .gapRight #a_favo.fa-regular,
.user-infobox-block .user-infobox-actions #a_favo.fa-regular,
body.layout-legacy .user-infobox-block .gapRight > #a_favo.fa-regular,
body.layout-modern .user-infobox-block .gapRight > #a_favo.fa-regular {
font-weight: 400 !important;
background: transparent !important;
background-color: transparent !important;
}
.user-infobox-block #a_favo.fa-regular:hover,
.user-infobox-block .gapRight #a_favo.fa-regular:hover,
.user-infobox-block .user-infobox-actions #a_favo.fa-regular:hover,
body.layout-legacy .user-infobox-block .gapRight > #a_favo.fa-regular:hover,
body.layout-modern .user-infobox-block .gapRight > #a_favo.fa-regular:hover {
background: transparent !important;
background-color: transparent !important;
}
.user-infobox-block #a_favo.fa-regular::before,
.user-infobox-block .gapRight #a_favo.fa-regular::before,
.user-infobox-block .user-infobox-actions #a_favo.fa-regular::before,
body.layout-legacy .user-infobox-block .gapRight > #a_favo.fa-regular::before,
body.layout-modern .user-infobox-block .gapRight > #a_favo.fa-regular::before {
font-weight: 400 !important;
}
.user-infobox-username { .user-infobox-username {
font-weight: 700; font-weight: 700;
color: var(--author-accent, inherit) !important; color: var(--author-accent, inherit) !important;
@@ -22487,20 +22544,146 @@ div#flash,
} }
} }
/* Chan Rehost Details Modal */ /* Chan Rehost Details Modal / Popup */
.chan-rehost-modal { .chan-rehost-modal {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
background: rgba(0, 0, 0, 0.82); background: transparent;
backdrop-filter: blur(6px); backdrop-filter: none;
z-index: 100200; z-index: 100200;
display: block;
}
.chan-rehost-modal .chan-rehost-modal-content,
.chan-rehost-modal.chan-rehost-popup-mode .chan-rehost-modal-content {
position: fixed !important;
width: 270px !important;
max-width: calc(100vw - 20px) !important;
background: #18181b !important;
border: 1px solid rgba(255, 255, 255, 0.18) !important;
border-radius: 10px !important;
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.85), 0 0 0 1px rgba(255, 255, 255, 0.08) !important;
padding: 10px 12px !important;
text-align: left !important;
box-sizing: border-box !important;
animation: chanRehostPopupIn 0.12s cubic-bezier(0.16, 1, 0.3, 1);
z-index: 100201 !important;
}
@keyframes chanRehostPopupIn {
from {
opacity: 0;
transform: scale(0.96) translateY(3px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.chan-rehost-popup-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
padding-bottom: 6px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.chan-rehost-popup-title {
font-size: 0.78rem;
font-weight: 700;
color: #d4d4d8;
display: flex;
align-items: center;
gap: 6px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.chan-rehost-popup-title i {
color: #4ade80;
font-size: 0.82rem;
}
.chan-rehost-popup-close {
background: none;
border: none;
color: #71717a;
font-size: 1.15rem;
cursor: pointer;
padding: 0 4px;
line-height: 1;
transition: color 0.15s;
}
.chan-rehost-popup-close:hover {
color: #fff;
}
.chan-rehost-ratings.chan-rehost-popup-ratings {
display: flex;
gap: 4px;
margin-bottom: 8px;
}
.chan-rehost-ratings.chan-rehost-popup-ratings .chan-rehost-pill {
flex: 1;
padding: 6px 2px;
font-size: 0.72rem;
font-weight: 600;
border-radius: 5px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.05);
color: #a1a1aa;
cursor: pointer;
transition: all 0.12s ease;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 3px;
white-space: nowrap;
} }
.chan-rehost-ratings.chan-rehost-popup-ratings .chan-rehost-pill:hover {
background: rgba(255, 255, 255, 0.12);
color: #fff;
}
.chan-rehost-popup-actions {
display: flex;
gap: 6px;
}
.chan-rehost-popup-save-btn {
width: 100%;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
background: #16a34a !important;
color: #fff !important;
border: none !important;
border-radius: 6px;
padding: 6px 12px;
font-size: 0.8rem;
font-weight: 600;
cursor: pointer;
transition: background 0.15s, opacity 0.15s;
}
.chan-rehost-popup-save-btn:hover {
background: #15803d !important;
}
.chan-rehost-popup-save-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.chan-rehost-modal-content { .chan-rehost-modal-content {
background: #18181b; background: #18181b;
border: 1px solid rgba(255, 255, 255, 0.14); border: 1px solid rgba(255, 255, 255, 0.14);
+129 -37
View File
@@ -284,6 +284,8 @@
}; };
let favSeq = 0;
const toggleFavEvent = async (e) => { const toggleFavEvent = async (e) => {
if (e && typeof e.preventDefault === 'function') e.preventDefault(); if (e && typeof e.preventDefault === 'function') e.preventDefault();
if (window.f0ckSession?.is_anon && window.f0ckSession?.anon_permissions && window.f0ckSession.anon_permissions.favorite === false) { if (window.f0ckSession?.is_anon && window.f0ckSession?.anon_permissions && window.f0ckSession.anon_permissions.favorite === false) {
@@ -300,7 +302,12 @@
const chanUrl = firstFavoBtn?.dataset?.chanUrl || chanRehostBtn?.dataset?.url; const chanUrl = firstFavoBtn?.dataset?.chanUrl || chanRehostBtn?.dataset?.url;
let localId = firstFavoBtn?.dataset?.localId; let localId = firstFavoBtn?.dataset?.localId;
const ctx = getContext();
const postid = localId ? Number(localId) : ctx?.postid;
if (!postid && !chanUrl) return;
const wasAlreadyFav = favoBtns[0].classList.contains('fa-solid') && !favoBtns[0].classList.contains('fa-spinner'); const wasAlreadyFav = favoBtns[0].classList.contains('fa-solid') && !favoBtns[0].classList.contains('fa-spinner');
const isNowFav = !wasAlreadyFav;
const setFavoUi = (isFav) => { const setFavoUi = (isFav) => {
favoBtns.forEach(btn => { favoBtns.forEach(btn => {
@@ -308,20 +315,98 @@
btn.classList.add('iconset', 'fa-heart'); btn.classList.add('iconset', 'fa-heart');
btn.classList.toggle('fa-solid', isFav); btn.classList.toggle('fa-solid', isFav);
btn.classList.toggle('fa-regular', !isFav); btn.classList.toggle('fa-regular', !isFav);
btn.title = isFav ? (window.f0ckI18n?.fav_remove || 'Remove from favorites') : (window.f0ckI18n?.fav_add || 'Favorite');
}); });
}; };
const setFavoLoading = () => { // 1. Instantly apply client UI
setFavoUi(isNowFav);
// Micro-animation for tactile pop
favoBtns.forEach(btn => { favoBtns.forEach(btn => {
btn.classList.remove('fa-heart'); btn.classList.remove('fav-pop');
btn.classList.add('iconset', 'fa-solid', 'fa-spinner', 'fa-spin'); void btn.offsetWidth;
btn.classList.add('fav-pop');
}); });
// Instant flash message & vibration feedback
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);
if (postid && window.invalidateItemCache) {
window.invalidateItemCache(postid);
}
// Optimistically update #favs badge list
const favcontainer = document.querySelector('#favs');
const prevFavsHtml = favcontainer ? favcontainer.innerHTML : '';
const prevFavsHidden = favcontainer ? favcontainer.hidden : true;
if (favcontainer) {
const currentUser = (window.f0ckSession?.user || '').toLowerCase();
const isAnon = !!(window.f0ckSession?.is_anon || window.f0ckSession?.user === 'anonymous');
const isAnonymized = !!(window.f0ckSession?.is_anonymized ?? (window.f0ckSession?.guest_anonymize && !window.f0ckSession?.logged_in) ?? (window.f0ckSession?.is_anon && window.f0ckSession?.anon_anonymize));
const findSelfBadge = () => {
const selfBadges = favcontainer.querySelectorAll('[data-self="true"]');
if (selfBadges.length) return selfBadges[0];
if (currentUser && currentUser !== 'anonymous') {
const links = favcontainer.querySelectorAll('a[href]');
for (const a of links) {
const path = a.getAttribute('href') || '';
if (path.toLowerCase().endsWith('/user/' + currentUser)) return a;
}
}
return null;
}; };
if (!isNowFav) {
const selfBadge = findSelfBadge();
if (selfBadge) selfBadge.remove();
if (favcontainer.children.length === 0) {
favcontainer.hidden = true;
}
} else {
let selfBadge = findSelfBadge();
if (!selfBadge) {
selfBadge = document.createElement('a');
selfBadge.dataset.self = 'true';
selfBadge.setAttribute('flow', 'up');
if (isAnonymized || isAnon) {
selfBadge.className = 'ghost-fav';
selfBadge.setAttribute('tooltip', 'anonymous');
selfBadge.style.cursor = 'default';
const img = document.createElement('img');
img.src = '/a/default.png';
img.style.height = "32px";
img.style.width = "32px";
selfBadge.appendChild(img);
} else {
selfBadge.href = `/user/${currentUser}`;
selfBadge.setAttribute('tooltip', window.f0ckSession?.display_name || window.f0ckSession?.user || 'anonymous');
const img = document.createElement('img');
const avatarFile = window.f0ckSession?.avatar_file;
const avatar = window.f0ckSession?.avatar;
img.src = avatarFile ? `/a/${avatarFile}` : (avatar ? `/t/${avatar}.webp` : '/a/default.png');
img.style.height = "32px";
img.style.width = "32px";
if (window.f0ckSession?.username_color) img.style.borderColor = window.f0ckSession.username_color;
selfBadge.appendChild(img);
}
favcontainer.appendChild(selfBadge);
}
favcontainer.hidden = false;
}
}
const mySeq = ++favSeq;
// 2. Run server operation in background — can take as long as it needs
(async () => {
try {
// If viewing un-rehosted 4chan post, auto-rehost first // If viewing un-rehosted 4chan post, auto-rehost first
if (isChan && !localId && chanUrl) { if (isChan && !localId && chanUrl) {
setFavoLoading(); if (mySeq !== favSeq) return;
try {
const csrfToken = window.f0ckSession?.csrf_token || ''; const csrfToken = window.f0ckSession?.csrf_token || '';
const rehostResp = await fetch('/api/v2/scroller/rehost', { const rehostResp = await fetch('/api/v2/scroller/rehost', {
method: 'POST', method: 'POST',
@@ -362,48 +447,42 @@
chanRehostBtn.outerHTML = ` chanRehostBtn.outerHTML = `
<span class="chan-rehosted-wrap" style="display:inline-flex;gap:4px;align-items:center;"> <span class="chan-rehosted-wrap" style="display:inline-flex;gap:4px;align-items:center;">
<a href="/${localId}" class="chan-rehost-action-btn rehosted" title="Already rehosted on f0ckm (Post #${localId})" style="display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;"><i class="fa-solid fa-check"></i> #${localId}</a> <a href="/${localId}" class="chan-rehost-action-btn rehosted" title="Already rehosted on f0ckm (Post #${localId})" style="display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;"><i class="fa-solid fa-check"></i> #${localId}</a>
<button type="button" class="chan-rehost-edit-btn" data-item-id="${localId}" title="Edit rating, tags & comment" style="display:inline-flex;align-items:center;padding:3px 7px;border-radius:6px;font-size:0.8rem;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;"><i class="fa-solid fa-pen"></i></button> <button type="button" class="chan-rehost-edit-btn" data-item-id="${localId}" title="Edit rating" style="display:inline-flex;align-items:center;padding:3px 7px;border-radius:6px;font-size:0.8rem;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;"><i class="fa-solid fa-pen"></i></button>
</span> </span>
`; `;
} }
} else { } else {
throw new Error(rehostData.msg || 'Rehost failed'); throw new Error(rehostData.msg || 'Rehost failed');
} }
} catch (err) {
console.error('[CHAN-FAV-ADMIN] Auto-rehost error:', err);
setFavoUi(wasAlreadyFav);
if (typeof window.flashMessage === 'function') {
window.flashMessage('Failed to favorite post', 3000, 'error');
}
return;
}
} }
const ctx = getContext(); const effectivePostId = localId ? Number(localId) : (postid || getContext()?.postid);
if (!ctx) { if (!effectivePostId) {
setFavoUi(wasAlreadyFav); throw new Error('Missing post ID');
return;
} }
const postid = localId ? Number(localId) : ctx.postid;
try { if (mySeq !== favSeq) return;
const res = await post('/api/v2/togglefav', { const res = await post('/api/v2/togglefav', {
postid: postid, postid: effectivePostId,
chan_url: chanUrl chan_url: chanUrl,
action: isNowFav ? 'add' : 'delete',
favorited: isNowFav
}); });
if (mySeq !== favSeq) return;
if (res && res.success) { if (res && res.success) {
if (window.invalidateItemCache) { if (window.invalidateItemCache) {
window.invalidateItemCache(postid); window.invalidateItemCache(effectivePostId);
} }
// Authoritative server state takes priority; fallback to inverting previous UI state const finalIsFav = (res.favorited !== undefined) ? !!res.favorited : isNowFav;
const isNowFav = (res.favorited !== undefined) ? !!res.favorited : !wasAlreadyFav; setFavoUi(finalIsFav);
setFavoUi(isNowFav); const curFavContainer = document.querySelector('#favs');
if (curFavContainer && Array.isArray(res.favs)) {
const favcontainer = document.querySelector('#favs'); curFavContainer.innerHTML = "";
if (favcontainer) { if (res.favs.length > 0) {
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 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(); const fragment = document.createDocumentFragment();
res.favs.forEach(f => { res.favs.forEach(f => {
@@ -431,6 +510,7 @@
} else if (isAnonymized || isFavAnon) { } else if (isAnonymized || isFavAnon) {
const a = document.createElement('a'); const a = document.createElement('a');
a.className = 'ghost-fav'; a.className = 'ghost-fav';
if (isSelf) a.dataset.self = 'true';
a.setAttribute('tooltip', 'anonymous'); a.setAttribute('tooltip', 'anonymous');
a.setAttribute('flow', 'up'); a.setAttribute('flow', 'up');
a.style.cursor = 'default'; a.style.cursor = 'default';
@@ -444,6 +524,7 @@
fragment.appendChild(a); fragment.appendChild(a);
} else { } else {
const a = document.createElement('a'); const a = document.createElement('a');
if (isSelf) a.dataset.self = 'true';
a.href = `/user/${(f.user || '').toLowerCase()}`; a.href = `/user/${(f.user || '').toLowerCase()}`;
a.setAttribute('tooltip', f.display_name || f.user || 'anonymous'); a.setAttribute('tooltip', f.display_name || f.user || 'anonymous');
a.setAttribute('flow', 'up'); a.setAttribute('flow', 'up');
@@ -458,28 +539,39 @@
fragment.appendChild(a); fragment.appendChild(a);
} }
}); });
favcontainer.appendChild(fragment); curFavContainer.appendChild(fragment);
favcontainer.hidden = false; curFavContainer.hidden = false;
} else { } else {
favcontainer.hidden = true; curFavContainer.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 { } else {
if (mySeq !== favSeq) return;
setFavoUi(wasAlreadyFav); setFavoUi(wasAlreadyFav);
const curFavContainer = document.querySelector('#favs');
if (curFavContainer) {
curFavContainer.innerHTML = prevFavsHtml;
curFavContainer.hidden = prevFavsHidden;
}
const errMsg = (res && (res.msg || res.error)) || 'Anonymous favoriting is disabled.'; const errMsg = (res && (res.msg || res.error)) || 'Anonymous favoriting is disabled.';
if (typeof window.flashMessage === 'function') { if (typeof window.flashMessage === 'function') {
window.flashMessage(errMsg, 3000, 'error'); window.flashMessage(errMsg, 3000, 'error');
} }
} }
} catch (err) { } catch (err) {
console.error('[CHAN-FAV-ADMIN] Error during background favorite sync:', err);
if (mySeq !== favSeq) return;
setFavoUi(wasAlreadyFav); setFavoUi(wasAlreadyFav);
const curFavContainer = document.querySelector('#favs');
if (curFavContainer) {
curFavContainer.innerHTML = prevFavsHtml;
curFavContainer.hidden = prevFavsHidden;
}
if (typeof window.flashMessage === 'function') { if (typeof window.flashMessage === 'function') {
window.flashMessage('Failed to update favorite.', 3000, 'error'); window.flashMessage('Failed to update favorite.', 3000, 'error');
} }
} }
})();
}; };
const deleteSubItemEvent = async (subf0ck, postid) => { const deleteSubItemEvent = async (subf0ck, postid) => {
+6 -1
View File
@@ -578,7 +578,12 @@
if (typeof window.showToastNotification === 'function') window.showToastNotification('New passkey registered!'); if (typeof window.showToastNotification === 'function') window.showToastNotification('New passkey registered!');
this._refreshModalContent(); this._refreshModalContent();
} catch (err) { } catch (err) {
if (err.name !== 'NotAllowedError') alert('Failed to add passkey: ' + err.message); console.warn('[ANON_PASSKEY] Modal add passkey error:', err);
if (err && err.name === 'NotAllowedError') {
alert('Passkey creation was cancelled or blocked. If Bitwarden or another password manager is locked, please unlock it.');
} else {
alert('Failed to add passkey: ' + ((err && err.message) || 'Unknown error'));
}
} finally { } finally {
addBtn.disabled = false; addBtn.disabled = false;
} }
+288 -132
View File
@@ -1924,6 +1924,39 @@ window.cancelAnimFrame = (function () {
// Clear onara-active from any and all elements to guarantee only 1 item is selected // Clear onara-active from any and all elements to guarantee only 1 item is selected
document.querySelectorAll('.onara-active').forEach(el => el.classList.remove('onara-active')); document.querySelectorAll('.onara-active').forEach(el => el.classList.remove('onara-active'));
// Check for 4chan thread post card in the background thread
let postNo = null;
if (url) {
const m = new URL(url, window.location.origin).pathname.match(/^\/4\/[a-z0-9_]+\/\d+\/(\d+)/i);
if (m) postNo = m[1];
}
if (!postNo && itemid && /^\d+$/.test(String(itemid))) {
postNo = String(itemid);
}
let targetChanCard = null;
if (postNo) {
targetChanCard = document.getElementById('p' + postNo) || document.querySelector(`.chan-post-card[data-no="${postNo}"]`);
}
if (targetChanCard) {
document.querySelectorAll('.chan-post-card.active-thread-post').forEach(el => el.classList.remove('active-thread-post'));
targetChanCard.classList.add('active-thread-post', 'onara-active');
const chanThumb = targetChanCard.querySelector('.chan-thumb-link');
if (chanThumb) chanThumb.classList.add('onara-active');
if (document.activeElement && document.activeElement !== targetChanCard && typeof document.activeElement.blur === 'function') {
document.activeElement.blur();
}
try {
targetChanCard.scrollIntoView({ block: 'center', behavior: forceScroll ? 'instant' : 'smooth' });
} catch (_) {
targetChanCard.scrollIntoView();
}
return targetChanCard;
}
let targetThumb = null; let targetThumb = null;
if (url) { if (url) {
try { try {
@@ -2177,6 +2210,47 @@ window.cancelAnimFrame = (function () {
const currentSeq = ++_onaraSyncSeq; const currentSeq = ++_onaraSyncSeq;
const urlObj = new URL(url || window.location.href, window.location.origin); const urlObj = new URL(url || window.location.href, window.location.origin);
if (urlObj.pathname.startsWith('/4/')) {
const chanStream = document.querySelector('.chan-posts-stream');
const chanM = urlObj.pathname.match(/^\/4\/([a-z0-9_]+)\/(\d+)/i);
let sameThread = false;
if (chanStream && chanM) {
const currentUrlMatch = window.location.pathname.match(/^\/4\/([a-z0-9_]+)\/(\d+)/i);
if (currentUrlMatch && currentUrlMatch[1].toLowerCase() === chanM[1].toLowerCase() && currentUrlMatch[2] === chanM[2]) {
sameThread = true;
} else if (document.querySelector('.chan-back-btn, .sidebar-chan-overview-btn, a[href*="/4/' + chanM[1] + '/' + chanM[2] + '"]')) {
sameThread = true;
}
}
if (sameThread) {
updateOnaraActiveItem(itemid, url, false, slug, extraMeta);
return;
}
if (chanM) {
const threadUrl = `/4/${chanM[1]}/${chanM[2]}`;
try {
const pageRes = await fetch(threadUrl, {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
if (pageRes.ok) {
const html = await pageRes.text();
const doc = new DOMParser().parseFromString(html, 'text/html');
const main = document.getElementById('main');
const newMain = doc.querySelector('#main');
if (main && newMain) {
main.className = newMain.className;
main.innerHTML = newMain.innerHTML;
window._onaraReturnUrl = threadUrl;
if (doc.title) window._onaraReturnTitle = doc.title;
updateOnaraActiveItem(itemid, url, true, slug, extraMeta);
}
}
} catch (e) {
console.warn('[ONARA] Failed to sync chan thread:', e);
}
}
return;
}
const feedBasePath = urlObj.pathname.replace(/\/+$/, '').replace(/\/(?:\d+|[a-zA-Z0-9_-]{11})$/, '') || '/'; const feedBasePath = urlObj.pathname.replace(/\/+$/, '').replace(/\/(?:\d+|[a-zA-Z0-9_-]{11})$/, '') || '/';
let targetPage = (typeof knownPage === 'number' && knownPage >= 1) ? knownPage : null; let targetPage = (typeof knownPage === 'number' && knownPage >= 1) ? knownPage : null;
@@ -2364,6 +2438,10 @@ window.cancelAnimFrame = (function () {
} catch {} } catch {}
} }
if (path.startsWith('/4/')) {
return path;
}
const postsEl = document.querySelector('.posts'); const postsEl = document.querySelector('.posts');
const activePagSpan = document.querySelector('.pagination span.btn.disabled, .pagination span.disabled'); const activePagSpan = document.querySelector('.pagination span.btn.disabled, .pagination span.disabled');
const pagPage = activePagSpan ? parseInt(activePagSpan.textContent.trim(), 10) : 0; const pagPage = activePagSpan ? parseInt(activePagSpan.textContent.trim(), 10) : 0;
@@ -2452,7 +2530,7 @@ window.cancelAnimFrame = (function () {
window._onaraReturnTitle = null; window._onaraReturnTitle = null;
// Maintain focus on active item so user TAB navigation seamlessly resumes from current progress // Maintain focus on active item so user TAB navigation seamlessly resumes from current progress
const activeThumb = document.querySelector('.posts > a.thumb.onara-active'); const activeThumb = document.querySelector('.posts > a.thumb.onara-active, .chan-post-card.active-thread-post .chan-thumb-link, .chan-post-card.onara-active .chan-thumb-link');
if (activeThumb) { if (activeThumb) {
try { try {
activeThumb.focus({ preventScroll: true }); activeThumb.focus({ preventScroll: true });
@@ -4437,7 +4515,7 @@ window.cancelAnimFrame = (function () {
const doc = new DOMParser().parseFromString(html, 'text/html'); const doc = new DOMParser().parseFromString(html, 'text/html');
const gridContent = doc.querySelector('#main') || doc.body; const gridContent = doc.querySelector('#main') || doc.body;
if (gridContent) { if (gridContent) {
main.className = ''; main.className = gridContent.className || '';
main.innerHTML = gridContent.innerHTML; main.innerHTML = gridContent.innerHTML;
if (typeof window.initLazyLoading === 'function') { if (typeof window.initLazyLoading === 'function') {
window.initLazyLoading(); window.initLazyLoading();
@@ -4452,7 +4530,7 @@ window.cancelAnimFrame = (function () {
} }
if (activeKey) { if (activeKey) {
updateOnaraActiveItem(activeKey, window.location.href); updateOnaraActiveItem(activeKey, window.location.href, true);
if (typeof window.trackVisit === 'function') { if (typeof window.trackVisit === 'function') {
window.trackVisit(activeKey); window.trackVisit(activeKey);
} }
@@ -4473,7 +4551,7 @@ window.cancelAnimFrame = (function () {
const keySegments = pathSegments.filter(s => /^\d+$/.test(s) || /^[a-zA-Z0-9_-]{11}$/.test(s)); const keySegments = pathSegments.filter(s => /^\d+$/.test(s) || /^[a-zA-Z0-9_-]{11}$/.test(s));
const activeKey = keySegments.length ? keySegments[keySegments.length - 1] : null; const activeKey = keySegments.length ? keySegments[keySegments.length - 1] : null;
if (activeKey) { if (activeKey) {
updateOnaraActiveItem(activeKey, window.location.href); updateOnaraActiveItem(activeKey, window.location.href, true);
if (typeof window.trackVisit === 'function') { if (typeof window.trackVisit === 'function') {
window.trackVisit(activeKey); window.trackVisit(activeKey);
} }
@@ -11261,7 +11339,10 @@ window.cancelAnimFrame = (function () {
if (isItem) { if (isItem) {
isNavigating = false; isNavigating = false;
return loadItemAjax(url, replace, options); const shouldInherit = (options.skipInherit || options.inheritContext === false) ? false : (options.inheritContext !== undefined ? !!options.inheritContext : false);
const itemOptions = { ...options };
if (replace) itemOptions.replace = true;
return loadItemAjax(url, shouldInherit, itemOptions);
} }
@@ -12369,7 +12450,12 @@ window.cancelAnimFrame = (function () {
// Prev/next arrows in item view always use explicit /h/foo/<id> or /tag/foo/<id> hrefs // Prev/next arrows in item view always use explicit /h/foo/<id> or /tag/foo/<id> hrefs
// (via link.main), so they are unaffected by this guard. // (via link.main), so they are unaffected by this guard.
// Note: Notification links call loadItemAjax(href, false) to skip inheritance directly. // Note: Notification links call loadItemAjax(href, false) to skip inheritance directly.
if (inheritContext && !isChanUrl) { const targetUrlObj = new URL(url, window.location.origin);
const targetPath = targetUrlObj.pathname;
const hasContextPrefix = /^\/(?:tag|h|user|favs|4)\//.test(targetPath);
const isBareItem = !hasContextPrefix;
if (inheritContext && !isChanUrl && !isBareItem && !options.skipInherit) {
if (!tag) { if (!tag) {
const wTagMatch = window.location.href.match(/\/tag\/([^/?]+)/); const wTagMatch = window.location.href.match(/\/tag\/([^/?]+)/);
if (wTagMatch) tag = decodeURIComponent(wTagMatch[1]); if (wTagMatch) tag = decodeURIComponent(wTagMatch[1]);
@@ -12592,6 +12678,8 @@ window.cancelAnimFrame = (function () {
promoteOnaraModals(); promoteOnaraModals();
const onaraModal = document.getElementById('onara-modal'); const onaraModal = document.getElementById('onara-modal');
if (onaraModal) onaraModal.scrollTo(0, 0); if (onaraModal) onaraModal.scrollTo(0, 0);
updateOnaraActiveItem(itemid, url, false, _cachedItem.slug, null);
syncOnaraBackgroundGrid(itemid, url, _cachedItem.page, _cachedItem.slug, null);
} else if (options.skipPush && history.state?.scroll !== undefined) { } else if (options.skipPush && history.state?.scroll !== undefined) {
requestAnimationFrame(() => window.scrollTo(0, history.state.scroll)); requestAnimationFrame(() => window.scrollTo(0, history.state.scroll));
} else if (!options.keepMedia) { } else if (!options.keepMedia) {
@@ -12601,7 +12689,10 @@ window.cancelAnimFrame = (function () {
const _hash = new URL(url, window.location.origin).hash; const _hash = new URL(url, window.location.origin).hash;
const itemKey = (window.f0ckSession?.enable_item_slugs !== false && _cachedItem.slug) ? _cachedItem.slug : (_cachedItem.slug || itemid); const itemKey = (window.f0ckSession?.enable_item_slugs !== false && _cachedItem.slug) ? _cachedItem.slug : (_cachedItem.slug || itemid);
let _pushUrl = `/${itemKey}`; let _pushUrl = `/${itemKey}`;
if (userHall && userHallOwner) _pushUrl = `/user/${encodeURIComponent(userHallOwner)}/hall/${encodeURIComponent(userHall)}/${itemKey}`; if (isChanUrl) {
const u = new URL(url, window.location.origin);
_pushUrl = u.pathname + u.search + u.hash;
} else if (userHall && userHallOwner) _pushUrl = `/user/${encodeURIComponent(userHallOwner)}/hall/${encodeURIComponent(userHall)}/${itemKey}`;
else if (user) { _pushUrl = `/user/${encodeURIComponent(user)}/${itemKey}`; if (isFavs) _pushUrl = `/user/${encodeURIComponent(user)}/favs/${itemKey}`; } else if (user) { _pushUrl = `/user/${encodeURIComponent(user)}/${itemKey}`; if (isFavs) _pushUrl = `/user/${encodeURIComponent(user)}/favs/${itemKey}`; }
else if (isFavs) _pushUrl = `/favs/${itemKey}`; else if (isFavs) _pushUrl = `/favs/${itemKey}`;
else if (tag) _pushUrl = `/tag/${encodeURIComponent(tag).replace(/%2C/g,',').replace(/%20/g,' ')}/${itemKey}`; else if (tag) _pushUrl = `/tag/${encodeURIComponent(tag).replace(/%2C/g,',').replace(/%20/g,' ')}/${itemKey}`;
@@ -12952,8 +13043,12 @@ window.cancelAnimFrame = (function () {
if (!options.keepMedia && !options.skipPush) { if (!options.keepMedia && !options.skipPush) {
// We overwrite proper URL even if the link clicked was "naked" // We overwrite proper URL even if the link clicked was "naked"
if (options.replace || options.replaceState) {
history.replaceState({ onara: useOnara }, '', pushUrl);
} else {
history.pushState({ onara: useOnara }, '', pushUrl); history.pushState({ onara: useOnara }, '', pushUrl);
} }
}
if (!options.keepMedia) { if (!options.keepMedia) {
setupMedia(); setupMedia();
@@ -13507,65 +13602,110 @@ window.cancelAnimFrame = (function () {
} }
} }
window.openChanRehostModal = function({ itemId, isPlayerView = false, onSave = null }) { window.openChanRehostModal = function({ itemId, onSave = null, anchorEl = null }) {
if (!itemId) return; if (!itemId) return;
let modal = document.getElementById('chan-rehost-modal'); let modal = document.getElementById('chan-rehost-modal');
if (!modal) { if (!modal) {
modal = document.createElement('div'); modal = document.createElement('div');
modal.id = 'chan-rehost-modal'; modal.id = 'chan-rehost-modal';
modal.className = 'modal-overlay chan-rehost-modal';
modal.style.display = 'none';
document.body.appendChild(modal); document.body.appendChild(modal);
} }
if (!anchorEl) {
anchorEl = document.querySelector(`.chan-rehost-edit-btn[data-item-id="${itemId}"]`) ||
document.querySelector(`.chan-rehost-btn[href="/${itemId}"]`) ||
document.querySelector(`.chan-rehost-action-btn[href="/${itemId}"]`) ||
document.querySelector(`#rehost-box-${itemId} .chan-rehosted-wrap`) ||
document.querySelector(`#rehost-box-${itemId}`) ||
document.getElementById('chan-item-rehost-btn');
}
modal.className = 'modal-overlay chan-rehost-modal chan-rehost-popup-mode';
modal.style.display = 'block';
document.body.classList.remove('modal-open');
modal.innerHTML = ` modal.innerHTML = `
<div class="modal-content chan-rehost-modal-content"> <div class="modal-content chan-rehost-modal-content chan-rehost-popup-content" role="dialog" aria-label="Rehost Rating">
<div class="chan-rehost-modal-header"> <div class="chan-rehost-popup-header">
<h3 class="chan-rehost-modal-title"> <span class="chan-rehost-popup-title">
<i class="fa-solid fa-cloud-arrow-up" style="color: #4ade80;"></i> <i class="fa-solid fa-shield-halved"></i>
<span>Rehost Settings</span> <span>Rating</span>
<a href="/${itemId}" target="_blank" class="chan-rehost-modal-post-link" title="Open post in new tab">#${itemId} <i class="fa-solid fa-arrow-up-right-from-square"></i></a> </span>
</h3> <button type="button" class="chan-rehost-modal-close chan-rehost-popup-close" aria-label="Close">&times;</button>
<button type="button" class="chan-rehost-modal-close" aria-label="Close">&times;</button>
</div> </div>
<div class="chan-rehost-modal-body"> <div class="chan-rehost-ratings chan-rehost-popup-ratings" id="chan-rehost-rating-group">
<div class="chan-rehost-form-group"> <button type="button" class="chan-rehost-pill untagged active" data-rating="untagged" title="Untagged"><i class="fa-solid fa-question"></i> Untagged</button>
<label><i class="fa-solid fa-shield-halved"></i> Rating</label> <button type="button" class="chan-rehost-pill sfw" data-rating="sfw" title="Safe for Work"><i class="fa-solid fa-shield"></i> SFW</button>
<div class="chan-rehost-ratings" id="chan-rehost-rating-group"> <button type="button" class="chan-rehost-pill nsfw" data-rating="nsfw" title="Not Safe for Work"><i class="fa-solid fa-eye-slash"></i> NSFW</button>
<button type="button" class="chan-rehost-pill untagged active" data-rating="untagged"><i class="fa-solid fa-question"></i> Untagged</button> <button type="button" class="chan-rehost-pill nsfl" data-rating="nsfl" title="Not Safe for Life"><i class="fa-solid fa-skull"></i> NSFL</button>
<button type="button" class="chan-rehost-pill sfw" data-rating="sfw"><i class="fa-solid fa-shield"></i> SFW</button>
<button type="button" class="chan-rehost-pill nsfw" data-rating="nsfw"><i class="fa-solid fa-eye-slash"></i> NSFW</button>
<button type="button" class="chan-rehost-pill nsfl" data-rating="nsfl"><i class="fa-solid fa-skull"></i> NSFL</button>
</div> </div>
</div> <div class="chan-rehost-popup-actions">
<button type="button" class="btn btn-primary chan-rehost-modal-save chan-rehost-popup-save-btn">
<div class="chan-rehost-form-group"> <i class="fa-solid fa-check"></i> Save
<label><i class="fa-solid fa-tags"></i> Tags (optional)</label> </button>
<input type="text" id="chan-rehost-tags-input" class="chan-rehost-input" placeholder="e.g. music, webm, anime (comma-separated)" autocomplete="off" />
</div>
<div class="chan-rehost-form-group">
<label><i class="fa-solid fa-comment"></i> First Comment (optional)</label>
<textarea id="chan-rehost-comment-input" class="chan-rehost-input chan-rehost-textarea" placeholder="Add a comment on this post..." rows="2"></textarea>
</div>
</div>
<div class="chan-rehost-modal-actions">
<button type="button" class="btn btn-secondary chan-rehost-modal-cancel">Close</button>
${isPlayerView ? `<button type="button" class="btn btn-info chan-rehost-modal-save-view" style="background:#0284c7;color:#fff;border:none;border-radius:6px;padding:8px 14px;cursor:pointer;font-weight:600;"><i class="fa-solid fa-play"></i> Save & View Post</button>` : ''}
<button type="button" class="btn btn-primary chan-rehost-modal-save" style="background:#16a34a;color:#fff;border:none;border-radius:6px;padding:8px 14px;cursor:pointer;font-weight:600;"><i class="fa-solid fa-check"></i> Save Details</button>
</div> </div>
</div> </div>
`; `;
modal.style.display = 'flex'; const content = modal.querySelector('.chan-rehost-modal-content');
document.body.classList.add('modal-open'); let positionHandler = null;
const updatePopupPosition = () => {
if (!content) return;
const cRect = content.getBoundingClientRect();
const margin = 8;
const pad = 8;
if (anchorEl && typeof anchorEl.getBoundingClientRect === 'function') {
const rect = anchorEl.getBoundingClientRect();
let left = rect.left + (rect.width / 2) - (cRect.width / 2);
if (left < pad) left = pad;
if (left + cRect.width > window.innerWidth - pad) {
left = window.innerWidth - cRect.width - pad;
}
let top;
if (rect.top >= cRect.height + margin + pad) {
top = rect.top - cRect.height - margin;
content.dataset.pos = 'above';
} else {
top = rect.bottom + margin;
content.dataset.pos = 'below';
}
if (top + cRect.height > window.innerHeight - pad) {
top = window.innerHeight - cRect.height - pad;
}
if (top < pad) top = pad;
content.style.position = 'fixed';
content.style.left = `${Math.round(left)}px`;
content.style.top = `${Math.round(top)}px`;
content.style.right = 'auto';
content.style.bottom = 'auto';
content.style.margin = '0';
} else {
content.style.position = 'fixed';
content.style.left = `${Math.round((window.innerWidth - cRect.width) / 2)}px`;
content.style.top = `${Math.round((window.innerHeight - cRect.height) / 2)}px`;
}
};
updatePopupPosition();
requestAnimationFrame(updatePopupPosition);
setTimeout(updatePopupPosition, 25);
setTimeout(updatePopupPosition, 100);
positionHandler = () => requestAnimationFrame(updatePopupPosition);
window.addEventListener('scroll', positionHandler, { passive: true, capture: true });
window.addEventListener('resize', positionHandler, { passive: true });
const ratingGroup = modal.querySelector('#chan-rehost-rating-group'); const ratingGroup = modal.querySelector('#chan-rehost-rating-group');
let selectedRating = 'untagged'; let selectedRating = 'untagged';
let userSelectedRating = false; let userSelectedRating = false;
let userEditedTags = false;
if (ratingGroup) {
ratingGroup.addEventListener('click', (e) => { ratingGroup.addEventListener('click', (e) => {
const pill = e.target.closest('.chan-rehost-pill'); const pill = e.target.closest('.chan-rehost-pill');
if (!pill) return; if (!pill) return;
@@ -13574,54 +13714,83 @@ window.cancelAnimFrame = (function () {
pill.classList.add('active'); pill.classList.add('active');
selectedRating = pill.dataset.rating; selectedRating = pill.dataset.rating;
}); });
}
const tagsInput = modal.querySelector('#chan-rehost-tags-input'); // Pre-fill current rating if available
const commentInput = modal.querySelector('#chan-rehost-comment-input');
tagsInput.addEventListener('input', () => {
userEditedTags = true;
});
// Pre-fill current rating and tags if available (don't overwrite if user already interacted)
fetch(`/api/v2/scroller/rehost/details/${itemId}`) fetch(`/api/v2/scroller/rehost/details/${itemId}`)
.then(r => r.json()) .then(r => r.json())
.then(data => { .then(data => {
if (data && data.success) { if (data && data.success) {
if (data.rating && data.rating !== 'untagged' && !userSelectedRating) { if (data.rating && data.rating !== 'untagged' && !userSelectedRating && ratingGroup) {
selectedRating = data.rating; selectedRating = data.rating;
ratingGroup.querySelectorAll('.chan-rehost-pill').forEach(b => { ratingGroup.querySelectorAll('.chan-rehost-pill').forEach(b => {
b.classList.toggle('active', b.dataset.rating === data.rating); b.classList.toggle('active', b.dataset.rating === data.rating);
}); });
} }
if (data.tags && data.tags.length && !userEditedTags && !tagsInput.value) {
tagsInput.value = data.tags.map(t => (typeof t === 'string' ? t : t.tag)).join(', ');
}
} }
}).catch(() => {}); }).catch(() => {});
setTimeout(() => tagsInput.focus(), 100); let isClosing = false;
const closeModal = () => { const closeModal = () => {
modal.style.display = 'none'; if (isClosing) return;
document.body.classList.remove('modal-open'); isClosing = true;
document.removeEventListener('keydown', handleKeydown); document.removeEventListener('keydown', handleKeydown);
document.removeEventListener('click', handleDocClick);
if (positionHandler) {
window.removeEventListener('scroll', positionHandler, { capture: true });
window.removeEventListener('resize', positionHandler);
positionHandler = null;
}
document.body.classList.remove('modal-open');
if (modal) {
modal.style.display = 'none';
if (modal.parentNode) {
modal.parentNode.removeChild(modal);
}
}
}; };
const handleKeydown = (e) => { const handleKeydown = (e) => {
if (e.key === 'Escape') closeModal(); if (e.key === 'Escape') {
e.preventDefault();
closeModal();
}
if (e.key === 'Enter') {
e.preventDefault();
saveDetails();
}
}; };
document.addEventListener('keydown', handleKeydown); document.addEventListener('keydown', handleKeydown);
modal.querySelector('.chan-rehost-modal-close').onclick = closeModal; const closeBtn = modal.querySelector('.chan-rehost-modal-close');
modal.querySelector('.chan-rehost-modal-cancel').onclick = closeModal; if (closeBtn) {
modal.onclick = (e) => { closeBtn.onclick = (e) => {
if (e.target === modal) closeModal(); e.preventDefault();
e.stopPropagation();
closeModal();
}; };
}
modal.onclick = (e) => {
if (e.target === modal) {
e.preventDefault();
e.stopPropagation();
closeModal();
}
};
const handleDocClick = (e) => {
if (!modal || isClosing) return;
if (!content.contains(e.target)) {
closeModal();
}
};
setTimeout(() => {
document.addEventListener('click', handleDocClick);
}, 50);
const updateDomAfterSave = (resData) => { const updateDomAfterSave = (resData) => {
try { try {
const finalRating = resData.rating || selectedRating || 'untagged'; const finalRating = resData.rating || selectedRating || 'untagged';
const finalTags = Array.isArray(resData.tags) ? resData.tags : [];
// 1. Update #tags container // 1. Update #tags container
const tagsContainer = document.querySelector('#tags'); const tagsContainer = document.querySelector('#tags');
@@ -13646,29 +13815,6 @@ window.cancelAnimFrame = (function () {
ratingEl.dataset.itemId = itemId; ratingEl.dataset.itemId = itemId;
ratingEl.dataset.rating = finalRating; ratingEl.dataset.rating = finalRating;
ratingEl.innerHTML = `<span class="rating-label">${rMeta.label}</span>`; ratingEl.innerHTML = `<span class="rating-label">${rMeta.label}</span>`;
// Remove previous tag badges and re-render new ones
tagsContainer.querySelectorAll('.tag-badge').forEach(el => el.remove());
finalTags.forEach(t => {
const tagName = typeof t === 'string' ? t : (t.tag || '');
const tagNorm = typeof t === 'string' ? tagName.toLowerCase().replace(/[^a-z0-9_-]/g, '') : (t.normalized || tagName);
const tagBadge = (typeof t === 'object' && t.badge) ? t.badge : 'badge-light';
const tagId = (typeof t === 'object' && t.id) ? t.id : '';
const span = document.createElement('span');
span.className = `badge ${tagBadge} tag-badge`;
if (tagId) span.dataset.tagId = tagId;
span.dataset.tag = tagName;
span.dataset.tagNormalized = tagNorm;
const a = document.createElement('a');
a.href = `/tag/${encodeURIComponent(tagNorm)}`;
a.className = 'tag-name';
a.textContent = tagName;
span.appendChild(a);
tagsContainer.appendChild(span);
});
} }
// 2. Update .item-layout-container // 2. Update .item-layout-container
@@ -13717,14 +13863,21 @@ window.cancelAnimFrame = (function () {
<a href="/${itemId}" class="chan-rehost-action-btn rehosted" title="View Post #${itemId}" style="display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:4px 12px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;white-space:nowrap;flex-shrink:0;"> <a href="/${itemId}" class="chan-rehost-action-btn rehosted" title="View Post #${itemId}" style="display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:4px 12px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;white-space:nowrap;flex-shrink:0;">
<i class="fa-solid fa-check"></i> #${itemId} <i class="fa-solid fa-check"></i> #${itemId}
</a> </a>
<button type="button" class="chan-rehost-edit-btn" data-item-id="${itemId}" title="Edit rating, tags & comment" style="display:inline-flex;align-items:center;padding:3px 7px;border-radius:6px;font-size:0.8rem;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;"> <button type="button" class="chan-rehost-edit-btn" data-item-id="${itemId}" title="Edit rating" style="display:inline-flex;align-items:center;padding:3px 7px;border-radius:6px;font-size:0.8rem;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;">
<i class="fa-solid fa-pen"></i> <i class="fa-solid fa-pen"></i>
</button> </button>
</span> </span>
`; `;
} }
// 7. Invalidate caches // 7. Update thread edit buttons if present on page
const threadEditBtns = document.querySelectorAll(`.chan-rehost-edit-btn[data-item-id="${itemId}"]`);
threadEditBtns.forEach(b => {
b.dataset.rating = finalRating;
b.title = 'Edit rating (' + finalRating.toUpperCase() + ')';
});
// 8. Invalidate caches
if (typeof window.invalidateItemCache === 'function') { if (typeof window.invalidateItemCache === 'function') {
window.invalidateItemCache(itemId); window.invalidateItemCache(itemId);
window.invalidateItemCache(window.location.pathname); window.invalidateItemCache(window.location.pathname);
@@ -13734,11 +13887,12 @@ window.cancelAnimFrame = (function () {
} }
}; };
const saveDetails = async (andView = false) => { const saveDetails = async () => {
const saveBtn = modal.querySelector('.chan-rehost-modal-save'); const saveBtn = modal.querySelector('.chan-rehost-modal-save');
const saveViewBtn = modal.querySelector('.chan-rehost-modal-save-view'); if (saveBtn) {
if (saveBtn) saveBtn.disabled = true; saveBtn.disabled = true;
if (saveViewBtn) saveViewBtn.disabled = true; saveBtn.innerHTML = '<i class="fa-solid fa-circle-notch fa-spin"></i> Saving…';
}
try { try {
const csrfToken = window.f0ckSession?.csrf_token || ''; const csrfToken = window.f0ckSession?.csrf_token || '';
@@ -13746,57 +13900,46 @@ window.cancelAnimFrame = (function () {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest',
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}) ...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {})
}, },
body: new URLSearchParams({ body: new URLSearchParams({
item_id: itemId, item_id: String(itemId),
rating: selectedRating, rating: selectedRating,
tags: tagsInput.value.trim(), tags: '',
comment: commentInput.value.trim() comment: ''
}) })
}); });
const resData = await resp.json(); const resData = await resp.json();
if (resData.success) { if (resData.success) {
if (typeof window.showToast === 'function') {
window.showToast('Details saved for #' + itemId + '!');
}
closeModal(); closeModal();
if (typeof window.showToast === 'function') {
window.showToast('Rating saved for #' + itemId + '!');
}
updateDomAfterSave(resData); updateDomAfterSave(resData);
if (typeof onSave === 'function') { if (typeof onSave === 'function') {
onSave(resData); onSave(resData);
} }
if (andView) {
if (typeof window.loadItemAjax === 'function') {
window.loadItemAjax('/' + itemId, true, { skipCache: true });
} else { } else {
window.location.href = '/' + itemId; alert(resData.msg || 'Failed to save rating');
if (saveBtn) {
saveBtn.disabled = false;
saveBtn.innerHTML = '<i class="fa-solid fa-check"></i> Save';
} }
} }
} else {
alert(resData.msg || 'Failed to save details');
if (saveBtn) saveBtn.disabled = false;
if (saveViewBtn) saveViewBtn.disabled = false;
}
} catch (err) { } catch (err) {
console.error('[REHOST-MODAL] Save error:', err); console.error('[REHOST-MODAL] Save error:', err);
alert('Error saving details: network error'); alert('Error saving rating: network error');
if (saveBtn) saveBtn.disabled = false; if (saveBtn) {
if (saveViewBtn) saveViewBtn.disabled = false; saveBtn.disabled = false;
saveBtn.innerHTML = '<i class="fa-solid fa-check"></i> Save';
}
} }
}; };
modal.querySelector('.chan-rehost-modal-save').onclick = () => saveDetails(false); const saveBtn = modal.querySelector('.chan-rehost-modal-save');
if (isPlayerView && modal.querySelector('.chan-rehost-modal-save-view')) { if (saveBtn) saveBtn.onclick = () => saveDetails();
modal.querySelector('.chan-rehost-modal-save-view').onclick = () => saveDetails(true);
}
tagsInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
saveDetails(false);
}
});
}; };
if (target.closest('#chan-item-rehost-btn')) { if (target.closest('#chan-item-rehost-btn')) {
@@ -13845,7 +13988,7 @@ window.cancelAnimFrame = (function () {
<a href="/${data.item_id}" class="chan-rehost-action-btn rehosted" title="View Post #${data.item_id}" style="display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:4px 12px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;white-space:nowrap;flex-shrink:0;"> <a href="/${data.item_id}" class="chan-rehost-action-btn rehosted" title="View Post #${data.item_id}" style="display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:4px 12px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;white-space:nowrap;flex-shrink:0;">
<i class="fa-solid fa-check"></i> #${data.item_id} <i class="fa-solid fa-check"></i> #${data.item_id}
</a> </a>
<button type="button" class="chan-rehost-edit-btn" data-item-id="${data.item_id}" title="Edit rating, tags & comment" style="display:inline-flex;align-items:center;padding:3px 7px;border-radius:6px;font-size:0.8rem;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;"> <button type="button" class="chan-rehost-edit-btn" data-item-id="${data.item_id}" title="Edit rating" style="display:inline-flex;align-items:center;padding:3px 7px;border-radius:6px;font-size:0.8rem;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;">
<i class="fa-solid fa-pen"></i> <i class="fa-solid fa-pen"></i>
</button> </button>
</span> </span>
@@ -13854,7 +13997,8 @@ window.cancelAnimFrame = (function () {
window.showToast(data.repost ? 'Already on site — linked to existing post' : 'Rehosted successfully!'); window.showToast(data.repost ? 'Already on site — linked to existing post' : 'Rehosted successfully!');
} }
if (typeof window.openChanRehostModal === 'function') { if (typeof window.openChanRehostModal === 'function') {
window.openChanRehostModal({ itemId: data.item_id, isPlayerView: true }); const newAnchor = document.querySelector(`.chan-rehost-edit-btn[data-item-id="${data.item_id}"]`) || document.querySelector(`.chan-rehost-action-btn[href="/${data.item_id}"]`);
window.openChanRehostModal({ itemId: data.item_id, anchorEl: newAnchor });
} }
} else { } else {
btn.classList.remove('loading'); btn.classList.remove('loading');
@@ -13875,8 +14019,7 @@ window.cancelAnimFrame = (function () {
const editBtn = target.closest('.chan-rehost-edit-btn'); const editBtn = target.closest('.chan-rehost-edit-btn');
const itemId = editBtn?.dataset?.itemId; const itemId = editBtn?.dataset?.itemId;
if (itemId && typeof window.openChanRehostModal === 'function') { if (itemId && typeof window.openChanRehostModal === 'function') {
const isPlayer = !!document.querySelector('.media-object'); window.openChanRehostModal({ itemId: Number(itemId), anchorEl: editBtn });
window.openChanRehostModal({ itemId: Number(itemId), isPlayerView: isPlayer });
} }
} else if (target.closest('#togglebg')) { } else if (target.closest('#togglebg')) {
e.preventDefault(); e.preventDefault();
@@ -19403,7 +19546,17 @@ class NotificationSystem {
const currentUserId = window.f0ckSession?.id ? Number(window.f0ckSession.id) : null; const currentUserId = window.f0ckSession?.id ? Number(window.f0ckSession.id) : null;
const favoBtns = document.querySelectorAll('#a_favo'); const favoBtns = document.querySelectorAll('#a_favo');
if (favoBtns.length && (currentUserId || currentUser)) { if (favoBtns.length && (currentUserId || currentUser)) {
const isNowFav = Array.isArray(data.favs) && data.favs.some(f => { if (currentUserId && data.user_id && Number(data.user_id) === currentUserId && data.favorited !== undefined) {
const isNowFav = !!data.favorited;
favoBtns.forEach(favoBtn => {
favoBtn.classList.remove('fa-spinner', 'fa-spin');
favoBtn.classList.add('iconset', 'fa-heart');
favoBtn.classList.toggle('fa-solid', isNowFav);
favoBtn.classList.toggle('fa-regular', !isNowFav);
});
} else if (!data.user_id || (currentUserId && Number(data.user_id) !== currentUserId)) {
// Notification is from someone else: only update to favorited if current user is found in list
const foundSelf = Array.isArray(data.favs) && data.favs.some(f => {
if (currentUserId && f.user_id && Number(f.user_id) === currentUserId) return true; if (currentUserId && f.user_id && Number(f.user_id) === currentUserId) return true;
const fUser = (f.user || '').toLowerCase(); const fUser = (f.user || '').toLowerCase();
const fLogin = (f.login || '').toLowerCase(); const fLogin = (f.login || '').toLowerCase();
@@ -19411,13 +19564,16 @@ class NotificationSystem {
if (currentLogin && (fUser === currentLogin || fLogin === currentLogin)) return true; if (currentLogin && (fUser === currentLogin || fLogin === currentLogin)) return true;
return false; return false;
}); });
if (foundSelf) {
favoBtns.forEach(favoBtn => { favoBtns.forEach(favoBtn => {
favoBtn.classList.remove('fa-spinner', 'fa-spin'); favoBtn.classList.remove('fa-spinner', 'fa-spin');
favoBtn.classList.add('iconset', 'fa-heart'); favoBtn.classList.add('iconset', 'fa-heart');
favoBtn.classList.toggle('fa-solid', isNowFav); favoBtn.classList.add('fa-solid');
favoBtn.classList.toggle('fa-regular', !isNowFav); favoBtn.classList.remove('fa-regular');
}); });
} }
}
}
if (!Array.isArray(data.favs) || data.favs.length === 0) { if (!Array.isArray(data.favs) || data.favs.length === 0) {
favsContainer.innerHTML = ''; favsContainer.innerHTML = '';
+1 -1
View File
@@ -1211,7 +1211,7 @@
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}) ...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {})
}, },
body: `postid=${id}${csrfToken ? `&csrf_token=${encodeURIComponent(csrfToken)}` : ''}` body: `postid=${id}&action=${nowFaved ? 'add' : 'delete'}&favorited=${nowFaved}${csrfToken ? `&csrf_token=${encodeURIComponent(csrfToken)}` : ''}`
}); });
const data = await resp.json().catch(() => ({})); const data = await resp.json().catch(() => ({}));
if (!resp.ok || !data.success) { if (!resp.ok || !data.success) {
+24
View File
@@ -1594,6 +1594,7 @@
}; };
let isReCenteringFeed = false; let isReCenteringFeed = false;
let lastAttemptedTagCenterKey = null;
const highlightActiveTagCard = (scrollIntoView = false) => { const highlightActiveTagCard = (scrollIntoView = false) => {
const identifiers = getCurrentItemIdentifiers(); const identifiers = getCurrentItemIdentifiers();
@@ -1619,6 +1620,10 @@
} }
}); });
if (matchedCard) {
lastAttemptedTagCenterKey = null;
}
if (matchedCard && scrollIntoView) { if (matchedCard && scrollIntoView) {
const isTagTabActive = document.querySelector('.sidebar-tab.active')?.dataset.tab === 'tag' && const isTagTabActive = document.querySelector('.sidebar-tab.active')?.dataset.tab === 'tag' &&
container.style.display !== 'none'; container.style.display !== 'none';
@@ -1639,6 +1644,11 @@
window._isPoolRandomNav = false; window._isPoolRandomNav = false;
return; return;
} }
const centerKey = `${currentTag}:${identifiers[0]}`;
if (lastAttemptedTagCenterKey === centerKey) {
return;
}
lastAttemptedTagCenterKey = centerKey;
// Non-pool navigation (next/prev): center feed around the active item // Non-pool navigation (next/prev): center feed around the active item
isReCenteringFeed = true; isReCenteringFeed = true;
loadTagFeed(true, identifiers[0]).finally(() => { loadTagFeed(true, identifiers[0]).finally(() => {
@@ -1664,6 +1674,7 @@
if (currentTag !== tag) { if (currentTag !== tag) {
currentTag = tag; currentTag = tag;
lastAttemptedTagCenterKey = null;
tagOffset = 0; tagOffset = 0;
tagFeedLoaded = false; tagFeedLoaded = false;
tagFeedNeedsReload = true; tagFeedNeedsReload = true;
@@ -1681,6 +1692,7 @@
} else { } else {
tagBtn.style.display = 'none'; tagBtn.style.display = 'none';
currentTag = null; currentTag = null;
lastAttemptedTagCenterKey = null;
tagFeedLoaded = false; tagFeedLoaded = false;
userManuallySelectedNonTagTab = false; userManuallySelectedNonTagTab = false;
@@ -1886,6 +1898,7 @@
}; };
let isReCenteringFavsFeed = false; let isReCenteringFavsFeed = false;
let lastAttemptedFavsCenterKey = null;
const highlightActiveFavsCard = (scrollIntoView = false) => { const highlightActiveFavsCard = (scrollIntoView = false) => {
const identifiers = getCurrentItemIdentifiers(); const identifiers = getCurrentItemIdentifiers();
@@ -1908,6 +1921,10 @@
} }
}); });
if (matchedCard) {
lastAttemptedFavsCenterKey = null;
}
if (matchedCard && scrollIntoView) { if (matchedCard && scrollIntoView) {
const isFavsTabActive = document.querySelector('.sidebar-tab.active')?.dataset.tab === 'favs' && const isFavsTabActive = document.querySelector('.sidebar-tab.active')?.dataset.tab === 'favs' &&
container.style.display !== 'none'; container.style.display !== 'none';
@@ -1921,6 +1938,11 @@
} }
} }
} else if (scrollIntoView && !matchedCard && identifiers.length > 0 && currentFavsUser && !isReCenteringFavsFeed && !favsFeedLoading) { } else if (scrollIntoView && !matchedCard && identifiers.length > 0 && currentFavsUser && !isReCenteringFavsFeed && !favsFeedLoading) {
const centerKey = `${currentFavsUser}:${identifiers[0]}`;
if (lastAttemptedFavsCenterKey === centerKey) {
return;
}
lastAttemptedFavsCenterKey = centerKey;
isReCenteringFavsFeed = true; isReCenteringFavsFeed = true;
loadFavsFeed(true, identifiers[0]).finally(() => { loadFavsFeed(true, identifiers[0]).finally(() => {
isReCenteringFavsFeed = false; isReCenteringFavsFeed = false;
@@ -2043,6 +2065,7 @@
if (currentFavsUser !== user) { if (currentFavsUser !== user) {
currentFavsUser = user; currentFavsUser = user;
lastAttemptedFavsCenterKey = null;
favsOffset = 0; favsOffset = 0;
favsFeedLoaded = false; favsFeedLoaded = false;
favsFeedNeedsReload = true; favsFeedNeedsReload = true;
@@ -2057,6 +2080,7 @@
} else { } else {
favsBtn.style.display = 'none'; favsBtn.style.display = 'none';
currentFavsUser = null; currentFavsUser = null;
lastAttemptedFavsCenterKey = null;
favsFeedLoaded = false; favsFeedLoaded = false;
const activeTab = document.querySelector('.sidebar-tab.active')?.dataset.tab; const activeTab = document.querySelector('.sidebar-tab.active')?.dataset.tab;
+16 -6
View File
@@ -3024,9 +3024,10 @@ window.initUploadForm = (selector) => {
? redirectCheckbox.checked ? redirectCheckbox.checked
: (localStorage.getItem('upload_redirect_to_item') !== 'false'); : (localStorage.getItem('upload_redirect_to_item') !== 'false');
if (shouldRedirectToItem && lastData?.itemid) { if (shouldRedirectToItem && lastData?.itemid) {
const targetUrl = lastData.slug ? `/${lastData.slug}` : `/${lastData.itemid}`; const isMultiNonAlbumShitpost = isShitpost && (successCount > 1 || urls.length > 1);
const targetUrl = isMultiNonAlbumShitpost ? '/' : (lastData.slug ? `/${lastData.slug}` : `/${lastData.itemid}`);
if (typeof window.loadPageAjax === 'function') { if (typeof window.loadPageAjax === 'function') {
window.loadPageAjax(targetUrl, true, { bypassCache: true }); window.loadPageAjax(targetUrl, false, { bypassCache: true, skipCache: true, skipInherit: true });
} else { } else {
window.location.href = targetUrl; window.location.href = targetUrl;
} }
@@ -3148,7 +3149,7 @@ window.initUploadForm = (selector) => {
if (shouldRedirectToItem) { if (shouldRedirectToItem) {
const targetUrl = res.slug ? `/${res.slug}` : (res.itemid ? `/${res.itemid}` : '/'); const targetUrl = res.slug ? `/${res.slug}` : (res.itemid ? `/${res.itemid}` : '/');
if (typeof window.loadPageAjax === 'function') { if (typeof window.loadPageAjax === 'function') {
window.loadPageAjax(targetUrl, true, { bypassCache: true }); window.loadPageAjax(targetUrl, false, { bypassCache: true, skipCache: true, skipInherit: true });
} else { } else {
window.location.href = targetUrl; window.location.href = targetUrl;
} }
@@ -3345,12 +3346,15 @@ window.initUploadForm = (selector) => {
} }
if (successCount > 0) { if (successCount > 0) {
const totalFilesToUpload = selectedFiles.length;
const isBgUrlUpload = lastData?.pending && selectedFiles.every(i => i.type === 'url');
if (dragModal) dragModal.classList.remove('show'); if (dragModal) dragModal.classList.remove('show');
const dropModal = document.getElementById('upload-drag-modal'); const dropModal = document.getElementById('upload-drag-modal');
if (dropModal) dropModal.classList.remove('show'); if (dropModal) dropModal.classList.remove('show');
if (window.resetGlobalScrollState) window.resetGlobalScrollState();
if (window.hideAllModals) window.hideAllModals();
form._f0ckUploader.reset(); form._f0ckUploader.reset();
const isBgUrlUpload = lastData?.pending && selectedFiles.every(i => i.type === 'url');
if (isBgUrlUpload) { if (isBgUrlUpload) {
if (typeof window.flashMessage === 'function') { if (typeof window.flashMessage === 'function') {
window.flashMessage((window.f0ckI18n && window.f0ckI18n.url_upload_started) || 'Background upload started', 3000, 'info'); window.flashMessage((window.f0ckI18n && window.f0ckI18n.url_upload_started) || 'Background upload started', 3000, 'info');
@@ -3398,10 +3402,16 @@ window.initUploadForm = (selector) => {
} }
if (shouldRedirectToItem) { if (shouldRedirectToItem) {
const isMultiNonAlbumShitpost = isShitpost && !isAlbum && (uploadedResults.length > 1 || successCount > 1 || totalFilesToUpload > 1);
let targetUrl;
if (isMultiNonAlbumShitpost) {
targetUrl = '/';
} else {
const targetItem = lastData || (uploadedResults && uploadedResults[0]); const targetItem = lastData || (uploadedResults && uploadedResults[0]);
const targetUrl = targetItem ? (targetItem.slug ? `/${targetItem.slug}` : (targetItem.itemid ? `/${targetItem.itemid}` : '/')) : '/'; targetUrl = targetItem ? (targetItem.slug ? `/${targetItem.slug}` : (targetItem.itemid ? `/${targetItem.itemid}` : '/')) : '/';
}
if (typeof window.loadPageAjax === 'function') { if (typeof window.loadPageAjax === 'function') {
window.loadPageAjax(targetUrl, true, { bypassCache: true }); window.loadPageAjax(targetUrl, false, { bypassCache: true, skipCache: true, skipInherit: true });
} else { } else {
window.location.href = targetUrl; window.location.href = targetUrl;
} }
+131 -40
View File
@@ -247,6 +247,8 @@
} }
}; };
let favSeq = 0;
const toggleFavEvent = async (e) => { const toggleFavEvent = async (e) => {
// e is the click event or undefined // e is the click event or undefined
if (e && typeof e.preventDefault === 'function') e.preventDefault(); if (e && typeof e.preventDefault === 'function') e.preventDefault();
@@ -264,7 +266,12 @@
const chanUrl = firstFavoBtn?.dataset?.chanUrl || chanRehostBtn?.dataset?.url; const chanUrl = firstFavoBtn?.dataset?.chanUrl || chanRehostBtn?.dataset?.url;
let localId = firstFavoBtn?.dataset?.localId; let localId = firstFavoBtn?.dataset?.localId;
const ctx = getContext();
const postid = localId ? Number(localId) : ctx?.postid;
if (!postid && !chanUrl) return;
const wasAlreadyFav = favoBtns[0].classList.contains('fa-solid') && !favoBtns[0].classList.contains('fa-spinner'); const wasAlreadyFav = favoBtns[0].classList.contains('fa-solid') && !favoBtns[0].classList.contains('fa-spinner');
const isNowFav = !wasAlreadyFav;
const setFavoUi = (isFav) => { const setFavoUi = (isFav) => {
favoBtns.forEach(btn => { favoBtns.forEach(btn => {
@@ -272,20 +279,98 @@
btn.classList.add('iconset', 'fa-heart'); btn.classList.add('iconset', 'fa-heart');
btn.classList.toggle('fa-solid', isFav); btn.classList.toggle('fa-solid', isFav);
btn.classList.toggle('fa-regular', !isFav); btn.classList.toggle('fa-regular', !isFav);
btn.title = isFav ? (window.f0ckI18n?.fav_remove || 'Remove from favorites') : (window.f0ckI18n?.fav_add || 'Favorite');
}); });
}; };
const setFavoLoading = () => { // 1. Instantly apply client UI
setFavoUi(isNowFav);
// Micro-animation for tactile pop
favoBtns.forEach(btn => { favoBtns.forEach(btn => {
btn.classList.remove('fa-heart'); btn.classList.remove('fav-pop');
btn.classList.add('iconset', 'fa-solid', 'fa-spinner', 'fa-spin'); void btn.offsetWidth;
btn.classList.add('fav-pop');
}); });
// Instant flash message & vibration feedback
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);
if (postid && window.invalidateItemCache) {
window.invalidateItemCache(postid);
}
// Optimistically update #favs badge list
const favcontainer = document.querySelector('#favs');
const prevFavsHtml = favcontainer ? favcontainer.innerHTML : '';
const prevFavsHidden = favcontainer ? favcontainer.hidden : true;
if (favcontainer) {
const currentUser = (window.f0ckSession?.user || '').toLowerCase();
const isAnon = !!(window.f0ckSession?.is_anon || window.f0ckSession?.user === 'anonymous');
const isAnonymized = !!(window.f0ckSession?.is_anonymized ?? (window.f0ckSession?.guest_anonymize && !window.f0ckSession?.logged_in) ?? (window.f0ckSession?.is_anon && window.f0ckSession?.anon_anonymize));
const findSelfBadge = () => {
const selfBadges = favcontainer.querySelectorAll('[data-self="true"]');
if (selfBadges.length) return selfBadges[0];
if (currentUser && currentUser !== 'anonymous') {
const links = favcontainer.querySelectorAll('a[href]');
for (const a of links) {
const path = a.getAttribute('href') || '';
if (path.toLowerCase().endsWith('/user/' + currentUser)) return a;
}
}
return null;
}; };
if (!isNowFav) {
const selfBadge = findSelfBadge();
if (selfBadge) selfBadge.remove();
if (favcontainer.children.length === 0) {
favcontainer.hidden = true;
}
} else {
let selfBadge = findSelfBadge();
if (!selfBadge) {
selfBadge = document.createElement('a');
selfBadge.dataset.self = 'true';
selfBadge.setAttribute('flow', 'up');
if (isAnonymized || isAnon) {
selfBadge.className = 'ghost-fav';
selfBadge.setAttribute('tooltip', 'anonymous');
selfBadge.style.cursor = 'default';
const img = document.createElement('img');
img.src = '/a/default.png';
img.style.height = "32px";
img.style.width = "32px";
selfBadge.appendChild(img);
} else {
selfBadge.href = `/user/${currentUser}`;
selfBadge.setAttribute('tooltip', window.f0ckSession?.display_name || window.f0ckSession?.user || 'anonymous');
const img = document.createElement('img');
const avatarFile = window.f0ckSession?.avatar_file;
const avatar = window.f0ckSession?.avatar;
img.src = avatarFile ? `/a/${avatarFile}` : (avatar ? `/t/${avatar}.webp` : '/a/default.png');
img.style.height = "32px";
img.style.width = "32px";
if (window.f0ckSession?.username_color) img.style.borderColor = window.f0ckSession.username_color;
selfBadge.appendChild(img);
}
favcontainer.appendChild(selfBadge);
}
favcontainer.hidden = false;
}
}
const mySeq = ++favSeq;
// 2. Run server operation in background — can take as long as it needs
(async () => {
try {
// If viewing un-rehosted 4chan post, auto-rehost first // If viewing un-rehosted 4chan post, auto-rehost first
if (isChan && !localId && chanUrl) { if (isChan && !localId && chanUrl) {
setFavoLoading(); if (mySeq !== favSeq) return;
try {
const csrfToken = window.f0ckSession?.csrf_token || ''; const csrfToken = window.f0ckSession?.csrf_token || '';
const rehostResp = await fetch('/api/v2/scroller/rehost', { const rehostResp = await fetch('/api/v2/scroller/rehost', {
method: 'POST', method: 'POST',
@@ -326,49 +411,43 @@
chanRehostBtn.outerHTML = ` chanRehostBtn.outerHTML = `
<span class="chan-rehosted-wrap" style="display:inline-flex;gap:4px;align-items:center;"> <span class="chan-rehosted-wrap" style="display:inline-flex;gap:4px;align-items:center;">
<a href="/${localId}" class="chan-rehost-action-btn rehosted" title="Already rehosted on f0ckm (Post #${localId})" style="display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;"><i class="fa-solid fa-check"></i> #${localId}</a> <a href="/${localId}" class="chan-rehost-action-btn rehosted" title="Already rehosted on f0ckm (Post #${localId})" style="display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;"><i class="fa-solid fa-check"></i> #${localId}</a>
<button type="button" class="chan-rehost-edit-btn" data-item-id="${localId}" title="Edit rating, tags & comment" style="display:inline-flex;align-items:center;padding:3px 7px;border-radius:6px;font-size:0.8rem;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;"><i class="fa-solid fa-pen"></i></button> <button type="button" class="chan-rehost-edit-btn" data-item-id="${localId}" title="Edit rating" style="display:inline-flex;align-items:center;padding:3px 7px;border-radius:6px;font-size:0.8rem;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;"><i class="fa-solid fa-pen"></i></button>
</span> </span>
`; `;
} }
} else { } else {
throw new Error(rehostData.msg || 'Rehost failed'); throw new Error(rehostData.msg || 'Rehost failed');
} }
} catch (err) {
console.error('[CHAN-FAV] Auto-rehost error:', err);
setFavoUi(wasAlreadyFav);
if (typeof window.flashMessage === 'function') {
window.flashMessage('Failed to favorite post', 3000, 'error');
}
return;
}
} }
const ctx = getContext(); const effectivePostId = localId ? Number(localId) : (postid || getContext()?.postid);
if (!ctx) { if (!effectivePostId) {
setFavoUi(wasAlreadyFav); throw new Error('Missing post ID');
return;
} }
const postid = localId ? Number(localId) : ctx.postid;
try { if (mySeq !== favSeq) return;
const res = await post('/api/v2/togglefav', { const res = await post('/api/v2/togglefav', {
postid: postid, postid: effectivePostId,
chan_url: chanUrl chan_url: chanUrl,
action: isNowFav ? 'add' : 'delete',
favorited: isNowFav
}); });
if (mySeq !== favSeq) return;
if (res && res.success) { if (res && res.success) {
if (window.invalidateItemCache) { if (window.invalidateItemCache) {
window.invalidateItemCache(postid); window.invalidateItemCache(effectivePostId);
} }
// Authoritative server state takes priority; fallback to inverting previous UI state const finalIsFav = (res.favorited !== undefined) ? !!res.favorited : isNowFav;
const isNowFav = (res.favorited !== undefined) ? !!res.favorited : !wasAlreadyFav; setFavoUi(finalIsFav);
setFavoUi(isNowFav); // Render authoritative favs list from server
const curFavContainer = document.querySelector('#favs');
// span#favs if (curFavContainer && Array.isArray(res.favs)) {
const favcontainer = document.querySelector('#favs'); curFavContainer.innerHTML = "";
if (favcontainer) { if (res.favs.length > 0) {
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 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(); const fragment = document.createDocumentFragment();
res.favs.forEach(f => { res.favs.forEach(f => {
@@ -396,6 +475,7 @@
} else if (isAnonymized || isFavAnon) { } else if (isAnonymized || isFavAnon) {
const a = document.createElement('a'); const a = document.createElement('a');
a.className = 'ghost-fav'; a.className = 'ghost-fav';
if (isSelf) a.dataset.self = 'true';
a.setAttribute('tooltip', 'anonymous'); a.setAttribute('tooltip', 'anonymous');
a.setAttribute('flow', 'up'); a.setAttribute('flow', 'up');
a.style.cursor = 'default'; a.style.cursor = 'default';
@@ -409,6 +489,7 @@
fragment.appendChild(a); fragment.appendChild(a);
} else { } else {
const a = document.createElement('a'); const a = document.createElement('a');
if (isSelf) a.dataset.self = 'true';
a.href = `/user/${(f.user || '').toLowerCase()}`; a.href = `/user/${(f.user || '').toLowerCase()}`;
a.setAttribute('tooltip', f.display_name || f.user || 'anonymous'); a.setAttribute('tooltip', f.display_name || f.user || 'anonymous');
a.setAttribute('flow', 'up'); a.setAttribute('flow', 'up');
@@ -423,29 +504,39 @@
fragment.appendChild(a); fragment.appendChild(a);
} }
}); });
favcontainer.appendChild(fragment); curFavContainer.appendChild(fragment);
favcontainer.hidden = false; curFavContainer.hidden = false;
} else { } else {
favcontainer.hidden = true; curFavContainer.hidden = true;
} }
} }
} else {
window.flashMessage((window.f0ckI18n && (isNowFav ? window.f0ckI18n.fav_added : window.f0ckI18n.fav_removed)) || (isNowFav ? 'ADDED TO FAVORITES' : 'REMOVED FROM FAVORITES')); if (mySeq !== favSeq) return;
if (navigator.vibrate) navigator.vibrate(50);
}
else {
setFavoUi(wasAlreadyFav); setFavoUi(wasAlreadyFav);
const curFavContainer = document.querySelector('#favs');
if (curFavContainer) {
curFavContainer.innerHTML = prevFavsHtml;
curFavContainer.hidden = prevFavsHidden;
}
const errMsg = (res && (res.msg || res.error)) || 'Anonymous favoriting is disabled.'; const errMsg = (res && (res.msg || res.error)) || 'Anonymous favoriting is disabled.';
if (typeof window.flashMessage === 'function') { if (typeof window.flashMessage === 'function') {
window.flashMessage(errMsg, 3000, 'error'); window.flashMessage(errMsg, 3000, 'error');
} }
} }
} catch (err) { } catch (err) {
console.error('[CHAN-FAV] Error during background favorite sync:', err);
if (mySeq !== favSeq) return;
setFavoUi(wasAlreadyFav); setFavoUi(wasAlreadyFav);
const curFavContainer = document.querySelector('#favs');
if (curFavContainer) {
curFavContainer.innerHTML = prevFavsHtml;
curFavContainer.hidden = prevFavsHidden;
}
if (typeof window.flashMessage === 'function') { if (typeof window.flashMessage === 'function') {
window.flashMessage('Failed to update favorite.', 3000, 'error'); window.flashMessage('Failed to update favorite.', 3000, 'error');
} }
} }
})();
}; };
// Event Delegation // Event Delegation
+4 -4
View File
@@ -89,7 +89,7 @@ export const handleBrandImageUpload = async (req, res) => {
return sendJson(res, { success: false, msg: 'Invalid content type — multipart boundary missing' }, 400); return sendJson(res, { success: false, msg: 'Invalid content type — multipart boundary missing' }, 400);
} }
const body = await collectBody(req, 5 * 1024 * 1024); // 5 MB hard cap const body = await collectBody(req, 10 * 1024 * 1024); // 10 MB request body cap
const parts = parseMultipart(body, boundaryMatch[1]); const parts = parseMultipart(body, boundaryMatch[1]);
const file = parts.file; const file = parts.file;
@@ -97,12 +97,12 @@ export const handleBrandImageUpload = async (req, res) => {
return sendJson(res, { success: false, msg: 'No file provided' }, 400); return sendJson(res, { success: false, msg: 'No file provided' }, 400);
} }
// 2 MB soft cap // 5 MB cap
const maxSize = 2 * 1024 * 1024; const maxSize = 5 * 1024 * 1024;
if (file.data.length > maxSize) { if (file.data.length > maxSize) {
return sendJson(res, { return sendJson(res, {
success: false, success: false,
msg: `File too large. Maximum is 2 MB, got ${(file.data.length / 1024 / 1024).toFixed(2)} MB` msg: `File too large. Maximum is 5 MB, got ${(file.data.length / 1024 / 1024).toFixed(2)} MB`
}, 400); }, 400);
} }
+2
View File
@@ -3745,6 +3745,7 @@ const f0cklib = {
}, },
resolveNumericItemId,
computeBaseMode, computeBaseMode,
getGlobalfilter, getGlobalfilter,
processMentions, processMentions,
@@ -3756,6 +3757,7 @@ const f0cklib = {
clearCountCache: () => countCache.clear() clearCountCache: () => countCache.clear()
}; };
export { resolveNumericItemId };
export default f0cklib; export default f0cklib;
+18 -3
View File
@@ -1904,6 +1904,18 @@ export default router => {
`; `;
let favorited = false; let favorited = false;
const reqAction = req.post?.action ?? req.body?.action;
const reqFavorited = req.post?.favorited ?? req.body?.favorited;
let shouldFav;
if (reqAction === 'add' || reqAction === 'favorite' || reqFavorited === true || reqFavorited === 'true') {
shouldFav = true;
} else if (reqAction === 'delete' || reqAction === 'remove' || reqAction === 'unfavorite' || reqFavorited === false || reqFavorited === 'false') {
shouldFav = false;
} else {
shouldFav = existing.length === 0;
}
if (!shouldFav) {
if (existing.length > 0) { if (existing.length > 0) {
// del fav // del fav
await db` await db`
@@ -1911,12 +1923,14 @@ export default router => {
where user_id = ${+req.session.id} where user_id = ${+req.session.id}
and item_id = ${+postid} and item_id = ${+postid}
`; `;
favorited = false;
f0cklib.updateUserAffinity({ user_id: req.session.id, item_id: +postid, scoreDelta: -5.0 }).catch(() => {}); f0cklib.updateUserAffinity({ user_id: req.session.id, item_id: +postid, scoreDelta: -5.0 }).catch(() => {});
if (req.session?.is_anon) { if (req.session?.is_anon) {
await logAnonActivity(req, { action: 'unfavorite', targetId: postid }); await logAnonActivity(req, { action: 'unfavorite', targetId: postid });
} }
}
favorited = false;
} else { } else {
if (existing.length === 0) {
// add fav — ON CONFLICT DO NOTHING guards against rapid double-taps // add fav — ON CONFLICT DO NOTHING guards against rapid double-taps
await db` await db`
insert into "favorites" ${db({ insert into "favorites" ${db({
@@ -1925,12 +1939,13 @@ export default router => {
}, 'item_id', 'user_id')} }, 'item_id', 'user_id')}
on conflict do nothing on conflict do nothing
`; `;
favorited = true;
f0cklib.updateUserAffinity({ user_id: req.session.id, item_id: +postid, scoreDelta: 5.0 }).catch(() => {}); f0cklib.updateUserAffinity({ user_id: req.session.id, item_id: +postid, scoreDelta: 5.0 }).catch(() => {});
if (req.session?.is_anon) { if (req.session?.is_anon) {
await logAnonActivity(req, { action: 'favorite', targetId: postid }); await logAnonActivity(req, { action: 'favorite', targetId: postid });
} }
} }
favorited = true;
}
const favs = await db` const favs = await db`
select "favorites".user_id, "user".user, "user".login, "user_options".avatar, "user_options".avatar_file, "user_options".display_name, "user_options".username_color, "user_options".hide_fav_badge, "anon_identities".fingerprint as anon_fingerprint select "favorites".user_id, "user".user, "user".login, "user_options".avatar, "user_options".avatar_file, "user_options".display_name, "user_options".username_color, "user_options".hide_fav_badge, "anon_identities".fingerprint as anon_fingerprint
@@ -1970,7 +1985,7 @@ export default router => {
// If the caller is in anonymous mode, strip other users' identities // If the caller is in anonymous mode, strip other users' identities
const isCallerAnonymized = isAnonymizeSession(req.session); const isCallerAnonymized = isAnonymizeSession(req.session);
const clientFavs = isCallerAnonymized const clientFavs = isCallerAnonymized
? sanitizedFavs.map(f => { ? favs.map(f => {
const isSelf = req.session && req.session.id && f.user_id && Number(f.user_id) === Number(req.session.id); const isSelf = req.session && req.session.id && f.user_id && Number(f.user_id) === Number(req.session.id);
if (isSelf) return f; if (isSelf) return f;
return { return {
+92 -40
View File
@@ -144,6 +144,48 @@ export default (router, tpl) => {
} }
}); });
function formatPosts(posts, board, rehostMap, userFavSet, targetPostNo = null, lang = 'en') {
return posts.map(p => {
const ext = (p.ext || '').toLowerCase();
const isVideo = ['.webm', '.mp4'].includes(ext);
const isImage = ['.jpg', '.jpeg', '.png', '.gif', '.webp'].includes(ext);
const externalMediaUrl = p.tim ? `https://i.4cdn.org/${board}/${p.tim}${ext}` : null;
const rehostInfo = (p.tim && rehostMap[p.tim]) || (externalMediaUrl && rehostMap[externalMediaUrl]) || null;
const localId = rehostInfo ? rehostInfo.id : null;
const effectiveTime = (localId && rehostInfo?.stamp) ? Number(rehostInfo.stamp) : p.time;
const isoStr = new Date(effectiveTime * 1000).toISOString();
return {
no: p.no,
sub: p.sub || '',
com_raw: p.com || '',
com_formatted: formatComment(p.com || ''),
name: p.name || 'Anonymous',
trip: p.trip || '',
time: effectiveTime,
timeago: lib.timeAgo(effectiveTime * 1000, lang),
date_str: (localId && rehostInfo?.stamp) ? new Date(effectiveTime * 1000).toLocaleString() : (p.now || new Date(effectiveTime * 1000).toLocaleString()),
iso_str: isoStr,
has_media: !!(p.tim && p.ext),
tim: p.tim,
ext: ext,
filename: p.filename ? `${p.filename}${ext}` : null,
w: p.w,
h: p.h,
fsize: p.fsize ? lib.formatSize(p.fsize) : null,
dest: p.tim ? `/api/v2/scroller/external/4chan/${board}/media/${p.tim}${ext}` : null,
thumb: p.tim ? `/api/v2/scroller/external/4chan/${board}/media/${p.tim}s.jpg` : null,
external_media_url: externalMediaUrl,
is_video: isVideo,
is_image: isImage,
local_id: localId,
rehosted: !!localId,
user_has_favorited: localId ? userFavSet.has(Number(localId)) : false,
is_onara_active: targetPostNo ? p.no === targetPostNo : false
};
});
}
// ─────────────────────────────────────────────────────────────────────────── // ───────────────────────────────────────────────────────────────────────────
// 2. GET /4/:board/:thread — Overview of all posts in that thread // 2. GET /4/:board/:thread — Overview of all posts in that thread
// ─────────────────────────────────────────────────────────────────────────── // ───────────────────────────────────────────────────────────────────────────
@@ -200,44 +242,7 @@ export default (router, tpl) => {
} }
// Format posts for template // Format posts for template
const formattedPosts = posts.map(p => { const formattedPosts = formatPosts(posts, board, rehostMap, userFavSet, null, req.lang || 'en');
const ext = (p.ext || '').toLowerCase();
const isVideo = ['.webm', '.mp4'].includes(ext);
const isImage = ['.jpg', '.jpeg', '.png', '.gif', '.webp'].includes(ext);
const externalMediaUrl = p.tim ? `https://i.4cdn.org/${board}/${p.tim}${ext}` : null;
const rehostInfo = (p.tim && rehostMap[p.tim]) || (externalMediaUrl && rehostMap[externalMediaUrl]) || null;
const localId = rehostInfo ? rehostInfo.id : null;
const effectiveTime = (localId && rehostInfo?.stamp) ? Number(rehostInfo.stamp) : p.time;
const isoStr = new Date(effectiveTime * 1000).toISOString();
return {
no: p.no,
sub: p.sub || '',
com_raw: p.com || '',
com_formatted: formatComment(p.com || ''),
name: p.name || 'Anonymous',
trip: p.trip || '',
time: effectiveTime,
timeago: lib.timeAgo(effectiveTime * 1000, req.lang || 'en'),
date_str: (localId && rehostInfo?.stamp) ? new Date(effectiveTime * 1000).toLocaleString() : (p.now || new Date(effectiveTime * 1000).toLocaleString()),
iso_str: isoStr,
has_media: !!(p.tim && p.ext),
tim: p.tim,
ext: ext,
filename: p.filename ? `${p.filename}${ext}` : null,
w: p.w,
h: p.h,
fsize: p.fsize ? lib.formatSize(p.fsize) : null,
dest: p.tim ? `/api/v2/scroller/external/4chan/${board}/media/${p.tim}${ext}` : null,
thumb: p.tim ? `/api/v2/scroller/external/4chan/${board}/media/${p.tim}s.jpg` : null,
external_media_url: externalMediaUrl,
is_video: isVideo,
is_image: isImage,
local_id: localId,
rehosted: !!localId,
user_has_favorited: localId ? userFavSet.has(Number(localId)) : false
};
});
const firstMediaPost = formattedPosts.find(p => p.has_media); const firstMediaPost = formattedPosts.find(p => p.has_media);
@@ -315,13 +320,19 @@ export default (router, tpl) => {
cdnUrls.push(`/api/v2/scroller/external/4chan/${board}/media/${p.tim}${ext}`); cdnUrls.push(`/api/v2/scroller/external/4chan/${board}/media/${p.tim}${ext}`);
}); });
const rehosts = {}; const rehosts = {};
const rehostMap = {};
if (cdnUrls.length > 0) { if (cdnUrls.length > 0) {
try { try {
const rows = await db`SELECT id, src, stamp FROM items WHERE src = ANY(${cdnUrls})`; const rows = await db`SELECT id, src, stamp FROM items WHERE src = ANY(${cdnUrls})`;
rows.forEach(r => { rows.forEach(r => {
const info = { id: r.id, stamp: r.stamp };
rehosts[r.src] = r.id; rehosts[r.src] = r.id;
rehostMap[r.src] = info;
const m = r.src.match(/(\d{13,20})/); const m = r.src.match(/(\d{13,20})/);
if (m) rehosts[m[1]] = r.id; if (m) {
rehosts[m[1]] = r.id;
rehostMap[m[1]] = info;
}
}); });
} catch (e) { } catch (e) {
console.error('[CHAN] Rehost check error:', e.message); console.error('[CHAN] Rehost check error:', e.message);
@@ -529,7 +540,48 @@ export default (router, tpl) => {
}); });
} }
// Standard full page render // Full page render: if Onara is active, render thread page with onara modal open
const cfgOnara = cfg.onara !== undefined ? cfg.onara : cfg.websrv?.onara;
const cookieOnara = req.cookies?.f0ck_onara !== undefined
? (req.cookies.f0ck_onara === '1' || req.cookies.f0ck_onara === 'true')
: (req.cookies?.onara !== undefined ? (req.cookies.onara === '1' || req.cookies.onara === 'true') : null);
const isOnara = (cfgOnara !== undefined && cfgOnara !== null)
? !!cfgOnara
: (cookieOnara !== null ? cookieOnara : !!req.session?.onara);
if (isOnara) {
const isModern = req.session?.use_new_layout;
const partialTpl = isModern ? 'item-partial-modern' : 'item-partial-legacy';
const itemHtml = tpl.render(partialTpl, data, req);
const formattedPosts = formatPosts(posts, board, rehostMap, userFavSet, targetPost.no, req.lang || 'en');
const firstMediaPost = formattedPosts.find(p => p.has_media);
const threadViewData = {
board,
tid,
op,
posts: formattedPosts,
media_count: mediaPosts.length,
first_media_no: firstMediaPost ? firstMediaPost.no : null,
rehosts_count: Object.keys(rehosts).length,
session: req.session ? { ...req.session } : false,
csrf_token: req.session?.csrf_token || '',
domain: cfg.main.url.domain,
tmp: null,
page_meta: data.page_meta,
is_onara_item: true,
onara: true,
onara_item_html: itemHtml,
item: data.item
};
return res.reply({
body: tpl.render('chan/thread', threadViewData, req)
});
}
// Standard full page render (when Onara is disabled)
return res.reply({ return res.reply({
body: tpl.render('item', data, req) body: tpl.render('item', data, req)
}); });
+10 -1
View File
@@ -632,7 +632,16 @@ export default (router) => {
const isOwner = !!(rows[0].username && session.user && rows[0].username.toLowerCase() === session.user.toLowerCase()); const isOwner = !!(rows[0].username && session.user && rows[0].username.toLowerCase() === session.user.toLowerCase());
const isAdmin = !!(session.admin || session.is_moderator); const isAdmin = !!(session.admin || session.is_moderator);
if (!isOwner && !isAdmin) { const isChanUser = !!(session.admin || (Array.isArray(session.groups) && session.groups.includes('4chan')));
const hasTagsOrComment = (tags && Array.isArray(tags) ? tags.length > 0 : (typeof tags === 'string' && tags.trim().length > 0)) ||
(comment && typeof comment === 'string' && comment.trim().length > 0);
if (hasTagsOrComment && !isOwner && !isAdmin) {
return res.reply({ code: 403, body: JSON.stringify({ success: false, msg: 'Only owner can edit tags and comment' }) });
}
if (!isOwner && !isAdmin && !isChanUser) {
return res.reply({ code: 403, body: JSON.stringify({ success: false, msg: 'Unauthorized' }) }); return res.reply({ code: 403, body: JSON.stringify({ success: false, msg: 'Unauthorized' }) });
} }
+1 -1
View File
@@ -39,7 +39,7 @@
<!-- Navbar Brand Image --> <!-- Navbar Brand Image -->
<div class="settings-item" id="brand-image-section" style="background: rgba(0,0,0,0.2); padding: 15px; border-radius: 4px; margin-top: 10px;"> <div class="settings-item" id="brand-image-section" style="background: rgba(0,0,0,0.2); padding: 15px; border-radius: 4px; margin-top: 10px;">
<label style="display: block; font-weight: bold; color: var(--accent); margin-bottom: 8px;">Navbar Brand Image</label> <label style="display: block; font-weight: bold; color: var(--accent); margin-bottom: 8px;">Navbar Brand Image</label>
<p style="margin: 0 0 12px 0; font-size: 0.8em; color: #aaa;">Upload a logo to display in the site navbar instead of plain text. Accepted: gif, jpg, png, webp, svg &mdash; max 2&nbsp;MB.</p> <p style="margin: 0 0 12px 0; font-size: 0.8em; color: #aaa;">Upload a logo to display in the site navbar instead of plain text. Accepted: gif, jpg, png, webp, svg &mdash; max 5&nbsp;MB.</p>
<div style="display: flex; align-items: center; gap: 16px; flex-wrap: wrap;"> <div style="display: flex; align-items: center; gap: 16px; flex-wrap: wrap;">
<!-- Preview --> <!-- Preview -->
+1 -1
View File
@@ -130,7 +130,7 @@
@if(item.local_id) @if(item.local_id)
<span class="chan-rehosted-wrap" style="display:inline-flex;gap:4px;align-items:center;"> <span class="chan-rehosted-wrap" style="display:inline-flex;gap:4px;align-items:center;">
<a href="/{{ item.local_id }}" class="chan-rehost-action-btn rehosted" title="Already rehosted on f0ckm (Post #{{ item.local_id }})" style="display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:4px 12px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;white-space:nowrap;flex-shrink:0;"><i class="fa-solid fa-check"></i> #{{ item.local_id }}</a> <a href="/{{ item.local_id }}" class="chan-rehost-action-btn rehosted" title="Already rehosted on f0ckm (Post #{{ item.local_id }})" style="display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:4px 12px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;white-space:nowrap;flex-shrink:0;"><i class="fa-solid fa-check"></i> #{{ item.local_id }}</a>
<button type="button" class="chan-rehost-edit-btn" data-item-id="{{ item.local_id }}" title="Edit rating, tags & comment" style="display:inline-flex;align-items:center;padding:3px 7px;border-radius:6px;font-size:0.8rem;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;"><i class="fa-solid fa-pen"></i></button> <button type="button" class="chan-rehost-edit-btn" data-item-id="{{ item.local_id }}" title="Edit rating" style="display:inline-flex;align-items:center;padding:3px 7px;border-radius:6px;font-size:0.8rem;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;"><i class="fa-solid fa-pen"></i></button>
</span> </span>
@else @else
<button type="button" class="chan-rehost-action-btn" id="chan-item-rehost-btn" data-url="{{ item.external_media_url }}" data-board="{{ item.external_board }}" data-tid="{{ item.external_tid }}" data-post="{{ item.external_id }}" data-filename="{{ item.original_filename || '' }}" data-width="{{ item.width || '' }}" data-height="{{ item.height || '' }}" title="Rehost to f0ckm" style="display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:4px 16px;min-width:100px;box-sizing:border-box;white-space:nowrap;flex-shrink:0;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.15);color:#4ade80;border:1px solid rgba(74,222,128,0.3);cursor:pointer;font-weight:600;"><i class="fa-solid fa-cloud-arrow-up"></i> Rehost</button> <button type="button" class="chan-rehost-action-btn" id="chan-item-rehost-btn" data-url="{{ item.external_media_url }}" data-board="{{ item.external_board }}" data-tid="{{ item.external_tid }}" data-post="{{ item.external_id }}" data-filename="{{ item.original_filename || '' }}" data-width="{{ item.width || '' }}" data-height="{{ item.height || '' }}" title="Rehost to f0ckm" style="display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:4px 16px;min-width:100px;box-sizing:border-box;white-space:nowrap;flex-shrink:0;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.15);color:#4ade80;border:1px solid rgba(74,222,128,0.3);cursor:pointer;font-weight:600;"><i class="fa-solid fa-cloud-arrow-up"></i> Rehost</button>
+1 -1
View File
@@ -162,7 +162,7 @@
@if(item.local_id) @if(item.local_id)
<span class="chan-rehosted-wrap" style="display:inline-flex;gap:4px;align-items:center;"> <span class="chan-rehosted-wrap" style="display:inline-flex;gap:4px;align-items:center;">
<a href="/{{ item.local_id }}" class="chan-rehost-action-btn rehosted" title="Already rehosted on f0ckm (Post #{{ item.local_id }})" style="display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:4px 12px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;white-space:nowrap;flex-shrink:0;"><i class="fa-solid fa-check"></i> #{{ item.local_id }}</a> <a href="/{{ item.local_id }}" class="chan-rehost-action-btn rehosted" title="Already rehosted on f0ckm (Post #{{ item.local_id }})" style="display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:4px 12px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;white-space:nowrap;flex-shrink:0;"><i class="fa-solid fa-check"></i> #{{ item.local_id }}</a>
<button type="button" class="chan-rehost-edit-btn" data-item-id="{{ item.local_id }}" title="Edit rating, tags & comment" style="display:inline-flex;align-items:center;padding:3px 7px;border-radius:6px;font-size:0.8rem;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;"><i class="fa-solid fa-pen"></i></button> <button type="button" class="chan-rehost-edit-btn" data-item-id="{{ item.local_id }}" title="Edit rating" style="display:inline-flex;align-items:center;padding:3px 7px;border-radius:6px;font-size:0.8rem;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;"><i class="fa-solid fa-pen"></i></button>
</span> </span>
@else @else
<button type="button" class="chan-rehost-action-btn" id="chan-item-rehost-btn" data-url="{{ item.external_media_url }}" data-board="{{ item.external_board }}" data-tid="{{ item.external_tid }}" data-post="{{ item.external_id }}" data-filename="{{ item.original_filename || '' }}" data-width="{{ item.width || '' }}" data-height="{{ item.height || '' }}" title="Rehost to f0ckm" style="display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:4px 16px;min-width:100px;box-sizing:border-box;white-space:nowrap;flex-shrink:0;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.15);color:#4ade80;border:1px solid rgba(74,222,128,0.3);cursor:pointer;font-weight:600;"><i class="fa-solid fa-cloud-arrow-up"></i> Rehost</button> <button type="button" class="chan-rehost-action-btn" id="chan-item-rehost-btn" data-url="{{ item.external_media_url }}" data-board="{{ item.external_board }}" data-tid="{{ item.external_tid }}" data-post="{{ item.external_id }}" data-filename="{{ item.original_filename || '' }}" data-width="{{ item.width || '' }}" data-height="{{ item.height || '' }}" title="Rehost to f0ckm" style="display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:4px 16px;min-width:100px;box-sizing:border-box;white-space:nowrap;flex-shrink:0;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.15);color:#4ade80;border:1px solid rgba(74,222,128,0.3);cursor:pointer;font-weight:600;"><i class="fa-solid fa-cloud-arrow-up"></i> Rehost</button>
+9 -2
View File
@@ -621,8 +621,15 @@
if (status) { status.style.display = 'block'; status.style.color = '#98c379'; status.textContent = 'Passkey added!'; } if (status) { status.style.display = 'block'; status.style.color = '#98c379'; status.textContent = 'Passkey added!'; }
await loadPasskeys(); await loadPasskeys();
} catch (err) { } catch (err) {
if (err.name !== 'NotAllowedError') { console.warn('[SETTINGS_PASSKEY] Add passkey error:', err);
if (status) { status.style.display = 'block'; status.style.color = '#e06c75'; status.textContent = 'Error: ' + err.message; } if (status) {
status.style.display = 'block';
status.style.color = '#e06c75';
if (err && err.name === 'NotAllowedError') {
status.textContent = 'Passkey creation was cancelled or blocked. If Bitwarden or another password manager is locked, please unlock it.';
} else {
status.textContent = 'Error: ' + ((err && err.message) || 'Unknown error');
}
} }
} finally { } finally {
if (btn) btn.disabled = false; if (btn) btn.disabled = false;
+25 -4
View File
@@ -376,9 +376,14 @@ async function loginWithPasskey() {
window.location.reload(); window.location.reload();
} catch (e) { } catch (e) {
if (e && e.name !== 'NotAllowedError' && errEl) { console.warn('[PASSKEY] Login failed:', e);
if (errEl) {
errEl.style.display = ''; errEl.style.display = '';
errEl.textContent = e.message || 'Passkey login failed.'; if (e && e.name === 'NotAllowedError') {
errEl.textContent = 'Passkey request was cancelled or blocked. If Bitwarden or another password manager is locked, please unlock it or check extension permissions.';
} else {
errEl.textContent = (e && e.message) || 'Passkey login failed.';
}
} }
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fa-solid fa-fingerprint"></i> Login with passkey'; } if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fa-solid fa-fingerprint"></i> Login with passkey'; }
} }
@@ -641,7 +646,15 @@ async function loginWithPasskey() {
try { try {
await window.f0ckAnonPasskey.doRegister(); await window.f0ckAnonPasskey.doRegister();
} catch(e) { } catch(e) {
if (errEl && e && e.name !== 'NotAllowedError') { errEl.style.display = ''; errEl.textContent = e.message || 'Something went wrong.'; } console.warn('[ANON_PASSKEY] Registration failed:', e);
if (errEl) {
errEl.style.display = '';
if (e && e.name === 'NotAllowedError') {
errEl.textContent = 'Passkey creation was cancelled or blocked. If Bitwarden or another password manager is locked, please unlock it or disable passkey interception.';
} else {
errEl.textContent = (e && e.message) || 'Something went wrong.';
}
}
createBtn.disabled = false; createBtn.disabled = false;
createBtn.innerHTML = '<i class="fa-solid fa-fingerprint"></i> Create my passkey'; createBtn.innerHTML = '<i class="fa-solid fa-fingerprint"></i> Create my passkey';
} }
@@ -655,7 +668,15 @@ async function loginWithPasskey() {
try { try {
await window.f0ckAnonPasskey.doAuthenticate(); await window.f0ckAnonPasskey.doAuthenticate();
} catch(e) { } catch(e) {
if (errEl && e && e.name !== 'NotAllowedError') { errEl.style.display = ''; errEl.textContent = e.message || 'Authentication failed.'; } console.warn('[ANON_PASSKEY] Authentication failed:', e);
if (errEl) {
errEl.style.display = '';
if (e && e.name === 'NotAllowedError') {
errEl.textContent = 'Passkey request was cancelled or blocked. If Bitwarden or another password manager is locked, please unlock it.';
} else {
errEl.textContent = (e && e.message) || 'Authentication failed.';
}
}
authBtn.disabled = false; authBtn.disabled = false;
authBtn.innerHTML = '<i class="fa-solid fa-fingerprint"></i> Use my passkey'; authBtn.innerHTML = '<i class="fa-solid fa-fingerprint"></i> Use my passkey';
} }