diff --git a/public/s/css/f0ckm.css b/public/s/css/f0ckm.css index cff124b..06237ce 100644 --- a/public/s/css/f0ckm.css +++ b/public/s/css/f0ckm.css @@ -10277,6 +10277,16 @@ table img { 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 */ .iconset#a_pin { 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; } +.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>i.iconset:hover, .gapRight>button:hover, @@ -17618,6 +17648,33 @@ textarea#profile_description { 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 { font-weight: 700; color: var(--author-accent, inherit) !important; @@ -22487,20 +22544,146 @@ div#flash, } } -/* Chan Rehost Details Modal */ +/* Chan Rehost Details Modal / Popup */ .chan-rehost-modal { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; - background: rgba(0, 0, 0, 0.82); - backdrop-filter: blur(6px); + background: transparent; + backdrop-filter: none; 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; align-items: 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 { background: #18181b; border: 1px solid rgba(255, 255, 255, 0.14); diff --git a/public/s/js/admin.js b/public/s/js/admin.js index 414b670..74a7add 100644 --- a/public/s/js/admin.js +++ b/public/s/js/admin.js @@ -284,6 +284,8 @@ }; + let favSeq = 0; + const toggleFavEvent = async (e) => { if (e && typeof e.preventDefault === 'function') e.preventDefault(); 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; 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 isNowFav = !wasAlreadyFav; const setFavoUi = (isFav) => { favoBtns.forEach(btn => { @@ -308,178 +315,263 @@ btn.classList.add('iconset', 'fa-heart'); btn.classList.toggle('fa-solid', isFav); btn.classList.toggle('fa-regular', !isFav); + btn.title = isFav ? (window.f0ckI18n?.fav_remove || 'Remove from favorites') : (window.f0ckI18n?.fav_add || 'Favorite'); }); }; - const setFavoLoading = () => { - favoBtns.forEach(btn => { - btn.classList.remove('fa-heart'); - btn.classList.add('iconset', 'fa-solid', 'fa-spinner', 'fa-spin'); - }); - }; + // 1. Instantly apply client UI + setFavoUi(isNowFav); - // If viewing un-rehosted 4chan post, auto-rehost first - if (isChan && !localId && chanUrl) { - setFavoLoading(); - try { - const csrfToken = window.f0ckSession?.csrf_token || ''; - const rehostResp = await fetch('/api/v2/scroller/rehost', { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - ...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}) - }, - body: new URLSearchParams({ - url: chanUrl, - original_filename: firstFavoBtn?.dataset?.filename || chanRehostBtn?.dataset?.filename || '', - width: firstFavoBtn?.dataset?.width || chanRehostBtn?.dataset?.width || '', - height: firstFavoBtn?.dataset?.height || chanRehostBtn?.dataset?.height || '' - }) - }); - const rehostData = await rehostResp.json(); - if (rehostData.success && rehostData.item_id) { - localId = rehostData.item_id; - favoBtns.forEach(btn => { - btn.dataset.itemId = localId; - btn.dataset.localId = localId; - }); - const commentsEl = document.querySelector("#comments-container"); - if (commentsEl) commentsEl.dataset.itemId = localId; - const infoEl = document.querySelector("#a_info"); - if (infoEl) infoEl.dataset.itemId = localId; + // Micro-animation for tactile pop + favoBtns.forEach(btn => { + btn.classList.remove('fav-pop'); + void btn.offsetWidth; + btn.classList.add('fav-pop'); + }); - const timeEl = document.querySelector('time.timeago'); - if (timeEl) { - const nowIso = new Date().toISOString(); - timeEl.dataset.iso = nowIso; - const fullDate = typeof window.f0ckFormatDateFull === 'function' ? window.f0ckFormatDateFull(nowIso) : new Date().toLocaleString(); - timeEl.setAttribute('tooltip', fullDate); - timeEl.textContent = (window.f0ckTimeAgo ? window.f0ckTimeAgo(nowIso) : (window.f0ckI18n?.timeago_just_now || 'just now')); + // 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 (chanRehostBtn) { - chanRehostBtn.classList.add('rehosted'); - chanRehostBtn.outerHTML = ` - - #${localId} - - - `; + 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 (isChan && !localId && chanUrl) { + if (mySeq !== favSeq) return; + const csrfToken = window.f0ckSession?.csrf_token || ''; + const rehostResp = await fetch('/api/v2/scroller/rehost', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + ...(csrfToken ? { 'X-CSRF-Token': csrfToken } : {}) + }, + body: new URLSearchParams({ + url: chanUrl, + original_filename: firstFavoBtn?.dataset?.filename || chanRehostBtn?.dataset?.filename || '', + width: firstFavoBtn?.dataset?.width || chanRehostBtn?.dataset?.width || '', + height: firstFavoBtn?.dataset?.height || chanRehostBtn?.dataset?.height || '' + }) + }); + const rehostData = await rehostResp.json(); + if (rehostData.success && rehostData.item_id) { + localId = rehostData.item_id; + favoBtns.forEach(btn => { + btn.dataset.itemId = localId; + btn.dataset.localId = localId; + }); + const commentsEl = document.querySelector("#comments-container"); + if (commentsEl) commentsEl.dataset.itemId = localId; + const infoEl = document.querySelector("#a_info"); + if (infoEl) infoEl.dataset.itemId = localId; + + const timeEl = document.querySelector('time.timeago'); + if (timeEl) { + const nowIso = new Date().toISOString(); + timeEl.dataset.iso = nowIso; + const fullDate = typeof window.f0ckFormatDateFull === 'function' ? window.f0ckFormatDateFull(nowIso) : new Date().toLocaleString(); + timeEl.setAttribute('tooltip', fullDate); + timeEl.textContent = (window.f0ckTimeAgo ? window.f0ckTimeAgo(nowIso) : (window.f0ckI18n?.timeago_just_now || 'just now')); + } + + if (chanRehostBtn) { + chanRehostBtn.classList.add('rehosted'); + chanRehostBtn.outerHTML = ` + + #${localId} + + + `; + } + } else { + throw new Error(rehostData.msg || 'Rehost failed'); + } + } + + const effectivePostId = localId ? Number(localId) : (postid || getContext()?.postid); + if (!effectivePostId) { + throw new Error('Missing post ID'); + } + + if (mySeq !== favSeq) return; + + const res = await post('/api/v2/togglefav', { + postid: effectivePostId, + chan_url: chanUrl, + action: isNowFav ? 'add' : 'delete', + favorited: isNowFav + }); + + if (mySeq !== favSeq) return; + + if (res && res.success) { + if (window.invalidateItemCache) { + window.invalidateItemCache(effectivePostId); + } + const finalIsFav = (res.favorited !== undefined) ? !!res.favorited : isNowFav; + setFavoUi(finalIsFav); + + const curFavContainer = document.querySelector('#favs'); + if (curFavContainer && Array.isArray(res.favs)) { + curFavContainer.innerHTML = ""; + if (res.favs.length > 0) { + const isAnonymized = !!(window.f0ckSession?.is_anonymized ?? (window.f0ckSession?.guest_anonymize && !window.f0ckSession?.logged_in) ?? (window.f0ckSession?.is_anon && window.f0ckSession?.anon_anonymize)); + const fragment = document.createDocumentFragment(); + res.favs.forEach(f => { + const isSelf = window.f0ckSession && ( + (window.f0ckSession.id && f.user_id && Number(window.f0ckSession.id) === Number(f.user_id)) || + (window.f0ckSession.user && f.user && window.f0ckSession.user.toLowerCase() === f.user.toLowerCase()) || + (window.f0ckSession.login && f.login && window.f0ckSession.login.toLowerCase() === f.login.toLowerCase()) + ); + const isFavAnon = f.is_anon || f.user === 'anonymous' || (typeof f.user === 'string' && f.user.startsWith('anon_')); + + if (f.hide_fav_badge && !isSelf) { + const a = document.createElement('a'); + a.className = 'ghost-fav'; + a.setAttribute('tooltip', '?'); + a.setAttribute('flow', 'up'); + a.style.cursor = 'default'; + + const img = document.createElement('img'); + img.src = '/s/img/ghost_fav.svg'; + img.style.height = "32px"; + img.style.width = "32px"; + + a.appendChild(img); + fragment.appendChild(a); + } else if (isAnonymized || isFavAnon) { + const a = document.createElement('a'); + a.className = 'ghost-fav'; + if (isSelf) a.dataset.self = 'true'; + a.setAttribute('tooltip', 'anonymous'); + a.setAttribute('flow', 'up'); + a.style.cursor = 'default'; + + const img = document.createElement('img'); + img.src = '/a/default.png'; + img.style.height = "32px"; + img.style.width = "32px"; + + a.appendChild(img); + fragment.appendChild(a); + } else { + const a = document.createElement('a'); + if (isSelf) a.dataset.self = 'true'; + a.href = `/user/${(f.user || '').toLowerCase()}`; + a.setAttribute('tooltip', f.display_name || f.user || 'anonymous'); + a.setAttribute('flow', 'up'); + + const img = document.createElement('img'); + img.src = f.avatar_file ? `/a/${f.avatar_file}` : (f.avatar ? `/t/${f.avatar}.webp` : '/a/default.png'); + img.style.height = "32px"; + img.style.width = "32px"; + if (f.username_color) img.style.borderColor = f.username_color; + + a.appendChild(img); + fragment.appendChild(a); + } + }); + curFavContainer.appendChild(fragment); + curFavContainer.hidden = false; + } else { + curFavContainer.hidden = true; + } } } else { - 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(); - if (!ctx) { - setFavoUi(wasAlreadyFav); - return; - } - const postid = localId ? Number(localId) : ctx.postid; - - try { - const res = await post('/api/v2/togglefav', { - postid: postid, - chan_url: chanUrl - }); - if (res && res.success) { - if (window.invalidateItemCache) { - window.invalidateItemCache(postid); - } - // Authoritative server state takes priority; fallback to inverting previous UI state - const isNowFav = (res.favorited !== undefined) ? !!res.favorited : !wasAlreadyFav; - - setFavoUi(isNowFav); - - const favcontainer = document.querySelector('#favs'); - if (favcontainer) { - favcontainer.innerHTML = ""; - if (res.favs && res.favs.length > 0) { - const isAnonymized = !!(window.f0ckSession?.is_anonymized ?? (window.f0ckSession?.guest_anonymize && !window.f0ckSession?.logged_in) ?? (window.f0ckSession?.is_anon && window.f0ckSession?.anon_anonymize)); - const fragment = document.createDocumentFragment(); - res.favs.forEach(f => { - const isSelf = window.f0ckSession && ( - (window.f0ckSession.id && f.user_id && Number(window.f0ckSession.id) === Number(f.user_id)) || - (window.f0ckSession.user && f.user && window.f0ckSession.user.toLowerCase() === f.user.toLowerCase()) || - (window.f0ckSession.login && f.login && window.f0ckSession.login.toLowerCase() === f.login.toLowerCase()) - ); - const isFavAnon = f.is_anon || f.user === 'anonymous' || (typeof f.user === 'string' && f.user.startsWith('anon_')); - - if (f.hide_fav_badge && !isSelf) { - const a = document.createElement('a'); - a.className = 'ghost-fav'; - a.setAttribute('tooltip', '?'); - a.setAttribute('flow', 'up'); - a.style.cursor = 'default'; - - const img = document.createElement('img'); - img.src = '/s/img/ghost_fav.svg'; - img.style.height = "32px"; - img.style.width = "32px"; - - a.appendChild(img); - fragment.appendChild(a); - } else if (isAnonymized || isFavAnon) { - const a = document.createElement('a'); - a.className = 'ghost-fav'; - a.setAttribute('tooltip', 'anonymous'); - a.setAttribute('flow', 'up'); - a.style.cursor = 'default'; - - const img = document.createElement('img'); - img.src = '/a/default.png'; - img.style.height = "32px"; - img.style.width = "32px"; - - a.appendChild(img); - fragment.appendChild(a); - } else { - const a = document.createElement('a'); - a.href = `/user/${(f.user || '').toLowerCase()}`; - a.setAttribute('tooltip', f.display_name || f.user || 'anonymous'); - a.setAttribute('flow', 'up'); - - const img = document.createElement('img'); - img.src = f.avatar_file ? `/a/${f.avatar_file}` : (f.avatar ? `/t/${f.avatar}.webp` : '/a/default.png'); - img.style.height = "32px"; - img.style.width = "32px"; - if (f.username_color) img.style.borderColor = f.username_color; - - a.appendChild(img); - fragment.appendChild(a); - } - }); - favcontainer.appendChild(fragment); - favcontainer.hidden = false; - } else { - favcontainer.hidden = true; + if (mySeq !== favSeq) return; + 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.'; + if (typeof window.flashMessage === 'function') { + window.flashMessage(errMsg, 3000, 'error'); } } - - 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 { + } catch (err) { + console.error('[CHAN-FAV-ADMIN] Error during background favorite sync:', err); + if (mySeq !== favSeq) return; setFavoUi(wasAlreadyFav); - const errMsg = (res && (res.msg || res.error)) || 'Anonymous favoriting is disabled.'; + const curFavContainer = document.querySelector('#favs'); + if (curFavContainer) { + curFavContainer.innerHTML = prevFavsHtml; + curFavContainer.hidden = prevFavsHidden; + } if (typeof window.flashMessage === 'function') { - window.flashMessage(errMsg, 3000, 'error'); + window.flashMessage('Failed to update favorite.', 3000, 'error'); } } - } catch (err) { - setFavoUi(wasAlreadyFav); - if (typeof window.flashMessage === 'function') { - window.flashMessage('Failed to update favorite.', 3000, 'error'); - } - } + })(); }; const deleteSubItemEvent = async (subf0ck, postid) => { diff --git a/public/s/js/anon_ssh.js b/public/s/js/anon_ssh.js index 83539e5..e613618 100644 --- a/public/s/js/anon_ssh.js +++ b/public/s/js/anon_ssh.js @@ -578,7 +578,12 @@ if (typeof window.showToastNotification === 'function') window.showToastNotification('New passkey registered!'); this._refreshModalContent(); } 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 { addBtn.disabled = false; } diff --git a/public/s/js/f0ckm.js b/public/s/js/f0ckm.js index 48675e9..81def84 100644 --- a/public/s/js/f0ckm.js +++ b/public/s/js/f0ckm.js @@ -1924,6 +1924,39 @@ window.cancelAnimFrame = (function () { // 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')); + // 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; if (url) { try { @@ -2177,6 +2210,47 @@ window.cancelAnimFrame = (function () { const currentSeq = ++_onaraSyncSeq; 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})$/, '') || '/'; let targetPage = (typeof knownPage === 'number' && knownPage >= 1) ? knownPage : null; @@ -2364,6 +2438,10 @@ window.cancelAnimFrame = (function () { } catch {} } + if (path.startsWith('/4/')) { + return path; + } + const postsEl = document.querySelector('.posts'); const activePagSpan = document.querySelector('.pagination span.btn.disabled, .pagination span.disabled'); const pagPage = activePagSpan ? parseInt(activePagSpan.textContent.trim(), 10) : 0; @@ -2452,7 +2530,7 @@ window.cancelAnimFrame = (function () { window._onaraReturnTitle = null; // 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) { try { activeThumb.focus({ preventScroll: true }); @@ -4437,7 +4515,7 @@ window.cancelAnimFrame = (function () { const doc = new DOMParser().parseFromString(html, 'text/html'); const gridContent = doc.querySelector('#main') || doc.body; if (gridContent) { - main.className = ''; + main.className = gridContent.className || ''; main.innerHTML = gridContent.innerHTML; if (typeof window.initLazyLoading === 'function') { window.initLazyLoading(); @@ -4452,7 +4530,7 @@ window.cancelAnimFrame = (function () { } if (activeKey) { - updateOnaraActiveItem(activeKey, window.location.href); + updateOnaraActiveItem(activeKey, window.location.href, true); if (typeof window.trackVisit === 'function') { 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 activeKey = keySegments.length ? keySegments[keySegments.length - 1] : null; if (activeKey) { - updateOnaraActiveItem(activeKey, window.location.href); + updateOnaraActiveItem(activeKey, window.location.href, true); if (typeof window.trackVisit === 'function') { window.trackVisit(activeKey); } @@ -11261,7 +11339,10 @@ window.cancelAnimFrame = (function () { if (isItem) { 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/ or /tag/foo/ hrefs // (via link.main), so they are unaffected by this guard. // 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) { const wTagMatch = window.location.href.match(/\/tag\/([^/?]+)/); if (wTagMatch) tag = decodeURIComponent(wTagMatch[1]); @@ -12592,6 +12678,8 @@ window.cancelAnimFrame = (function () { promoteOnaraModals(); const onaraModal = document.getElementById('onara-modal'); 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) { requestAnimationFrame(() => window.scrollTo(0, history.state.scroll)); } else if (!options.keepMedia) { @@ -12601,7 +12689,10 @@ window.cancelAnimFrame = (function () { const _hash = new URL(url, window.location.origin).hash; const itemKey = (window.f0ckSession?.enable_item_slugs !== false && _cachedItem.slug) ? _cachedItem.slug : (_cachedItem.slug || itemid); 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 (isFavs) _pushUrl = `/favs/${itemKey}`; else if (tag) _pushUrl = `/tag/${encodeURIComponent(tag).replace(/%2C/g,',').replace(/%20/g,' ')}/${itemKey}`; @@ -12952,7 +13043,11 @@ window.cancelAnimFrame = (function () { if (!options.keepMedia && !options.skipPush) { // We overwrite proper URL even if the link clicked was "naked" - history.pushState({ onara: useOnara }, '', pushUrl); + if (options.replace || options.replaceState) { + history.replaceState({ onara: useOnara }, '', pushUrl); + } else { + history.pushState({ onara: useOnara }, '', pushUrl); + } } if (!options.keepMedia) { @@ -13507,121 +13602,195 @@ window.cancelAnimFrame = (function () { } } - window.openChanRehostModal = function({ itemId, isPlayerView = false, onSave = null }) { + window.openChanRehostModal = function({ itemId, onSave = null, anchorEl = null }) { if (!itemId) return; let modal = document.getElementById('chan-rehost-modal'); if (!modal) { modal = document.createElement('div'); modal.id = 'chan-rehost-modal'; - modal.className = 'modal-overlay chan-rehost-modal'; - modal.style.display = 'none'; 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 = ` -