fdsa
This commit is contained in:
@@ -3164,6 +3164,18 @@ body.sidebar-right-hidden #sidebar-drag-zone {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.thumb > .sidebar-media-placeholder {
|
||||
z-index: 1;
|
||||
font-size: 2.8em;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.thumb:has(.sidebar-media-placeholder),
|
||||
.thumb.thumb-fallback:has(.sidebar-media-placeholder) {
|
||||
opacity: 1 !important;
|
||||
background-image: none !important;
|
||||
}
|
||||
|
||||
.sidebar-video-details {
|
||||
display: flex;
|
||||
margin-top: 8px;
|
||||
|
||||
+193
-55
@@ -297,7 +297,7 @@ window.cancelAnimFrame = (function () {
|
||||
* Forcefully refreshes all thumbnail occurrences for a specific item in the DOM.
|
||||
* Handles grid items (data-bg), images (src), and the background canvas.
|
||||
*/
|
||||
window.refreshItemThumbnails = (itemId, timestamp = Date.now()) => {
|
||||
window.refreshItemThumbnails = (itemId, timestamp = Date.now(), hasCoverart = undefined) => {
|
||||
if (!itemId) return;
|
||||
const idStr = String(itemId);
|
||||
|
||||
@@ -340,15 +340,28 @@ window.cancelAnimFrame = (function () {
|
||||
// Update audio cover in player if viewing this audio item
|
||||
const audioCover = document.getElementById('f0ck-audio-cover');
|
||||
if (audioCover && currentId === idStr) {
|
||||
const coverUrl = `/ca/${idStr}.webp?t=${timestamp}`;
|
||||
audioCover.src = coverUrl;
|
||||
const parent = audioCover.parentElement;
|
||||
if (parent) {
|
||||
parent.style.background = `url('${coverUrl}') no-repeat center / contain black`;
|
||||
}
|
||||
const audioEl = document.querySelector('audio#my-video');
|
||||
if (audioEl) {
|
||||
audioEl.setAttribute('poster', coverUrl);
|
||||
if (hasCoverart === true || (hasCoverart === undefined && audioCover.src && audioCover.src.includes('/ca/'))) {
|
||||
const coverUrl = `/ca/${idStr}.webp?t=${timestamp}`;
|
||||
audioCover.src = coverUrl;
|
||||
const parent = audioCover.parentElement;
|
||||
if (parent) {
|
||||
parent.style.background = `url('${coverUrl}') no-repeat center / contain black`;
|
||||
}
|
||||
const audioEl = document.querySelector('audio#my-video');
|
||||
if (audioEl) {
|
||||
audioEl.setAttribute('poster', coverUrl);
|
||||
}
|
||||
} else if (hasCoverart === false) {
|
||||
const fallbackUrl = '/s/img/audio.webp';
|
||||
audioCover.src = fallbackUrl;
|
||||
const parent = audioCover.parentElement;
|
||||
if (parent) {
|
||||
parent.style.background = `url('${fallbackUrl}') no-repeat center / contain black`;
|
||||
}
|
||||
const audioEl = document.querySelector('audio#my-video');
|
||||
if (audioEl) {
|
||||
audioEl.setAttribute('poster', fallbackUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -434,7 +447,19 @@ window.cancelAnimFrame = (function () {
|
||||
applyThumb(thumb, finalBg);
|
||||
} else {
|
||||
img.onload = () => applyThumb(thumb, finalBg);
|
||||
img.onerror = () => thumb.classList.remove('lazy-thumb');
|
||||
img.onerror = () => {
|
||||
const mime = thumb.dataset.mime || '';
|
||||
if (mime.startsWith('audio/')) {
|
||||
if (!thumb.querySelector('.sidebar-media-placeholder.audio')) {
|
||||
const ph = document.createElement('div');
|
||||
ph.className = 'sidebar-media-placeholder audio';
|
||||
ph.innerHTML = '<i class="fa-solid fa-music"></i>';
|
||||
thumb.prepend(ph);
|
||||
}
|
||||
thumb.classList.add('thumb-fallback');
|
||||
}
|
||||
thumb.classList.remove('lazy-thumb');
|
||||
};
|
||||
}
|
||||
thumb.dataset.lazyObserved = 'true';
|
||||
}
|
||||
@@ -463,7 +488,12 @@ window.cancelAnimFrame = (function () {
|
||||
} else {
|
||||
const mime = thumb.dataset.mime || '';
|
||||
if (mime.startsWith('audio/')) {
|
||||
thumb.style.setProperty('--thumb-bg', `url('/s/img/audio.webp')`);
|
||||
if (!thumb.querySelector('.sidebar-media-placeholder.audio')) {
|
||||
const ph = document.createElement('div');
|
||||
ph.className = 'sidebar-media-placeholder audio';
|
||||
ph.innerHTML = '<i class="fa-solid fa-music"></i>';
|
||||
thumb.prepend(ph);
|
||||
}
|
||||
thumb.classList.add('thumb-fallback');
|
||||
}
|
||||
thumb.classList.remove('lazy-thumb');
|
||||
@@ -1346,8 +1376,8 @@ window.cancelAnimFrame = (function () {
|
||||
};
|
||||
window.openOnaraModal = openOnaraModal;
|
||||
|
||||
const updateOnaraActiveItem = (itemid, url) => {
|
||||
if (!isOnaraActive()) return;
|
||||
const updateOnaraActiveItem = (itemid, url, forceScroll = false, slug = null) => {
|
||||
if (!isOnaraActive()) return null;
|
||||
// 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'));
|
||||
|
||||
@@ -1358,6 +1388,9 @@ window.cancelAnimFrame = (function () {
|
||||
targetThumb = document.querySelector(`.posts > a.thumb[href="${parsedPath}"], .posts > a.thumb[href="${url}"]`);
|
||||
} catch {}
|
||||
}
|
||||
if (!targetThumb && slug) {
|
||||
targetThumb = document.querySelector(`.posts > a.thumb[href$="/${slug}"]`);
|
||||
}
|
||||
if (!targetThumb && itemid) {
|
||||
targetThumb = document.querySelector(`.posts > a.thumb[href$="/${itemid}"], .posts > a.thumb[data-bg*="/${itemid}."]`);
|
||||
}
|
||||
@@ -1374,13 +1407,140 @@ window.cancelAnimFrame = (function () {
|
||||
const vh = window.innerHeight || document.documentElement.clientHeight;
|
||||
// If any part of the thumbnail is already visible in the viewport, do not scroll
|
||||
const isVisible = rect.bottom > navbarH && rect.top < vh;
|
||||
if (!isVisible) {
|
||||
targetThumb.scrollIntoView({ block: 'nearest', inline: 'nearest', behavior: 'instant' });
|
||||
if (!isVisible || forceScroll) {
|
||||
targetThumb.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
|
||||
}
|
||||
}
|
||||
return targetThumb;
|
||||
};
|
||||
window.updateOnaraActiveItem = updateOnaraActiveItem;
|
||||
|
||||
let _onaraSyncSeq = 0;
|
||||
const syncOnaraBackgroundGrid = async (itemid, url, knownPage = null, slug = null) => {
|
||||
if (!isOnaraActive()) return;
|
||||
|
||||
// Fast path: if target item is already in current background DOM, just highlight and ensure visibility
|
||||
const existingThumb = updateOnaraActiveItem(itemid, url, false, slug);
|
||||
if (existingThumb) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSeq = ++_onaraSyncSeq;
|
||||
const urlObj = new URL(url || window.location.href, window.location.origin);
|
||||
const feedBasePath = urlObj.pathname.replace(/\/+$/, '').replace(/\/(?:\d+|[a-zA-Z0-9_-]{11})$/, '') || '/';
|
||||
|
||||
let targetPage = (typeof knownPage === 'number' && knownPage >= 1) ? knownPage : null;
|
||||
|
||||
if (!targetPage) {
|
||||
try {
|
||||
const pageParams = new URLSearchParams();
|
||||
pageParams.set('id', itemid);
|
||||
if (feedBasePath.startsWith('/tag/')) {
|
||||
pageParams.set('tag', decodeURIComponent(feedBasePath.replace(/^\/tag\//, '')));
|
||||
} else if (feedBasePath.startsWith('/h/')) {
|
||||
pageParams.set('hall', decodeURIComponent(feedBasePath.replace(/^\/h\//, '')));
|
||||
} else if (feedBasePath.match(/^\/user\/([^/]+)\/hall\/([^/]+)/)) {
|
||||
const m = feedBasePath.match(/^\/user\/([^/]+)\/hall\/([^/]+)/);
|
||||
pageParams.set('userHallOwner', decodeURIComponent(m[1]));
|
||||
pageParams.set('userHall', decodeURIComponent(m[2]));
|
||||
} else if (feedBasePath.match(/^\/user\/([^/]+)\/favs/)) {
|
||||
const m = feedBasePath.match(/^\/user\/([^/]+)\/favs/);
|
||||
pageParams.set('user', decodeURIComponent(m[1]));
|
||||
pageParams.set('fav', 'true');
|
||||
} else if (feedBasePath.match(/^\/user\/([^/]+)/)) {
|
||||
const m = feedBasePath.match(/^\/user\/([^/]+)/);
|
||||
pageParams.set('user', decodeURIComponent(m[1]));
|
||||
} else if (feedBasePath === '/favs') {
|
||||
pageParams.set('fav', 'true');
|
||||
}
|
||||
if (typeof window.activeMode !== 'undefined') {
|
||||
pageParams.set('mode', window.activeMode);
|
||||
}
|
||||
const isStrict = window.f0ckSession?.strict_mode || (localStorage.getItem('search_strict') === 'true') || urlObj.searchParams.get('strict') === '1';
|
||||
if (isStrict) {
|
||||
pageParams.set('strict', '1');
|
||||
}
|
||||
const resp = await fetch(`/api/v2/item-page?${pageParams.toString()}`);
|
||||
if (resp.ok) {
|
||||
const pageData = await resp.json();
|
||||
if (pageData && pageData.success && pageData.page) {
|
||||
targetPage = pageData.page;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[ONARA] Failed to fetch item-page:', e);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentSeq !== _onaraSyncSeq) return;
|
||||
if (!targetPage || targetPage < 1) targetPage = 1;
|
||||
|
||||
const pagePath = targetPage === 1 ? feedBasePath : (feedBasePath === '/' ? `/p/${targetPage}` : `${feedBasePath}/p/${targetPage}`);
|
||||
const searchParams = new URLSearchParams(urlObj.search);
|
||||
const searchStr = searchParams.toString() ? `?${searchParams.toString()}` : '';
|
||||
const pageUrl = pagePath + searchStr;
|
||||
|
||||
try {
|
||||
const pageRes = await fetch(pageUrl, {
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
});
|
||||
if (currentSeq !== _onaraSyncSeq) return;
|
||||
if (!pageRes.ok) return;
|
||||
|
||||
const html = await pageRes.text();
|
||||
if (currentSeq !== _onaraSyncSeq) return;
|
||||
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
const newPosts = doc.querySelector('.posts');
|
||||
const oldPosts = document.querySelector('.posts');
|
||||
|
||||
if (newPosts && oldPosts) {
|
||||
oldPosts.innerHTML = newPosts.innerHTML;
|
||||
oldPosts.dataset.currentPage = targetPage;
|
||||
if (newPosts.dataset.hasMore) {
|
||||
oldPosts.dataset.hasMore = newPosts.dataset.hasMore;
|
||||
}
|
||||
if (typeof window.initLazyLoading === 'function') {
|
||||
window.initLazyLoading();
|
||||
}
|
||||
}
|
||||
|
||||
const newPag = doc.querySelector('.pagination-wrapper');
|
||||
const oldPag = document.querySelector('.pagination-wrapper');
|
||||
if (newPag && oldPag) {
|
||||
oldPag.innerHTML = newPag.innerHTML;
|
||||
}
|
||||
|
||||
const newTitle = doc.querySelector('.page-title-wrap, .page-title, .user-profile-header, .hall-header');
|
||||
const oldTitle = document.querySelector('.page-title-wrap, .page-title, .user-profile-header, .hall-header');
|
||||
if (newTitle && oldTitle) {
|
||||
oldTitle.replaceWith(newTitle);
|
||||
}
|
||||
|
||||
window._onaraCurrentGridUrl = pageUrl;
|
||||
window._onaraReturnUrl = pageUrl;
|
||||
if (doc.title) {
|
||||
window._onaraReturnTitle = doc.title;
|
||||
}
|
||||
|
||||
const thumb = updateOnaraActiveItem(itemid, url, true, slug);
|
||||
if (!thumb && oldPosts) {
|
||||
const synthThumb = document.createElement('a');
|
||||
synthThumb.href = url;
|
||||
synthThumb.className = 'thumb lazy-thumb onara-active';
|
||||
synthThumb.dataset.bg = `/t/${itemid}.webp`;
|
||||
synthThumb.dataset.size = '1';
|
||||
synthThumb.innerHTML = '<div class="thumb-indicators"></div><p></p>';
|
||||
oldPosts.prepend(synthThumb);
|
||||
if (typeof window.initLazyLoading === 'function') window.initLazyLoading();
|
||||
synthThumb.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[ONARA] Failed to sync background grid:', err);
|
||||
}
|
||||
};
|
||||
window.syncOnaraBackgroundGrid = syncOnaraBackgroundGrid;
|
||||
|
||||
const isItemPath = (pathOrUrl) => {
|
||||
try {
|
||||
const p = new URL(pathOrUrl, window.location.origin).pathname;
|
||||
@@ -4436,6 +4596,10 @@ window.cancelAnimFrame = (function () {
|
||||
});
|
||||
}
|
||||
|
||||
if (useOnara) {
|
||||
params.append('onara', '1');
|
||||
}
|
||||
|
||||
if (params.toString() !== '') {
|
||||
ajaxUrl += (ajaxUrl.includes('?') ? '&' : '?') + params.toString();
|
||||
}
|
||||
@@ -4466,7 +4630,8 @@ window.cancelAnimFrame = (function () {
|
||||
onaraMount.innerHTML = '';
|
||||
}
|
||||
_container = onaraMount;
|
||||
updateOnaraActiveItem(itemid, url);
|
||||
updateOnaraActiveItem(itemid, url, false, _cachedItem.slug);
|
||||
syncOnaraBackgroundGrid(itemid, url, _cachedItem.page, _cachedItem.slug);
|
||||
} else {
|
||||
_container = document.querySelector('#main .container') || (document.getElementById('main')?.classList.contains('item-view') ? document.getElementById('main') : null);
|
||||
const _isStructuralPage = !!document.querySelector('.pagewrapper');
|
||||
@@ -4597,12 +4762,14 @@ window.cancelAnimFrame = (function () {
|
||||
if (!freshText) return;
|
||||
let freshHtml = freshText;
|
||||
let freshSlug = null;
|
||||
let freshPage = null;
|
||||
try {
|
||||
const d = JSON.parse(freshText);
|
||||
if (d && typeof d.html === 'string') freshHtml = d.html;
|
||||
if (d && (d.slug || d.item?.slug)) freshSlug = d.slug || d.item?.slug;
|
||||
if (d && d.page) freshPage = d.page;
|
||||
} catch(_) {}
|
||||
itemCacheMap.set(_itemCacheKey, { html: freshHtml, slug: freshSlug, ts: Date.now() });
|
||||
itemCacheMap.set(_itemCacheKey, { html: freshHtml, slug: freshSlug, page: freshPage, ts: Date.now() });
|
||||
window.f0ckDebug('[itemCache] Background revalidation complete for', _itemCacheKey);
|
||||
})
|
||||
.catch(() => {});
|
||||
@@ -4629,7 +4796,7 @@ window.cancelAnimFrame = (function () {
|
||||
- Total Network: ${(tBody - tStart).toFixed(2)}ms
|
||||
- Content Size: ${(rawText.length / 1024).toFixed(2)} KB`);
|
||||
|
||||
let html, paginationHtml, responseSlug = null;
|
||||
let html, paginationHtml, responseSlug = null, responsePage = null;
|
||||
|
||||
try {
|
||||
// Optimistically try to parse as JSON first
|
||||
@@ -4643,6 +4810,7 @@ window.cancelAnimFrame = (function () {
|
||||
html = data.html;
|
||||
paginationHtml = data.pagination;
|
||||
responseSlug = data.slug || data.item?.slug || null;
|
||||
responsePage = data.page || null;
|
||||
} else {
|
||||
html = rawText;
|
||||
}
|
||||
@@ -4653,7 +4821,7 @@ window.cancelAnimFrame = (function () {
|
||||
|
||||
// ── Store in item cache (stale-while-revalidate) ───────────────────────
|
||||
if (html && !options.noCacheStore) {
|
||||
itemCacheMap.set(_itemCacheKey, { html, slug: responseSlug, ts: Date.now() });
|
||||
itemCacheMap.set(_itemCacheKey, { html, slug: responseSlug, page: responsePage, ts: Date.now() });
|
||||
if (itemCacheMap.size > ITEM_CACHE_MAX) {
|
||||
// Evict oldest entry
|
||||
itemCacheMap.delete(itemCacheMap.keys().next().value);
|
||||
@@ -4673,7 +4841,8 @@ window.cancelAnimFrame = (function () {
|
||||
onaraMount.innerHTML = '';
|
||||
}
|
||||
container = onaraMount;
|
||||
updateOnaraActiveItem(itemid, url);
|
||||
updateOnaraActiveItem(itemid, url, false, responseSlug);
|
||||
syncOnaraBackgroundGrid(itemid, url, responsePage, responseSlug);
|
||||
} else {
|
||||
container = document.querySelector('#main .container') || (document.getElementById('main') && document.getElementById('main').classList.contains('item-view') ? document.getElementById('main') : null);
|
||||
const isStructuralPage = !!document.querySelector('.pagewrapper');
|
||||
@@ -5121,38 +5290,7 @@ window.cancelAnimFrame = (function () {
|
||||
|
||||
// Background grid sync — page number already in response, no extra fetch needed
|
||||
if (isOnaraActive() && targetId) {
|
||||
const postsEl = document.querySelector('.posts[data-current-page]');
|
||||
const currentPage = postsEl ? parseInt(postsEl.dataset.currentPage, 10) : 1;
|
||||
|
||||
if (targetPage !== currentPage) {
|
||||
// Build page URL from current grid base
|
||||
let gridBase = window._onaraCurrentGridUrl
|
||||
? window._onaraCurrentGridUrl.replace(/\/p\/\d+/, '').replace(/\/$/, '')
|
||||
: window.location.pathname
|
||||
.replace(/\/[a-zA-Z0-9_-]{11}$/, '')
|
||||
.replace(/\/\d+$/, '')
|
||||
.replace(/\/p\/\d+/, '')
|
||||
.replace(/\/$/, '');
|
||||
const pageUrl = targetPage === 1 ? (gridBase || '/') : `${gridBase}/p/${targetPage}`;
|
||||
|
||||
fetch(pageUrl)
|
||||
.then(r => r.text())
|
||||
.then(html => {
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
const newPosts = doc.querySelector('.posts');
|
||||
const oldPosts = document.querySelector('.posts');
|
||||
if (newPosts && oldPosts) {
|
||||
oldPosts.innerHTML = newPosts.innerHTML;
|
||||
oldPosts.dataset.currentPage = targetPage;
|
||||
if (typeof window.initLazyLoading === 'function') window.initLazyLoading();
|
||||
}
|
||||
window._onaraCurrentGridUrl = pageUrl;
|
||||
if (typeof updateOnaraActiveItem === 'function') {
|
||||
updateOnaraActiveItem(targetId, `/${targetKey}`);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
syncOnaraBackgroundGrid(targetId, `/${targetKey}`, targetPage, targetKey);
|
||||
}
|
||||
} else if (params.has('tag') || params.has('hall') || params.has('user') || params.has('userHall')) {
|
||||
// Context had no matching items with the active MIME filter — try global random with the same filter
|
||||
@@ -9110,7 +9248,7 @@ class NotificationSystem {
|
||||
} else if (data.type === 'rethumb') {
|
||||
window.f0ckDebug(`[SSE] Rethumb update received for item ${data.data?.item_id}`);
|
||||
if (data.data && data.data.item_id && window.refreshItemThumbnails) {
|
||||
window.refreshItemThumbnails(data.data.item_id);
|
||||
window.refreshItemThumbnails(data.data.item_id, Date.now(), data.data.has_coverart);
|
||||
}
|
||||
} else if (data.type === 'new_item') {
|
||||
window.f0ckDebug(`[SSE] New item received:`, data.data);
|
||||
@@ -13838,7 +13976,7 @@ document.addEventListener('click', (e) => {
|
||||
// Cache-bust thumbnails and player media on page
|
||||
const timestamp = Date.now();
|
||||
if (typeof window.refreshItemThumbnails === 'function') {
|
||||
window.refreshItemThumbnails(itemId, timestamp);
|
||||
window.refreshItemThumbnails(itemId, timestamp, data.has_coverart);
|
||||
} else {
|
||||
document.querySelectorAll(`img[src*="/t/${itemId}.webp"]`).forEach(img => {
|
||||
const url = new URL(img.src, window.location.origin);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"lastId": 48551,
|
||||
"timestamp": "2026-09-13T02:54:17.890Z"
|
||||
}
|
||||
+44
-7
@@ -468,7 +468,13 @@ export default new class queue {
|
||||
this._lastCoverExtracted = false; // Reset state for this call
|
||||
if (link && typeof link === 'string' && link.match(/soundcloud/)) {
|
||||
const proxyArgs = (cfg.main.socks && cfg.main.socks !== 'undefined' && cfg.main.socks !== '') ? ['--proxy', cfg.main.socks.includes('://') ? cfg.main.socks : `socks5h://${cfg.main.socks}`] : [];
|
||||
let cover = (await this.spawn('yt-dlp', [...proxyArgs, '-f', 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w', '--get-thumbnail', link])).stdout.trim().split('\n').map(l => l.trim()).filter(l => l.length > 0).pop();
|
||||
let cover = null;
|
||||
try {
|
||||
const ytRes = await this.spawn('yt-dlp', [...proxyArgs, '--get-thumbnail', link], { quiet: true });
|
||||
cover = ytRes.stdout.trim().split('\n').map(l => l.trim()).filter(l => l.length > 0).pop();
|
||||
} catch (err) {
|
||||
console.warn(`[QUEUE] yt-dlp thumbnail fetch failed for SoundCloud track (${link}):`, err.message || err);
|
||||
}
|
||||
if (cover && !cover.match(/default_avatar/)) {
|
||||
cover = cover.replace(/-(large|original)\./, '-t500x500.');
|
||||
try {
|
||||
@@ -517,13 +523,9 @@ export default new class queue {
|
||||
}
|
||||
} catch (_) { }
|
||||
}
|
||||
// If no cover art found, use audio.webp as the thumbnail
|
||||
// If no cover art found, generate audio placeholder matching .sidebar-media-placeholder.audio
|
||||
if (!coverExtracted) {
|
||||
const audioFallback = path.join(cfg.paths.s, 'img', 'audio.webp');
|
||||
await fs.promises.copyFile(audioFallback, tmpFile).catch(async () => {
|
||||
// If copy fails, fall back to generated placeholder
|
||||
await this.spawn('magick', ['-size', thumbSpec, 'xc:#1a1a1a', '-gravity', 'center', '-fill', '#666', '-pointsize', '40', '-annotate', '0', '♪', tmpFile]).catch(() => {});
|
||||
});
|
||||
await this.genAudioPlaceholder(tmpFile, thumbSpec, thumbSize);
|
||||
}
|
||||
// Store extraction result for caller
|
||||
this._lastCoverExtracted = coverExtracted;
|
||||
@@ -647,6 +649,13 @@ export default new class queue {
|
||||
// Cleanup temp files
|
||||
await fs.promises.unlink(tmpFile).catch(() => {});
|
||||
await fs.promises.unlink(tmpJpg).catch(() => {});
|
||||
if (mime && mime.startsWith('audio/')) {
|
||||
try {
|
||||
await this.genAudioPlaceholder(outPath, thumbSpec, thumbSize);
|
||||
console.warn(`[QUEUE] Used audio placeholder thumbnail for item ${itemid}`);
|
||||
return false;
|
||||
} catch (_) {}
|
||||
}
|
||||
// Fallback: copy 404.gif as the thumbnail
|
||||
const fallback404 = path.join(cfg.paths.s, 'img', '404.gif');
|
||||
try {
|
||||
@@ -659,6 +668,34 @@ export default new class queue {
|
||||
}
|
||||
};
|
||||
|
||||
async genAudioPlaceholder(targetFile, thumbSpec = '512x512', thumbSize = 512) {
|
||||
const faFont = path.join(cfg.paths.s, 'fa', 'webfonts', 'fa-solid-900.ttf');
|
||||
const ptSize = String(Math.round(thumbSize * 0.35));
|
||||
try {
|
||||
await this.spawn('magick', [
|
||||
'-size', thumbSpec, 'radial-gradient:#334a15-#1e1e1e',
|
||||
'(', '+clone', '-font', faFont, '-pointsize', ptSize, '-gravity', 'center', '-fill', 'rgba(153,255,0,0.5)', '-annotate', '0', '\uf001', '-blur', '0x12', ')',
|
||||
'-composite',
|
||||
'-font', faFont, '-pointsize', ptSize, '-gravity', 'center', '-fill', '#99ff00', '-annotate', '0', '\uf001',
|
||||
targetFile
|
||||
]);
|
||||
return true;
|
||||
} catch (_) {
|
||||
const audioFallback = path.join(cfg.paths.s, 'img', 'audio.webp');
|
||||
try {
|
||||
await fs.promises.copyFile(audioFallback, targetFile);
|
||||
return true;
|
||||
} catch (copyErr) {
|
||||
await this.spawn('magick', [
|
||||
'-size', thumbSpec, 'radial-gradient:#334a15-#1e1e1e',
|
||||
'-gravity', 'center', '-fill', '#99ff00', '-pointsize', '60', '-annotate', '0', '♪',
|
||||
targetFile
|
||||
]).catch(() => {});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
async genBlurredThumbnail(itemid, pending = false) {
|
||||
let tDir = pending ? path.join(cfg.paths.pending, 't') : cfg.paths.t;
|
||||
|
||||
@@ -769,6 +769,7 @@ const f0cklib = {
|
||||
items.is_pinned,
|
||||
items.is_oc,
|
||||
items.xd_score,
|
||||
items.has_coverart,
|
||||
${user_id ? db`max(coalesce(uvv.view_count, 0)) as my_views,` : db``}
|
||||
${user_id ? db`EXISTS (SELECT 1 FROM notifications WHERE user_id = ${user_id} AND item_id = items.id AND is_read = false) as has_notification,` : db`false as has_notification,`}
|
||||
(case when min(ta.tag_id) = 1 then 'SFW' when min(ta.tag_id) = 2 then 'NSFW' else 'NSFL' end) as tag,
|
||||
@@ -794,6 +795,7 @@ const f0cklib = {
|
||||
const meta = xdScoreMeta(row.xd_score);
|
||||
row.xd_tier = meta.tier;
|
||||
row.xd_label = meta.label;
|
||||
row.is_audio = !!(row.mime && row.mime.startsWith('audio/'));
|
||||
}
|
||||
|
||||
// Dynamic thumb sizing: applies to the main feed including mime/rating filters.
|
||||
|
||||
+28
-1
@@ -189,6 +189,32 @@ export default (router, tpl) => {
|
||||
- Comments/Sub: ${tAjaxAux - tAjaxFetch}ms
|
||||
- Render: ${tAjaxRender - tAjaxAux}ms`);
|
||||
|
||||
let itemPage = null;
|
||||
if (query.onara === '1' || req.cookies?.onara === '1' || cfg.onara || query.get_page === '1') {
|
||||
try {
|
||||
itemPage = await f0cklib.getItemPage({
|
||||
targetItemId: data.item?.id || itemid,
|
||||
targetItemPinned: data.item?.is_pinned,
|
||||
user: query.user,
|
||||
tag: query.tag,
|
||||
hall: query.hall,
|
||||
userHall: query.userHall,
|
||||
userHallOwner: query.userHallOwner,
|
||||
mime: query.mime || (req.cookies.mime || null),
|
||||
fav: query.fav === 'true',
|
||||
mode: reqMode,
|
||||
ratings: ratingsArr,
|
||||
strict: query.strict === '1' || query.strict === 'true' || req.session?.strict_mode,
|
||||
session: req.session,
|
||||
exclude: req.session ? (req.session.excluded_tags || []) : [],
|
||||
user_id: req.session?.id,
|
||||
is_admin: req.session?.admin,
|
||||
minXdScore: req.session?.min_xd_score || 0,
|
||||
tagger: query.tagger || null
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
res.reply({
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -196,7 +222,8 @@ export default (router, tpl) => {
|
||||
pagination: paginationHtml,
|
||||
title: data.title,
|
||||
id: itemid,
|
||||
slug: data.item?.slug || null
|
||||
slug: data.item?.slug || null,
|
||||
page: itemPage
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
@@ -575,25 +575,25 @@ export default router => {
|
||||
// Lightweight page-lookup for Onara grid sync.
|
||||
// Called async after random navigation — user never waits for this.
|
||||
group.get(/\/item-page$/, async (req, res) => {
|
||||
const id = parseInt(req.url.qs.id, 10) || 0;
|
||||
if (!id) return res.json({ success: false, page: 1 });
|
||||
const rawId = req.url.qs?.id || req.url.qs?.slug || req.url.qs?.item;
|
||||
if (!rawId) return res.json({ success: false, page: 1 });
|
||||
|
||||
const tag = req.url.qs.tag || null;
|
||||
const hall = req.url.qs.hall || null;
|
||||
const userHall = req.url.qs.userHall || null;
|
||||
const userHallOwner= req.url.qs.userHallOwner|| null;
|
||||
const user = req.url.qs.user || null;
|
||||
const cookieMime = req.cookies?.mime !== undefined ? (decodeURIComponent(req.cookies.mime).trim() || null) : null;
|
||||
const mime = (typeof req.url.qs.mime !== 'undefined') ? (req.url.qs.mime || null) : (cookieMime || null);
|
||||
const isFav = req.url.qs.fav === 'true';
|
||||
const isStrict = req.url.qs.strict === '1';
|
||||
const tag = req.url.qs?.tag || null;
|
||||
const hall = req.url.qs?.hall || null;
|
||||
const userHall = req.url.qs?.userHall || null;
|
||||
const userHallOwner= req.url.qs?.userHallOwner|| null;
|
||||
const user = req.url.qs?.user || null;
|
||||
const cookieMime = req.cookies?.mime !== undefined ? (decodeURIComponent(req.cookies.mime).trim() || null) : null;
|
||||
const mime = (typeof req.url.qs?.mime !== 'undefined') ? (req.url.qs.mime || null) : (cookieMime || null);
|
||||
const isFav = req.url.qs?.fav === 'true';
|
||||
const isStrict = req.url.qs?.strict === '1';
|
||||
const mode = req.mode ?? 0;
|
||||
const ratingsRaw = req.cookies.ratings;
|
||||
const ratingsArr = ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)) : null;
|
||||
|
||||
try {
|
||||
const page = await f0cklib.getItemPage({
|
||||
targetItemId: id,
|
||||
targetItemId: rawId,
|
||||
user, tag, hall, userHall, userHallOwner, mime,
|
||||
fav: isFav,
|
||||
mode,
|
||||
@@ -602,7 +602,9 @@ export default router => {
|
||||
session: req.session,
|
||||
exclude: req.session?.excluded_tags || [],
|
||||
user_id: req.session?.id,
|
||||
is_admin: req.session?.admin
|
||||
is_admin: req.session?.admin,
|
||||
minXdScore: req.url.qs?.min_xd !== undefined ? +req.url.qs.min_xd : (req.session?.min_xd_score || 0),
|
||||
tagger: req.url.qs?.tagger || null
|
||||
});
|
||||
return res.json({ success: true, page });
|
||||
} catch (e) {
|
||||
|
||||
@@ -824,10 +824,18 @@ export default router => {
|
||||
|
||||
try {
|
||||
await queue.genThumbnail(filename, mime, itemid, url, isApprovalRequired);
|
||||
if (mime.startsWith('audio/') && queue._lastCoverExtracted) {
|
||||
await db`UPDATE items SET has_coverart = TRUE WHERE id = ${itemid}`;
|
||||
}
|
||||
await queue.genBlurredThumbnail(itemid, isApprovalRequired);
|
||||
} catch (err) {
|
||||
const tDir = isApprovalRequired ? path.join(cfg.paths.pending, 't') : cfg.paths.t;
|
||||
await queue.spawn('magick', ['-size', '128x128', 'xc:#1a1a1a', path.join(tDir, `${itemid}.webp`)]).catch(() => {});
|
||||
const outPath = path.join(tDir, `${itemid}.webp`);
|
||||
if (mime.startsWith('audio/')) {
|
||||
await queue.genAudioPlaceholder(outPath).catch(() => {});
|
||||
} else {
|
||||
await queue.spawn('magick', ['-size', '128x128', 'xc:#1a1a1a', outPath]).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// Assign rating tag (only if a rating was selected)
|
||||
|
||||
@@ -87,17 +87,29 @@ export default (router, tpl) => {
|
||||
`;
|
||||
|
||||
if (items.length > 0) {
|
||||
const inputs = items.map(item => path.join(cfg.paths.t, `${item.id}.webp`));
|
||||
await fs.mkdir(CACHE_DIR, { recursive: true });
|
||||
const inputs = [];
|
||||
for (const item of items) {
|
||||
const filePath = path.join(cfg.paths.t, `${item.id}.webp`);
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
inputs.push(`${filePath}[0]`);
|
||||
} catch {
|
||||
// Ignore missing thumbnail
|
||||
}
|
||||
}
|
||||
|
||||
const { execFile } = await import('child_process');
|
||||
const util = await import('util');
|
||||
const execFilePromise = util.promisify(execFile);
|
||||
if (inputs.length > 0) {
|
||||
await fs.mkdir(CACHE_DIR, { recursive: true });
|
||||
|
||||
await execFilePromise('magick', [...inputs, '+append', '-background', 'none', '-resize', '600x300^', '-gravity', 'center', '-extent', '600x300', cachePath]);
|
||||
const { execFile } = await import('child_process');
|
||||
const util = await import('util');
|
||||
const execFilePromise = util.promisify(execFile);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'image/webp', 'Cache-Control': 'public, max-age=3600' });
|
||||
return res.end(await fs.readFile(cachePath));
|
||||
await execFilePromise('magick', [...inputs, '+append', '-background', 'none', '-resize', '600x300^', '-gravity', 'center', '-extent', '600x300', cachePath]);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'image/webp', 'Cache-Control': 'public, max-age=3600' });
|
||||
return res.end(await fs.readFile(cachePath));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[HALL_IMAGE] Error:', e);
|
||||
|
||||
@@ -38,14 +38,50 @@ export default (router, tpl) => {
|
||||
route: /^\/s\//
|
||||
});
|
||||
|
||||
router.static({
|
||||
dir: cfg.paths.t,
|
||||
route: /^\/t\//
|
||||
const getImgMime = (filename) => {
|
||||
if (filename.endsWith('.webp')) return 'image/webp';
|
||||
if (filename.endsWith('.png')) return 'image/png';
|
||||
if (filename.endsWith('.gif')) return 'image/gif';
|
||||
if (filename.endsWith('.jpg') || filename.endsWith('.jpeg')) return 'image/jpeg';
|
||||
return 'application/octet-stream';
|
||||
};
|
||||
|
||||
router.get(/^\/t\/(?<file>.+)$/, async (req, res) => {
|
||||
const file = req.params.file;
|
||||
const filePath = path.join(cfg.paths.t, file);
|
||||
try {
|
||||
const stat = await fs.stat(filePath);
|
||||
if (stat.isFile()) {
|
||||
const content = await fs.readFile(filePath);
|
||||
res.writeHead(200, {
|
||||
'Content-Length': stat.size,
|
||||
'Content-Type': getImgMime(file),
|
||||
'Cache-Control': 'public, max-age=3600'
|
||||
});
|
||||
return res.end(content);
|
||||
}
|
||||
} catch (_) {}
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
||||
return res.end('404 - file not found.');
|
||||
});
|
||||
|
||||
router.static({
|
||||
dir: cfg.paths.ca,
|
||||
route: /^\/ca\//
|
||||
router.get(/^\/ca\/(?<file>.+)$/, async (req, res) => {
|
||||
const file = req.params.file;
|
||||
const filePath = path.join(cfg.paths.ca, file);
|
||||
try {
|
||||
const stat = await fs.stat(filePath);
|
||||
if (stat.isFile()) {
|
||||
const content = await fs.readFile(filePath);
|
||||
res.writeHead(200, {
|
||||
'Content-Length': stat.size,
|
||||
'Content-Type': getImgMime(file),
|
||||
'Cache-Control': 'public, max-age=3600'
|
||||
});
|
||||
return res.end(content);
|
||||
}
|
||||
} catch (_) {}
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
||||
return res.end('404 - file not found.');
|
||||
});
|
||||
|
||||
router.static({
|
||||
|
||||
@@ -40,11 +40,22 @@ export async function regenerateTagImage(tag, mode) {
|
||||
`;
|
||||
|
||||
if (items.length > 0) {
|
||||
const inputs = items.map(item => path.join(cfg.paths.t, `${item.id}.webp`));
|
||||
const inputs = [];
|
||||
for (const item of items) {
|
||||
const filePath = path.join(cfg.paths.t, `${item.id}.webp`);
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
inputs.push(`${filePath}[0]`);
|
||||
} catch {
|
||||
// Ignore missing thumbnail
|
||||
}
|
||||
}
|
||||
|
||||
await fs.mkdir(path.dirname(cachePath), { recursive: true });
|
||||
await execFilePromise('magick', [...inputs, '+append', '-background', 'none', '-resize', '600x300^', '-gravity', 'center', '-extent', '600x300', cachePath]);
|
||||
return cachePath;
|
||||
if (inputs.length > 0) {
|
||||
await fs.mkdir(path.dirname(cachePath), { recursive: true });
|
||||
await execFilePromise('magick', [...inputs, '+append', '-background', 'none', '-resize', '600x300^', '-gravity', 'center', '-extent', '600x300', cachePath]);
|
||||
return cachePath;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[TAG_IMAGE] Failed to generate image for tag "${tag}" (mode ${mode}):`, err);
|
||||
|
||||
@@ -295,14 +295,26 @@ export default (router, tpl) => {
|
||||
`;
|
||||
|
||||
if (items.length > 0) {
|
||||
const inputs = items.map(item => path.join(cfg.paths.t, `${item.id}.webp`));
|
||||
await fs.mkdir(CACHE_DIR, { recursive: true });
|
||||
await execFile('magick', [
|
||||
...inputs, '+append', '-background', 'none',
|
||||
'-resize', '600x300^', '-gravity', 'center', '-extent', '600x300', cachePath
|
||||
]);
|
||||
res.writeHead(200, { 'Content-Type': 'image/webp', 'Cache-Control': 'public, max-age=3600' });
|
||||
return res.end(await fs.readFile(cachePath));
|
||||
const inputs = [];
|
||||
for (const item of items) {
|
||||
const filePath = path.join(cfg.paths.t, `${item.id}.webp`);
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
inputs.push(`${filePath}[0]`);
|
||||
} catch {
|
||||
// Ignore missing thumbnail
|
||||
}
|
||||
}
|
||||
|
||||
if (inputs.length > 0) {
|
||||
await fs.mkdir(CACHE_DIR, { recursive: true });
|
||||
await execFile('magick', [
|
||||
...inputs, '+append', '-background', 'none',
|
||||
'-resize', '600x300^', '-gravity', 'center', '-extent', '600x300', cachePath
|
||||
]);
|
||||
res.writeHead(200, { 'Content-Type': 'image/webp', 'Cache-Control': 'public, max-age=3600' });
|
||||
return res.end(await fs.readFile(cachePath));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[USER_HALL_IMAGE]', e);
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
<div class="posts" data-current-page="{{ pagination.current }}" data-has-more="{{ pagination.next ? 'true' : 'false' }}">
|
||||
@each(items as item)
|
||||
<a href="{{ link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.has_notification ? 'has-notif' : '' }} {{ item.is_pinned ? 'is-pinned' : '' }} {{ item.is_onara_active ? 'onara-active' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="@if(!is_anonymized){!! item.display_name || item.username !!}@else anonymous@endif" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp" data-size="{{ enable_dynamic_thumbs ? (item.thumb_size || 1) : 1 }}">
|
||||
@if(item.is_audio && !item.has_coverart)
|
||||
<div class="sidebar-media-placeholder audio">
|
||||
<i class="fa-solid fa-music"></i>
|
||||
</div>
|
||||
@endif
|
||||
<div class="thumb-indicators">
|
||||
@if(item.is_pinned)
|
||||
<i class="fa-solid fa-thumbtack pin-indicator anim"></i>
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
@each(items as item)
|
||||
<a href="{{ link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.has_notification ? 'has-notif' : '' }} {{ item.is_pinned ? 'is-pinned' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="@if(!is_anonymized){!! item.display_name || item.username !!}@else anonymous@endif" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').replace('x-zip-compressed', 'zip').replace('x-rar-compressed', 'rar').replace('vnd.rar', 'rar').replace('x-7z-compressed', '7z').replace('x-tar', 'tar').replace('x-bzip2', 'bz2').replace('x-xz', 'xz').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp" data-size="{{ enable_dynamic_thumbs ? (item.thumb_size || 1) : 1 }}">
|
||||
@if(item.is_audio && !item.has_coverart)
|
||||
<div class="sidebar-media-placeholder audio">
|
||||
<i class="fa-solid fa-music"></i>
|
||||
</div>
|
||||
@endif
|
||||
<div class="thumb-indicators">
|
||||
@if(item.is_pinned)
|
||||
<i class="fa-solid fa-thumbtack pin-indicator anim"></i>
|
||||
|
||||
@@ -124,6 +124,11 @@
|
||||
<div class="posts no-infinite-scroll">
|
||||
@each(f0cks.items as item)
|
||||
<a href="{{ f0cks.link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.is_pinned ? 'is-pinned' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="{!! item.display_name || item.username !!}" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp">
|
||||
@if(item.is_audio && !item.has_coverart)
|
||||
<div class="sidebar-media-placeholder audio">
|
||||
<i class="fa-solid fa-music"></i>
|
||||
</div>
|
||||
@endif
|
||||
<div class="thumb-indicators">
|
||||
@if(item.is_pinned)
|
||||
<i class="fa-solid fa-thumbtack pin-indicator anim"></i>
|
||||
@@ -157,6 +162,11 @@
|
||||
<div class="posts no-infinite-scroll">
|
||||
@each(favs.items as item)
|
||||
<a href="{{ favs.link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.is_pinned ? 'is-pinned' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="{!! item.display_name || item.username !!}" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp">
|
||||
@if(item.is_audio && !item.has_coverart)
|
||||
<div class="sidebar-media-placeholder audio">
|
||||
<i class="fa-solid fa-music"></i>
|
||||
</div>
|
||||
@endif
|
||||
<div class="thumb-indicators">
|
||||
@if(item.is_pinned)
|
||||
<i class="fa-solid fa-thumbtack pin-indicator anim"></i>
|
||||
|
||||
Reference in New Issue
Block a user