// -- 全局常量与状态 -- const ITEMS_PER_PAGE = 10; const platformResults = new Map(); // -- DOM 元素获取 -- const searchForm = document.getElementById("searchForm"); const resultsDiv = document.getElementById("results"); const errorDiv = document.getElementById("error"); const progressBar = document.getElementById("progressBar"); const searchBtn = document.getElementById("searchBtn"); const searchBtnText = document.getElementById("searchBtnText"); const searchIcon = searchBtn?.querySelector("i"); const customApiInput = document.getElementById("customApi"); let siteNavigationDiv; let toggleNavButton; let navLinksContainer; let isNavCollapsed = false; // Track navigation state for mobile (now largely ignored for mobile) let isMobileView = false; // Track if we are in mobile view let siteNavOriginalTop = 0; // Store the original top position of the navigation bar (less relevant for fixed bottom) let scrollbarWidth = 0; // Store calculated scrollbar width // Scroll to top functionality const scrollToTopBtn = document.getElementById("scrollToTopBtn"); const scrollToCommentsBtn = document.getElementById("scrollToCommentsBtn"); // Get the comments button window.addEventListener("scroll", () => { if (window.scrollY > 200) { // Show buttons after scrolling down 200px scrollToTopBtn.classList.add("flex"); scrollToTopBtn.classList.remove("hidden"); scrollToCommentsBtn.classList.add("flex"); // Show comments button scrollToCommentsBtn.classList.remove("hidden"); } else { scrollToTopBtn.classList.add("hidden"); scrollToTopBtn.classList.remove("flex"); scrollToCommentsBtn.classList.add("hidden"); // Hide comments button scrollToCommentsBtn.classList.remove("flex"); } }); // Initialize button visibility on page load (in case user refreshes scrolled down) // This also handles the case where the user might load the page already scrolled document.addEventListener("DOMContentLoaded", () => { if (window.scrollY > 200) { scrollToTopBtn.classList.add("flex"); scrollToTopBtn.classList.remove("hidden"); scrollToCommentsBtn.classList.add("flex"); scrollToCommentsBtn.classList.remove("hidden"); } else { scrollToTopBtn.classList.add("hidden"); scrollToTopBtn.classList.remove("flex"); scrollToCommentsBtn.classList.add("hidden"); scrollToCommentsBtn.classList.remove("flex"); } }); scrollToTopBtn.addEventListener("click", () => { window.scrollTo({ top: 0, behavior: "smooth", }); }); // Smooth scroll to comments scrollToCommentsBtn.addEventListener("click", (e) => { e.preventDefault(); // Prevent default anchor jump const commentsSection = document.getElementById("Comments"); if (commentsSection) { commentsSection.scrollIntoView({ behavior: "smooth", }); } }); /** * 页面加载后初始化 */ window.addEventListener("DOMContentLoaded", () => { if (searchBtn) searchBtn.disabled = false; const magicCheckbox = document.getElementById("magicAccess"); if (magicCheckbox) { magicCheckbox.checked = true; } quicklink.listen({ priority: true }); Artalk.init({ el: "#Comments", pageKey: "https://searchgal.homes", // Original domain from user's file server: "https://artalk.saop.cc", site: "Galgame 聚合搜索", }); siteNavigationDiv = document.createElement("div"); siteNavigationDiv.id = "siteNavigation"; // Initial classes are minimal, updateNavigationLayout will set full classes siteNavigationDiv.className = "z-20 flex flex-col items-center animate__animated animate__fadeInUp animate__faster"; document.body.appendChild(siteNavigationDiv); navLinksContainer = document.createElement("div"); // Changed to fixed height, horizontal scroll, no wrap // Increased horizontal padding (px-2 to px-4) to prevent focus ring clipping navLinksContainer.className = "nav-links-container flex flex-nowrap overflow-x-auto gap-2 items-center w-full h-12 px-4"; siteNavigationDiv.appendChild(navLinksContainer); toggleNavButton = document.createElement("button"); toggleNavButton.id = "toggleNavButton"; // Always hidden as mobile collapse is removed toggleNavButton.className = "hidden"; // Always hidden toggleNavButton.innerHTML = ''; siteNavigationDiv.appendChild(toggleNavButton); siteNavigationDiv.addEventListener("click", (e) => { const targetLink = e.target.closest('a[href^="#"]'); if (targetLink) { e.preventDefault(); const targetId = targetLink.getAttribute("href").substring(1); const targetElement = document.getElementById(targetId); if (targetElement) { const viewportHeight = window.innerHeight; const scrollOffset = viewportHeight * 0.25; const elementPosition = targetElement.getBoundingClientRect().top + window.scrollY; window.scrollTo({ top: elementPosition - scrollOffset, behavior: "smooth", }); } } }); // Calculate scrollbar width once scrollbarWidth = getScrollbarWidth(); // Initial layout update and event listeners updateNavigationLayout(); updateSiteNavigation(); // Call to set initial visibility adjustBodyPaddingForScrollbar(); // Adjust padding on initial load if (searchForm) { searchForm.addEventListener("submit", handleSearchSubmit); } if (resultsDiv) { resultsDiv.addEventListener("click", handlePaginationClick); } window.addEventListener( "resize", debounce(() => { scrollbarWidth = getScrollbarWidth(); // Recalculate scrollbar width on resize adjustBodyPaddingForScrollbar(); // Adjust padding after resize updateNavigationLayout(); // Update layout (fixed/absolute, bottom/top) updateSiteNavigationWidth(); // Update width }, 200) ); window.addEventListener("scroll", debounce(handleScroll, 10)); }); /** * Calculates the width of the scrollbar. * @returns {number} The width of the scrollbar in pixels. */ function getScrollbarWidth() { // Create a temporary div to measure scrollbar width const outer = document.createElement("div"); outer.style.visibility = "hidden"; outer.style.overflow = "scroll"; // Force scrollbar outer.style.msOverflowStyle = "scrollbar"; // For IE11 document.body.appendChild(outer); const inner = document.createElement("div"); outer.appendChild(inner); const scrollbarWidth = outer.offsetWidth - inner.offsetWidth; outer.parentNode.removeChild(outer); return scrollbarWidth; } /** * Adjusts body padding to prevent content shifting when scrollbar appears/disappears. */ function adjustBodyPaddingForScrollbar() { if (isMobileView) { // Do not adjust for mobile, as scrollbars are often overlaid document.body.style.paddingRight = ""; return; } const hasScrollbar = document.body.scrollHeight > window.innerHeight; const currentPaddingRight = parseInt(getComputedStyle(document.body).paddingRight, 10) || 0; if (hasScrollbar && currentPaddingRight === 0) { document.body.style.paddingRight = `${scrollbarWidth}px`; } else if (!hasScrollbar && currentPaddingRight === scrollbarWidth) { document.body.style.paddingRight = ""; } } /** * 处理页面滚动事件 (不再用于桌面导航栏固定,仅用于滚动条调整) */ function handleScroll() { adjustBodyPaddingForScrollbar(); } /** * 根据屏幕宽度调整导航栏布局 (fixed bottom) */ function updateNavigationLayout() { if (!siteNavigationDiv || !resultsDiv) return; const breakpoint = 768; // Tailwind's 'md' breakpoint isMobileView = window.innerWidth < breakpoint; // Clear all existing classes to ensure a clean slate siteNavigationDiv.className = ""; // Apply base classes for fixed bottom, centered, full width, padding siteNavigationDiv.classList.add( "z-20", "flex", "flex-col", "items-center", "animate__animated", "animate__fadeInUp", "animate__faster", "fixed", "bottom-0", // Stays at the very bottom "w-full", // Full width "px-2" // Consistent padding - changed from 'p-2' to 'px-2' to avoid top/bottom padding affecting height calculation for resultsDiv margin ); // Apply responsive max-width and rounding if (isMobileView) { siteNavigationDiv.classList.add("rounded-t-xl", "max-w-md"); } else { // For desktop, remove max-width and make it truly full viewport width siteNavigationDiv.classList.remove("max-w-4xl"); // Remove previous max-width siteNavigationDiv.classList.add("rounded-t-xl"); // Keep rounded top for desktop } // Ensure toggle button is hidden and nav links are always visible toggleNavButton.classList.add("hidden"); navLinksContainer.classList.remove("hidden", "animate__fadeIn"); // Update resultsDiv margin to account for fixed bottom navigation updateSiteNavigationWidth(); siteNavOriginalTop = siteNavigationDiv.getBoundingClientRect().top + window.scrollY; // Still update, though less critical adjustBodyPaddingForScrollbar(); } /** * 统一处理搜索表单提交 * @param {Event} e 事件对象 */ async function handleSearchSubmit(e) { e.preventDefault(); platformResults.clear(); clearUI(); resultsDiv.classList.add( "animate__animated", "animate__fadeOut", "animate__faster" ); resultsDiv.addEventListener( "animationend", function handler() { this.classList.remove( "animate__animated", "animate__fadeOut", "animate__faster" ); this.removeEventListener("animationend", handler); const formData = new FormData(searchForm); const gameName = formData.get("game").trim(); const zypassword = formData.get("zypassword").trim(); const searchMode = formData.get("searchMode"); const magic = document.getElementById("magicAccess")?.checked || false; const customApi = customApiInput ? customApiInput.value.trim() : ""; if (!gameName) { showError("游戏名称不能为空"); setLoadingState(false); return; } setLoadingState(true); const searchParams = { gameName, zypassword, magic, patchMode: searchMode === "patch", customApi, }; let totalTasks = 0; searchGameStream(searchParams, { onTotal: (total) => { totalTasks = total; }, onProgress: (progress) => { if (progressBar && totalTasks > 0) { const percent = Math.min( 100, Math.round((progress.completed / totalTasks) * 100) ); progressBar.style.width = `${percent}%`; } if (searchBtnText) { searchBtnText.textContent = `进度: ${progress.completed} / ${totalTasks}`; } }, onResult: (result) => { platformResults.set(result.name, { ...result, items: result.items || [], currentPage: 1, }); const platformCard = createPlatformCard(result, true); resultsDiv.appendChild(platformCard); updateSiteNavigation(); // Update navigation visibility and content }, onDone: () => { if (searchBtnText) searchBtnText.textContent = "搜索完成!"; setTimeout(() => setLoadingState(false), 1200); updateNavigationLayout(); siteNavOriginalTop = siteNavigationDiv.getBoundingClientRect().top + window.scrollY; handleScroll(); adjustBodyPaddingForScrollbar(); // Adjust padding after content is rendered }, onError: (err) => { showError(err.message); setLoadingState(false); }, }).catch((err) => { showError(err.message || "发生未知错误"); setLoadingState(false); }); }, { once: true } ); } /** * 处理分页按钮点击(事件委托) * @param {Event} e 点击事件 */ function handlePaginationClick(e) { const button = e.target.closest(".prev-page-btn, .next-page-btn"); if (!button || button.disabled) return; const platformName = button.dataset.platform; const platformData = platformResults.get(platformName); if (!platformData) { console.error(`错误:找不到平台 "${platformName}" 的数据。`); return; } const isNext = button.classList.contains("next-page-btn"); const totalPages = Math.ceil(platformData.items.length / ITEMS_PER_PAGE); let newPage = platformData.currentPage + (isNext ? 1 : -1); if (newPage < 1 || newPage > totalPages) return; platformData.currentPage = newPage; platformResults.set(platformName, platformData); const oldCard = resultsDiv.querySelector( `div[data-platform="${platformName}"]` ); if (oldCard) { const newCard = createPlatformCard(platformData, false); oldCard.replaceWith(newCard); newCard.classList.add( "animate__animated", "animate__fadeIn", "animate__faster" ); newCard.addEventListener( "animationend", function handler() { this.classList.remove( "animate__animated", "animate__fadeIn", "animate__faster" ); this.removeEventListener("animationend", handler); }, { once: true } ); } adjustBodyPaddingForScrollbar(); // Adjust padding after pagination } /** * 动态设置导航栏宽度和桌面端定位 */ function updateSiteNavigationWidth() { if (!siteNavigationDiv) return; const firstCard = resultsDiv.querySelector("[data-platform]"); if (firstCard) { // The width is now primarily controlled by Tailwind's 'w-full' and removed 'max-w-*' for desktop siteNavigationDiv.style.width = ""; // Ensure no inline width overrides Tailwind siteNavigationDiv.style.left = ""; // Ensure no inline left overrides Tailwind centering siteNavigationDiv.style.transform = ""; // Ensure no inline transform overrides Tailwind centering // Set marginBottom for resultsDiv as nav is at the bottom resultsDiv.style.marginBottom = `${siteNavigationDiv.offsetHeight}px`; // Removed +24 resultsDiv.style.marginTop = ""; // Clear any lingering top margin } else { siteNavigationDiv.style.width = ""; siteNavigationDiv.style.left = ""; siteNavigationDiv.style.top = ""; siteNavigationDiv.classList.add("hidden"); resultsDiv.style.marginTop = ""; resultsDiv.style.marginBottom = ""; // Clear margin if nav is hidden } // siteNavOriginalTop is less relevant now as nav is always fixed at bottom } /** * 更新导航链接并控制可见性 */ function updateSiteNavigation() { if (!siteNavigationDiv || !navLinksContainer || !toggleNavButton) return; navLinksContainer.innerHTML = ""; const colorMap = { lime: { text: "text-lime-800", bg: "bg-lime-200", hoverBg: "hover:bg-lime-300", }, white: { text: "text-gray-800", bg: "bg-gray-200", hoverBg: "hover:bg-gray-300", }, gold: { text: "text-yellow-800", bg: "bg-yellow-200", hoverBg: "hover:bg-yellow-300", }, red: { text: "text-red-800", bg: "bg-red-200", hoverBg: "hover:bg-red-300", }, default: { text: "text-indigo-800", bg: "bg-indigo-200", hoverBg: "hover:bg-indigo-300", }, }; const sortedPlatformNames = Array.from(platformResults.keys()).sort(); // Unified visibility control for siteNavigationDiv if (sortedPlatformNames.length === 0) { siteNavigationDiv.classList.add("hidden"); toggleNavButton.classList.add("hidden"); isNavCollapsed = false; // Reset collapse state resultsDiv.style.marginBottom = ""; // Reset margin if nav hidden console.log( "updateSiteNavigation: No results, hiding nav. platformResults.size:", platformResults.size ); return; } else { siteNavigationDiv.classList.remove("hidden"); siteNavigationDiv.classList.add("animate__fadeInUp"); // Re-add entrance animation siteNavigationDiv.classList.remove("animate__fadeOutDown"); // Ensure fadeOut is removed console.log( "updateSiteNavigation: Results present, showing nav. platformResults.size:", platformResults.size ); } sortedPlatformNames.forEach((name) => { const platform = platformResults.get(name); const link = document.createElement("a"); link.href = `#${name}`; const colorKey = platform.color && colorMap[platform.color] ? platform.color : "default"; const colorClasses = colorMap[colorKey]; link.className = `text-sm px-3 py-1 rounded-full transition-all duration-200 ease-in-out whitespace-nowrap ${colorClasses.text} ${colorClasses.bg} ${colorClasses.hoverBg} hover:scale-105 active:scale-95 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-400`; link.textContent = platform.name; navLinksContainer.appendChild(link); }); updateNavigationLayout(); } /** * 根据平台结果数据创建 HTML 卡片元素 * @param {object} result - 单个平台的结果数据 * @param {boolean} withAnimation - 是否应用入场动画 * @returns {HTMLElement} */ function createPlatformCard(result, withAnimation = true) { const currentPage = result.currentPage || 1; const colorMap = { lime: { bg: "bg-lime-100", text: "text-lime-700", icon: "text-lime-400", border: "border-lime-200", }, white: { bg: "bg-gray-50", text: "text-gray-500", icon: "text-gray-300", border: "border-gray-100", }, gold: { bg: "bg-yellow-100", text: "text-yellow-700", icon: "text-yellow-400", border: "border-yellow-200", }, red: { bg: "bg-red-100", text: "text-red-700", icon: "text-red-400", border: "border-red-200", }, default: { bg: "bg-gradient-to-br from-indigo-100 via-white to-pink-50", text: "text-indigo-700", icon: "text-indigo-400", border: "border-gray-100", }, }; const colorKey = result.color && colorMap[result.color] ? result.color : "default"; const color = colorMap[colorKey]; let home = "", domain = ""; if (result.items && result.items.length > 0) { try { const url = new URL(item.url); home = url.origin; domain = url.hostname; } catch {} } const tags = { gold: '需要魔法', lime: '无需登录', red: '错误', white: '需要登录', default: '综合', }; const tag = tags[colorKey] || ""; let itemsHtml = ""; if (result.items && result.items.length > 0) { const start = (currentPage - 1) * ITEMS_PER_PAGE; const end = start + ITEMS_PER_PAGE; const paginatedItems = result.items.slice(start, end); itemsHtml = `