Files
f0bm/public/s/js/f0ck.js
2026-01-24 13:37:13 +01:00

931 lines
32 KiB
JavaScript

window.requestAnimFrame = (function () {
return window.requestAnimationFrame
|| window.webkitRequestAnimationFrame
|| window.mozRequestAnimationFrame
|| function (callback) { window.setTimeout(callback, 1000 / 60); };
})();
(() => {
let video;
// User & Visitor dropdown toggle
const userToggle = document.getElementById('nav-user-toggle');
const userMenu = document.getElementById('nav-user-menu');
const visitorToggle = document.getElementById('nav-visitor-toggle');
const visitorMenu = document.getElementById('nav-visitor-menu');
if (userToggle && userMenu) {
userToggle.addEventListener('click', (e) => {
e.stopPropagation();
userMenu.classList.toggle('show');
});
}
if (visitorToggle && visitorMenu) {
visitorToggle.addEventListener('click', (e) => {
e.stopPropagation();
visitorMenu.classList.toggle('show');
});
}
document.addEventListener('click', (e) => {
if (userMenu && !userMenu.contains(e.target) && userToggle && !userToggle.contains(e.target)) {
userMenu.classList.remove('show');
}
if (visitorMenu && !visitorMenu.contains(e.target) && visitorToggle && !visitorToggle.contains(e.target)) {
visitorMenu.classList.remove('show');
}
});
// Login Modal Logic
const loginBtn = document.getElementById('nav-login-btn');
const loginModal = document.getElementById('login-modal');
const loginClose = document.getElementById('login-modal-close');
if (loginBtn && loginModal) {
loginBtn.addEventListener('click', (e) => {
e.preventDefault();
loginModal.style.display = 'flex';
// Close dropdown
if (visitorMenu) visitorMenu.classList.remove('show');
});
if (loginClose) {
loginClose.addEventListener('click', () => {
loginModal.style.display = 'none';
});
}
loginModal.addEventListener('click', (e) => {
if (e.target === loginModal) {
loginModal.style.display = 'none';
}
});
// Handle ESC key to close
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && loginModal.style.display === 'flex') {
loginModal.style.display = 'none';
}
});
}
// Initialize background preference
if (localStorage.getItem('background') == undefined) {
localStorage.setItem('background', 'true');
}
var background = localStorage.getItem('background') === 'true';
// Apply initial visual state
var initialCanvas = document.getElementById('bg');
if (initialCanvas) {
if (background) {
initialCanvas.classList.add('fader-in');
initialCanvas.classList.remove('fader-out');
} else {
initialCanvas.classList.add('fader-out');
initialCanvas.classList.remove('fader-in');
}
}
if (elem = document.querySelector("#my-video")) {
video = new v0ck(elem);
/* Listener moved to global keybindings
document.addEventListener("keydown", e => {
if (e.key === " " && e.target.tagName !== "INPUT" && e.target.tagName !== "TEXTAREA") {
video[video.paused ? 'play' : 'pause']();
document.querySelector('.v0ck_overlay').classList[video.paused ? 'remove' : 'add']('v0ck_hidden');
}
});
*/
if (elem !== null) {
// ... existing code ...
}
}
// Export init function for dynamic calls
window.initBackground = () => {
// Re-fetch elements as they might have been replaced
const elem = document.querySelector("#my-video");
const canvas = document.getElementById('bg');
if (elem) {
// Initialize video wrapper if needed or just get instance
// Assuming v0ck handles re-init or we just use raw element for events
// But video variable is local.
// We need to re-bind 'play' event if it's a new element.
if (canvas) {
// Restore visual state on re-init
if (background) {
canvas.classList.add('fader-in');
canvas.classList.remove('fader-out');
} else {
canvas.classList.add('fader-out');
canvas.classList.remove('fader-in');
}
const context = canvas.getContext('2d');
const cw = canvas.width = canvas.clientWidth | 0;
const ch = canvas.height = canvas.clientHeight | 0;
const animationLoop = () => {
if (elem.paused || elem.ended || !background)
return;
context.drawImage(elem, 0, 0, cw, ch);
window.requestAnimFrame(animationLoop);
}
elem.addEventListener('play', animationLoop);
if (!elem.paused) {
animationLoop();
}
}
}
};
// Initial call
window.initBackground();
const loadPageAjax = async (url) => {
// Show loading indicator
const navbar = document.querySelector("nav.navbar");
if (navbar) navbar.classList.add("pbwork");
try {
// Extract page number, user, tag, etc.
let page = 1;
const pMatch = url.match(/\/p\/(\d+)/);
if (pMatch) page = pMatch[1];
// Extract context
let tag = null, user = null, mime = null;
const tagMatch = url.match(/\/tag\/([^/]+)/);
if (tagMatch) tag = decodeURIComponent(tagMatch[1]);
const userMatch = url.match(/\/user\/([^/]+)/);
if (userMatch) user = decodeURIComponent(userMatch[1]);
const mimeMatch = url.match(/\/(image|audio|video)/);
if (mimeMatch) mime = mimeMatch[1];
let ajaxUrl = `/ajax/items/?page=${page}`;
if (tag) ajaxUrl += `&tag=${encodeURIComponent(tag)}`;
if (user) ajaxUrl += `&user=${encodeURIComponent(user)}`;
if (mime) ajaxUrl += `&mime=${encodeURIComponent(mime)}`;
console.log("Fetching Page:", ajaxUrl);
const response = await fetch(ajaxUrl, { credentials: 'include' });
const data = await response.json();
if (data.success) {
// Replace grid content
// If "infinite scroll" we might append, but pagination implies jumping properly?
// User said "resembled in pagination", which implies staying in sync.
// If I click Next Page, I expect to SEE page 2.
// But infinite scroll usually appends.
// Let's implement REPLACE for explicit page navigation to be safe/standard.
// Wait, the "infinite scroll" feature usually implies APPEND.
// If the user wants infinite scroll, they shouldn't click pagination?
// But if they scroll, `changePage` is called which clicks `.next`.
// So if I replace content, it breaks infinite scroll flow (items disappear).
// So I should APPEND if it's "next page" and we are already on the page?
// But `changePage` is triggered by scroll.
// Let's APPEND.
const posts = document.querySelector('.posts');
if (posts) {
// Check if we are appending (next page) or jumping
// For simple "infinite scroll", we append.
posts.insertAdjacentHTML('beforeend', data.html);
}
// Update pagination
if (data.pagination) {
document.querySelectorAll('.pagination-wrapper').forEach(el => el.innerHTML = data.pagination);
}
// Update History
history.pushState({}, '', url);
}
} catch (err) {
console.error(err);
window.location.href = url; // Fallback
} finally {
if (navbar) navbar.classList.remove("pbwork");
// Restore pagination visibility for Grid View
const navPag = document.querySelector('.pagination-container-fluid');
if (navPag) navPag.style.display = '';
}
};
let tt = false;
const stimeout = 500;
const setupMedia = () => {
if (elem = document.querySelector("#my-video")) {
video = new v0ck(elem);
}
};
// Navbar scroll effect - make background black when scrolling
const navbar = document.querySelector('.navbar');
if (navbar) {
window.addEventListener('scroll', () => {
if (window.scrollY > 50) {
navbar.classList.add('scrolled');
} else {
navbar.classList.remove('scrolled');
}
});
}
const loadItemAjax = async (url, inheritContext = true) => {
console.log("loadItemAjax called with:", url, "inheritContext:", inheritContext);
// Show loading indicator
const navbar = document.querySelector("nav.navbar");
if (navbar) navbar.classList.add("pbwork");
// Extract item ID from URL. Use the last numeric segment to avoid matching context IDs (like tag/1/...)
// Split path, filter numeric, pop last.
const pathSegments = new URL(url, window.location.origin).pathname.split('/');
const numericSegments = pathSegments.filter(s => /^\d+$/.test(s));
// Hide navbar pagination for Item View (matches SSR)
const navPag = document.querySelector('.pagination-container-fluid');
if (navPag) navPag.style.display = 'none';
if (numericSegments.length === 0) {
console.warn("loadItemAjax: No ID match found in URL", url);
// fallback for weird/external links
window.location.href = url;
return;
}
const itemid = numericSegments.pop();
// <context-preservation>
// Extract context from Target URL first
let tag = null, user = null, isFavs = false;
const tagMatch = url.match(/\/tag\/([^/]+)/);
if (tagMatch) tag = decodeURIComponent(tagMatch[1]);
const userMatch = url.match(/\/user\/([^/]+)/);
if (userMatch) {
user = decodeURIComponent(userMatch[1]);
if (url.match(/\/user\/[^/]+\/favs(\/|$|\?)/)) isFavs = true;
}
// If missing and inheritContext is true, check Window Location
if (inheritContext) {
if (!tag) {
const wTagMatch = window.location.href.match(/\/tag\/([^/]+)/);
if (wTagMatch) tag = decodeURIComponent(wTagMatch[1]);
}
if (!user) {
const wUserMatch = window.location.href.match(/\/user\/([^/]+)/);
if (wUserMatch) {
user = decodeURIComponent(wUserMatch[1]);
// Check for /favs (with or without trailing /, item id, or query params)
if (window.location.href.match(/\/user\/[^/]+\/favs(\/|$|\?)/)) isFavs = true;
}
}
}
// </context-preservation>
try {
// Construct AJAX URL
let ajaxUrl = `/ajax/item/${itemid}`;
const params = new URLSearchParams();
if (tag) params.append('tag', tag);
if (user) params.append('user', user);
if (isFavs) params.append('fav', 'true');
if ([...params].length > 0) {
ajaxUrl += '?' + params.toString();
}
console.log("Fetching:", ajaxUrl);
const response = await fetch(ajaxUrl, { credentials: 'include' });
if (!response.ok) throw new Error(`Network response was not ok: ${response.status}`);
const rawText = await response.text();
let html, paginationHtml;
try {
// Optimistically try to parse as JSON first
const data = JSON.parse(rawText);
if (data && typeof data.html === 'string') {
html = data.html;
paginationHtml = data.pagination;
} else {
html = rawText;
}
} catch (e) {
// If JSON parse fails, assume it's HTML text
html = rawText;
}
let container = document.querySelector('#main .container');
if (!container && document.querySelector('.index-container')) {
// Transition from Index to Item View
const main = document.getElementById('main');
main.innerHTML = '<div class="container"></div>';
container = main.querySelector('.container');
} else if (!container && document.getElementById('main')) {
// Transition from User Profile or other pages without .container
const main = document.getElementById('main');
main.innerHTML = '<div class="container"></div>';
container = main.querySelector('.container');
} else if (container) {
// Check if we are on Tags Overview logic (which reuses .container)
const tagsOverview = container.querySelector('.tags');
if (tagsOverview) {
container.innerHTML = '';
} else {
// Already in Item View, clear usage
const oldContent = container.querySelector('.content');
const oldMetadata = container.querySelector('.metadata');
const oldHeader = container.querySelector('._204863');
if (oldHeader) oldHeader.remove();
if (oldContent) oldContent.remove();
if (oldMetadata) oldMetadata.remove();
}
}
container.insertAdjacentHTML('beforeend', html);
// Update pagination if present
if (paginationHtml) {
const pagWrappers = document.querySelectorAll('.pagination-wrapper');
pagWrappers.forEach(el => el.innerHTML = paginationHtml);
}
// Construct proper History URL (Context Aware)
// If we inherited context, we should reflect it in the URL
let pushUrl = `/${itemid}`;
// Logic from ajax.mjs context reconstruction:
if (user) {
pushUrl = `/user/${encodeURIComponent(user)}/${itemid}`;
if (isFavs) pushUrl = `/user/${encodeURIComponent(user)}/favs/${itemid}`;
}
else if (tag) pushUrl = `/tag/${encodeURIComponent(tag)}/${itemid}`;
// We overwrite proper URL even if the link clicked was "naked"
history.pushState({}, '', pushUrl);
setupMedia();
if (window.initBackground) window.initBackground();
// Try to extract ID from response if possible or just use itemid
document.title = `f0bm - ${itemid}`;
if (navbar) navbar.classList.remove("pbwork");
console.log("AJAX load complete");
} catch (err) {
console.error("AJAX load failed:", err);
}
};
const changePage = (e, pbwork = true) => {
if (pbwork) {
const nav = document.querySelector("nav.navbar");
if (nav) nav.classList.add("pbwork");
}
// Trigger native click for navigation
e.click();
};
// Intercept clicks
document.addEventListener('click', (e) => {
// Check for thumbnail links on index page
const thumbnail = e.target.closest('.posts > a');
if (thumbnail && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) {
e.preventDefault();
// Thumbnails inherit context (e.g. from Tag Index)
loadItemAjax(thumbnail.href, true);
return;
}
const link = e.target.closest('#next, #prev, #random, #nav-random, .id-link, .nav-next, .nav-prev');
if (link && link.href && link.hostname === window.location.hostname && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) {
// Special check for random
if (link.id === 'random' || link.id === 'nav-random') {
e.preventDefault();
const nav = document.querySelector("nav.navbar");
if (nav) nav.classList.add("pbwork");
// Extract current context from window location
let randomUrl = '/api/v2/random';
const params = new URLSearchParams();
const wTagMatch = window.location.href.match(/\/tag\/([^/]+)/);
if (wTagMatch) params.append('tag', decodeURIComponent(wTagMatch[1]));
const wUserMatch = window.location.href.match(/\/user\/([^/]+)/);
if (wUserMatch) {
params.append('user', decodeURIComponent(wUserMatch[1]));
if (window.location.href.match(/\/favs(\/|$|\?)/)) {
params.append('fav', 'true');
}
}
if ([...params].length > 0) {
randomUrl += '?' + params.toString();
}
fetch(randomUrl)
.then(r => r.json())
.then(data => {
if (data.success && data.items && data.items.id) {
// Inherit context so URL matches current filter
loadItemAjax(`/${data.items.id}`, true);
} else {
window.location.href = link.href;
}
})
.catch((err) => {
console.error("Random fetch failed:", err);
window.location.href = link.href;
});
return;
}
// Standard item links
e.preventDefault();
if (link.href.match(/\/p\/\d+/) || link.href.match(/[?&]page=\d+/)) {
loadPageAjax(link.href);
} else {
loadItemAjax(link.href, true);
}
} else if (e.target.closest('#togglebg')) {
e.preventDefault();
background = !background;
localStorage.setItem('background', background.toString());
var canvas = document.getElementById('bg');
if (canvas) {
if (background) {
canvas.classList.remove('fader-out');
canvas.classList.add('fader-in');
// Re-trigger loop if started completely fresh or paused
if (video && !video.paused) {
// We need to access animationLoop from closure?
// Accessing it via window.initBackground might be cleaner or just restart it.
// But initBackground defines it locally.
// We can just rely on initBackground being called or canvas update.
// Actually, if we just change opacity, the loop doesn't need to stop/start technically,
// but for performance we stopped it if !background.
// So we should restart it.
window.initBackground();
}
} else {
canvas.classList.remove('fader-in');
canvas.classList.add('fader-out');
}
}
} else if (e.target.closest('.removetag')) {
e.preventDefault();
const removeBtn = e.target.closest('.removetag');
const tagLink = removeBtn.previousElementSibling;
if (tagLink) {
const tagName = tagLink.textContent.trim();
const idLink = document.querySelector('.id-link');
const id = idLink ? idLink.textContent.trim() : null;
if (id && tagName) {
const modal = document.getElementById('delete-tag-modal');
const nameEl = document.getElementById('delete-tag-name');
const confirmBtn = document.getElementById('delete-tag-confirm');
const cancelBtn = document.getElementById('delete-tag-cancel');
if (modal) {
nameEl.textContent = tagName;
modal.style.display = 'flex';
const closeModal = () => {
modal.style.display = 'none';
confirmBtn.onclick = null;
cancelBtn.onclick = null;
};
cancelBtn.onclick = closeModal;
confirmBtn.onclick = () => {
confirmBtn.textContent = 'Deleting...';
confirmBtn.disabled = true;
fetch(`/api/v2/admin/${id}/tags/${encodeURIComponent(tagName)}`, {
method: 'DELETE'
})
.then(r => r.json())
.then(data => {
if (data.success) {
removeBtn.parentElement.remove();
closeModal();
} else {
alert('Error: ' + (data.msg || 'Unknown error'));
confirmBtn.textContent = 'Delete';
confirmBtn.disabled = false;
}
})
.catch(err => {
console.error(err);
alert('Failed to delete tag');
confirmBtn.textContent = 'Delete';
confirmBtn.disabled = false;
});
};
}
}
}
}
});
window.addEventListener('popstate', (e) => {
if (window.location.href.match(/\/p\/\d+/) || window.location.href.match(/[?&]page=\d+/) || window.location.pathname === '/') {
loadPageAjax(window.location.href);
} else {
loadItemAjax(window.location.href, true);
}
});
// <keybindings>
const clickOnElementBinding = selector => () => (elem = document.querySelector(selector)) ? elem.click() : null;
const keybindings = {
"ArrowLeft": clickOnElementBinding("#next"),
"a": clickOnElementBinding("#next"),
"ArrowRight": clickOnElementBinding("#prev"),
"d": clickOnElementBinding("#prev"),
"r": clickOnElementBinding("#random, #nav-random"),
"l": () => {
const toggle = document.querySelector("#togglebg");
if (toggle) toggle.click();
},
" ": () => {
if (video && typeof video.play === 'function') { // Check if video wrapper exists/is valid
video[video.paused ? 'play' : 'pause']();
const overlay = document.querySelector('.v0ck_overlay');
if (overlay) overlay.classList[video.paused ? 'remove' : 'add']('v0ck_hidden');
} else {
const img = document.querySelector("#f0ck-image");
if (img) img.click();
}
}
};
document.addEventListener("keydown", e => {
if (e.key in keybindings && e.target.tagName !== "INPUT" && e.target.tagName !== "TEXTAREA") {
if (e.shiftKey || e.ctrlKey || e.metaKey || e.altKey)
return;
e.preventDefault();
keybindings[e.key]();
}
});
// </keybindings>
// <image-responsive>
const imgSize = e => new Promise((res, _) => {
const i = new Image();
i.addEventListener('load', function () {
res({ width: this.width, height: this.height });
});
i.src = e.src;
});
// <wheeler>
const wheelEventListener = function (event) {
if (event.target.closest('.media-object, .steuerung')) {
if (event.deltaY < 0) {
const el = document.getElementById('next');
if (el && el.href && !el.href.endsWith('#')) el.click();
} else if (event.deltaY > 0) {
const el = document.getElementById('prev');
if (el && el.href && !el.href.endsWith('#')) el.click();
}
}
};
window.addEventListener('wheel', wheelEventListener);
// </wheeler>
if (f0ckimage = document.querySelector("img#f0ck-image")) {
const f0ckimagescroll = document.querySelector("#image-scroll");
let isImageExpanded = false;
console.log("entry point - image unclicked")
f0ckimage.addEventListener("click", async e => {
e.preventDefault();
const img = await imgSize(f0ckimage);
console.log("img clicked");
if (isImageExpanded) {
isImageExpanded = false;
f0ckimagescroll.removeAttribute("style");
f0ckimage.removeAttribute("style");
console.log("image is not expanded")
window.addEventListener('wheel', wheelEventListener);
} else {
if (img.width > img.height) return;
isImageExpanded = true;
window.removeEventListener('wheel', wheelEventListener);
f0ckimagescroll.setAttribute("style", "overflow-y: scroll");
f0ckimage.setAttribute("style", "max-height: none; height: auto; width: 100%; position: absolute; left: 0; border: var(--img-border-width) solid var(--img-border-color); border-top: none; border-bottom: none;");
}
});
}
// </image-responsive>
// <infinite-scroll>
const postsContainer = document.querySelector("div.posts");
if (postsContainer) {
// Infinite scroll state
let infiniteState = {
loading: false,
hasMore: true,
currentPage: 1
};
// Extract current page from URL
const pageMatch = window.location.pathname.match(/\/p\/(\d+)/);
if (pageMatch) infiniteState.currentPage = parseInt(pageMatch[1]);
// Extract context (tag/user/mime) from URL
const getContext = () => {
const ctx = {};
const tagMatch = window.location.pathname.match(/\/tag\/([^/]+)/);
if (tagMatch) ctx.tag = decodeURIComponent(tagMatch[1]);
const userMatch = window.location.pathname.match(/\/user\/([^/]+)/);
if (userMatch) ctx.user = decodeURIComponent(userMatch[1]);
const mimeMatch = window.location.pathname.match(/\/(image|audio|video)(?:\/|$)/);
if (mimeMatch) ctx.mime = mimeMatch[1];
return ctx;
};
// Build URL path for history
const buildUrl = (page) => {
const ctx = getContext();
let path = '/';
if (ctx.tag) path += `tag/${ctx.tag}/`;
if (ctx.user) path += `user/${ctx.user}/`;
if (ctx.mime) path += `${ctx.mime}/`;
if (page > 1) path += `p/${page}`;
return path.replace(/\/$/, '') || '/';
};
// Fetch and append more items
const loadMoreItems = async () => {
if (infiniteState.loading || !infiniteState.hasMore) return;
infiniteState.loading = true;
const foot = document.querySelector("div#footbar");
if (foot) {
foot.innerHTML = '<span class="loading-spinner">Loading...</span>';
foot.style.color = 'var(--footbar-color)';
}
const nextPage = infiniteState.currentPage + 1;
const ctx = getContext();
const params = new URLSearchParams();
params.append('page', nextPage);
if (ctx.tag) params.append('tag', ctx.tag);
if (ctx.user) params.append('user', ctx.user);
if (ctx.mime) params.append('mime', ctx.mime);
try {
const response = await fetch(`/ajax/items?${params.toString()}`);
const data = await response.json();
if (data.success && data.html) {
// Append new thumbnails
postsContainer.insertAdjacentHTML('beforeend', data.html);
// Update state
infiniteState.currentPage = data.currentPage;
infiniteState.hasMore = data.hasMore;
// Update URL
history.replaceState({}, '', buildUrl(infiniteState.currentPage));
// Update pagination display if exists
const paginationLinks = document.querySelectorAll('.pagination .pagination-int-item, .pagination .btn');
paginationLinks.forEach(el => {
if (el.textContent.trim() == infiniteState.currentPage) {
el.classList.add('disabled');
}
});
} else {
infiniteState.hasMore = false;
}
} catch (err) {
console.error('Infinite scroll fetch error:', err);
} finally {
infiniteState.loading = false;
if (foot) {
foot.innerHTML = infiniteState.hasMore ? '&#9660;' : '&#8212;';
foot.style.color = 'transparent';
}
}
};
// Scroll detection - preload before reaching bottom
const PRELOAD_OFFSET = 500; // pixels before bottom to trigger load
window.addEventListener("scroll", () => {
if (!document.querySelector('#main')) return;
const scrollPosition = window.innerHeight + window.scrollY;
const pageHeight = document.querySelector('#main').offsetHeight;
const distanceFromBottom = pageHeight - scrollPosition;
// Load more when within PRELOAD_OFFSET pixels of bottom
if (distanceFromBottom < PRELOAD_OFFSET && infiniteState.hasMore && !infiniteState.loading) {
loadMoreItems();
}
});
}
// </infinite-scroll>
// <visualizer>
if (audioElement = document.querySelector("audio")) {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = 1920;
canvas.height = 1080;
setTimeout(() => {
document.querySelector(".v0ck").insertAdjacentElement("afterbegin", canvas);
}, 400);
const audioCtx = new AudioContext();
const analyser = audioCtx.createAnalyser();
analyser.fftSize = 2048;
const source = audioCtx.createMediaElementSource(audioElement);
source.connect(analyser);
source.connect(audioCtx.destination);
let data = new Uint8Array(analyser.frequencyBinCount);
requestAnimationFrame(loopingFunction);
function loopingFunction() {
requestAnimationFrame(loopingFunction);
analyser.getByteFrequencyData(data);
draw(data);
}
function draw(data) {
data = [...data];
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = getComputedStyle(document.body).getPropertyValue("--accent");
data.forEach((value, i) => {
const percent = value / 256;
const height = (canvas.height * percent / 2) - 40;
const offset = canvas.height - height - 1;
const barWidth = canvas.width / analyser.frequencyBinCount;
ctx.fillRect(i * barWidth, offset, barWidth, height);
});
}
audioElement.onplay = () => {
audioCtx.resume();
};
}
// </visualizer>
// <mediakeys>
if (elem = document.querySelector("#my-video") && "mediaSession" in navigator) {
const playpauseEvent = () => {
video[video.paused ? 'play' : 'pause']();
document.querySelector('.v0ck_overlay').classList[video.paused ? 'remove' : 'add']('v0ck_hidden');
};
navigator.mediaSession.setActionHandler('play', playpauseEvent);
navigator.mediaSession.setActionHandler('pause', playpauseEvent);
navigator.mediaSession.setActionHandler('stop', playpauseEvent);
navigator.mediaSession.setActionHandler('previoustrack', () => {
if (link = document.querySelector(".pagination > .prev:not(.disabled)"))
changePage(link);
});
navigator.mediaSession.setActionHandler('nexttrack', () => {
if (link = document.querySelector(".pagination > .next:not(.disabled)"))
changePage(link);
});
}
// </mediakeys>
// <scroller>
// <search-overlay>
const initSearch = () => {
if (!document.getElementById('search-overlay')) {
const overlay = document.createElement('div');
overlay.id = 'search-overlay';
overlay.innerHTML = `
<div id="search-close">&times;</div>
<input type="text" id="search-input" placeholder="Search Tags..." autocomplete="off">
`;
document.body.appendChild(overlay);
const input = document.getElementById('search-input');
const close = document.getElementById('search-close');
const btns = document.querySelectorAll('#nav-search-btn, #nav-search-btn-guest');
const toggleSearch = (show) => {
if (show) {
overlay.style.display = 'flex';
// Force reflow
overlay.offsetHeight;
overlay.classList.add('visible');
input.focus();
} else {
overlay.classList.remove('visible');
setTimeout(() => {
overlay.style.display = 'none';
}, 200);
}
};
btns.forEach(btn => btn.addEventListener('click', (e) => {
e.preventDefault();
toggleSearch(true);
}));
close.addEventListener('click', () => toggleSearch(false));
// Close on click outside (background)
overlay.addEventListener('click', (e) => {
if (e.target === overlay) toggleSearch(false);
});
// ESC to close
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && overlay.classList.contains('visible')) {
toggleSearch(false);
}
// "k" to open
if (e.key === 'k' && e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA' && !overlay.classList.contains('visible')) {
e.preventDefault();
toggleSearch(true);
}
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
const val = input.value.trim();
if (val) {
window.location.href = `/tag/${encodeURIComponent(val)}`;
}
}
});
}
};
initSearch();
// </search-overlay>
// </scroller>
})();
// disable default scroll event when mouse is on content div
// this is useful for items that have a lot of tags for example: 12536
const targetSelector = '.content';
let isMouseOver = true;
function isPageScrollable() {
return document.documentElement.scrollHeight > document.documentElement.clientHeight;
}
function onWheel(e) {
if (isMouseOver && isPageScrollable()) {
e.preventDefault();
}
}
function init() {
const el = document.querySelector(targetSelector);
if (!el) return;
el.addEventListener('mouseenter', () => isMouseOver = true);
el.addEventListener('mouseleave', () => isMouseOver = false);
window.addEventListener('wheel', onWheel, { passive: false });
}
window.addEventListener('load', init);
const sbtForm = document.getElementById('sbtForm');
if (sbtForm) {
sbtForm.addEventListener('submit', (e) => {
e.preventDefault();
const input = document.getElementById('sbtInput').value.trim();
if (input) {
window.location.href = `/tag/${encodeURIComponent(input)}`;
}
});
}