mirror of
https://github.com/Moe-Sakura/frontend.git
synced 2026-03-19 05:39:45 +08:00
feat: 集成 VNDB 显示游戏详情,优化搜索体验
* 集成 VNDB,展示游戏封面、简介和外部链接。 * 根据 VNDB 匹配度,高亮显示搜索结果中的最佳匹配项。 * 新增“锁定视图”功能,可隐藏界面仅显示背景。 * 动态展示前端与后端项目的最新版本号。 * 优化搜索冷却计时器显示,提供更清晰的冷却时间。 * 新增样式文件,支持 VNDB 界面和动画效果。 * 移除 Content-Security-Policy 元标签,修复局域网访问时自动提升至HTTPS造成的问题
This commit is contained in:
581
index.html
581
index.html
@@ -3,10 +3,6 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="upgrade-insecure-requests; block-all-mixed-content"
|
||||
/>
|
||||
<title>SearchGal - Galgame 聚合搜索</title>
|
||||
<style>
|
||||
@import "tailwindcss";
|
||||
@@ -24,6 +20,8 @@
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="src/style.css" />
|
||||
|
||||
<meta
|
||||
name="description"
|
||||
content="SearchGal 是一个聚合搜索 Galgame 的网站,支持多平台搜索"
|
||||
@@ -75,309 +73,325 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main class="flex-1 flex flex-col items-center justify-center min-h-screen">
|
||||
<div
|
||||
class="container mx-auto max-w-4xl bg-white/95 rounded-[8px] shadow-xl px-0 py-0 mt-0 z-10 relative flex flex-col transition-all duration-300"
|
||||
>
|
||||
<div
|
||||
class="relative px-0 pt-0 pb-0 border-b border-gray-100 overflow-hidden"
|
||||
>
|
||||
<img
|
||||
src="https://www.loliapi.com/acg/pc/"
|
||||
alt="随图"
|
||||
class="w-full object-contain object-center transition-all duration-300 hover:scale-105 hover:brightness-95 draggable"
|
||||
draggable="true"
|
||||
/>
|
||||
<div id="background-layer"></div>
|
||||
<main class="flex-1 flex flex-col min-h-screen">
|
||||
<div id="content-wrapper" class="flex justify-center w-full max-w-7xl mx-auto gap-8">
|
||||
<div id="vndb-info-panel" class="hidden md:block w-0 max-w-md p-0 flex-shrink-0 opacity-0">
|
||||
<div class="relative">
|
||||
<div id="ext-links-container" class="absolute top-2 -left-14 flex flex-col gap-2"></div>
|
||||
<img id="vndb-image" src="" alt="Game Art" class="rounded-lg shadow-lg w-full h-auto object-contain mb-4">
|
||||
</div>
|
||||
<h2 id="vndb-title" class="text-4xl font-bold text-white text-center drop-shadow-lg mb-10"></h2>
|
||||
<div id="vndb-description" class="text-base text-gray-200 max-h-80 overflow-y-auto pr-2"></div>
|
||||
</div>
|
||||
<div
|
||||
class="px-12 pt-6 pb-5 bg-gradient-to-r from-indigo-50 via-pink-50 to-yellow-50 text-center flex items-center justify-center min-h-[56px] transition-all duration-300"
|
||||
>
|
||||
<h1
|
||||
class="text-2xl md:text-3xl font-extrabold text-indigo-700 drop-shadow-lg inline-flex items-center gap-2 md:gap-3 whitespace-nowrap"
|
||||
>
|
||||
<i class="fas fa-gamepad text-pink-400"></i>
|
||||
Galgame 聚合搜索
|
||||
</h1>
|
||||
</div>
|
||||
<form
|
||||
id="searchForm"
|
||||
class="flex flex-col gap-5 px-6 pt-6 pb-4 relative"
|
||||
>
|
||||
<div id="scrollable-content" class="w-full max-w-4xl flex-shrink-0">
|
||||
<div
|
||||
class="flex flex-col md:flex-row gap-2 md:gap-0 items-center w-full rounded-[8px]"
|
||||
id="main-container"
|
||||
class="container mx-auto w-full bg-white/95 rounded-[8px] shadow-xl px-0 py-0 mt-0 z-10 relative flex flex-col transition-all duration-300"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col md:flex-row w-full items-stretch md:items-center gap-0"
|
||||
class="relative px-0 pt-0 pb-0 border-b border-gray-100 overflow-hidden"
|
||||
>
|
||||
<div class="relative flex-1 min-w-0 flex items-center">
|
||||
<img
|
||||
src="https://www.loliapi.com/acg/pc/"
|
||||
alt="随图"
|
||||
class="w-full object-contain object-center transition-all duration-300 hover:scale-105 hover:brightness-95 draggable"
|
||||
draggable="true"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="px-12 pt-6 pb-5 bg-gradient-to-r from-indigo-50 via-pink-50 to-yellow-50 text-center flex items-center justify-center min-h-[56px] transition-all duration-300"
|
||||
>
|
||||
<h1
|
||||
class="text-2xl md:text-3xl font-extrabold text-indigo-700 drop-shadow-lg inline-flex items-center gap-2 md:gap-3 whitespace-nowrap"
|
||||
>
|
||||
<i class="fas fa-gamepad text-pink-400"></i>
|
||||
Galgame 聚合搜索
|
||||
</h1>
|
||||
</div>
|
||||
<form
|
||||
id="searchForm"
|
||||
class="flex flex-col gap-5 px-6 pt-6 pb-4 relative"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col md:flex-row gap-2 md:gap-0 items-center w-full rounded-[8px]"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col md:flex-row w-full items-stretch md:items-center gap-0"
|
||||
>
|
||||
<div class="relative flex-1 min-w-0 flex items-center">
|
||||
<span
|
||||
class="absolute left-3 top-1/2 -translate-y-1/2 text-indigo-400 pointer-events-none text-lg z-10"
|
||||
><i class="fas fa-keyboard"></i
|
||||
></span>
|
||||
<input
|
||||
type="text"
|
||||
id="game"
|
||||
name="game"
|
||||
required
|
||||
placeholder="游戏或补丁关键字词"
|
||||
class="pl-10 pr-4 h-11 border border-gray-300 border-b-0 md:border-b md:border-r-0 rounded-t-[8px] md:rounded-l-[8px] md:rounded-tr-none md:rounded-bl-[8px] md:rounded-br-none focus:outline-none focus:ring-2 focus:ring-indigo-400 bg-gray-50 text-base shadow-sm transition w-full md:w-auto placeholder-gray-400 min-w-0 flex-1 rounded-b-none"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="relative flex items-center w-full md:max-w-[280px] md:w-[280px] mt-0 md:mt-0"
|
||||
>
|
||||
<span
|
||||
class="absolute left-3 top-1/2 -translate-y-1/2 text-pink-400 pointer-events-none text-base z-10"
|
||||
><i class="fas fa-lock"></i
|
||||
></span>
|
||||
<input
|
||||
type="password"
|
||||
id="zypassword"
|
||||
name="zypassword"
|
||||
placeholder="紫缘 Gal 密码(可选)"
|
||||
class="pl-10 pr-16 h-11 border border-t-0 border-gray-300 md:border-l-0 md:border-t border-b-0 md:border-b rounded-b-[8px] md:rounded-none md:rounded-br-[8px] md:rounded-tr-[8px] focus:outline-none focus:ring-2 focus:ring-pink-300 bg-gray-50 text-sm shadow-sm transition w-full min-w-0 md:w-auto md:flex-shrink md:flex-grow md:basis-1/3 placeholder-gray-400 rounded-t-none md:rounded-t-none"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<a
|
||||
href="https://galzy.eu.org/"
|
||||
target="_blank"
|
||||
class="absolute right-1.5 top-1/2 -translate-y-1/2 text-xs text-pink-600 bg-pink-50 hover:bg-pink-100 hover:underline rounded-[6px] px-1.5 py-0.5 transition whitespace-nowrap shadow-sm border border-pink-200 h-[26px] flex items-center leading-[1.6] z-10"
|
||||
>点我获取密码</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative flex-1 min-w-0 mt-2">
|
||||
<span
|
||||
class="absolute left-3 top-1/2 -translate-y-1/2 text-indigo-400 pointer-events-none text-lg z-10"
|
||||
><i class="fas fa-keyboard"></i
|
||||
></span>
|
||||
class="absolute left-3 top-1/2 -translate-y-1/2 text-purple-400 pointer-events-none text-base z-10"
|
||||
>
|
||||
<i class="fas fa-code"></i>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
id="game"
|
||||
name="game"
|
||||
required
|
||||
placeholder="游戏或补丁关键字词"
|
||||
class="pl-10 pr-4 h-11 border border-gray-300 border-b-0 md:border-b md:border-r-0 rounded-t-[8px] md:rounded-l-[8px] md:rounded-tr-none md:rounded-bl-[8px] md:rounded-br-none focus:outline-none focus:ring-2 focus:ring-indigo-400 bg-gray-50 text-base shadow-sm transition w-full md:w-auto placeholder-gray-400 min-w-0 flex-1 rounded-b-none"
|
||||
id="customApi"
|
||||
name="customApi"
|
||||
placeholder="自定义 API 地址(可选)"
|
||||
class="pl-10 pr-4 sm:pr-48 h-9 border border-gray-200 rounded-[6px] focus:outline-none focus:ring-1 focus:ring-purple-400 bg-gray-50 text-sm shadow-xs transition w-full placeholder-gray-400"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
aria-describedby="apiHint"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="relative flex items-center w-full md:max-w-[280px] md:w-[280px] mt-0 md:mt-0"
|
||||
>
|
||||
<span
|
||||
class="absolute left-3 top-1/2 -translate-y-1/2 text-pink-400 pointer-events-none text-base z-10"
|
||||
><i class="fas fa-lock"></i
|
||||
></span>
|
||||
<input
|
||||
type="password"
|
||||
id="zypassword"
|
||||
name="zypassword"
|
||||
placeholder="紫缘 Gal 密码(可选)"
|
||||
class="pl-10 pr-16 h-11 border border-t-0 border-gray-300 md:border-l-0 md:border-t border-b-0 md:border-b rounded-b-[8px] md:rounded-none md:rounded-br-[8px] md:rounded-tr-[8px] focus:outline-none focus:ring-2 focus:ring-pink-300 bg-gray-50 text-sm shadow-sm transition w-full min-w-0 md:w-auto md:flex-shrink md:flex-grow md:basis-1/3 placeholder-gray-400 rounded-t-none md:rounded-t-none"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<a
|
||||
href="https://galzy.eu.org/"
|
||||
target="_blank"
|
||||
class="absolute right-1.5 top-1/2 -translate-y-1/2 text-xs text-pink-600 bg-pink-50 hover:bg-pink-100 hover:underline rounded-[6px] px-1.5 py-0.5 transition whitespace-nowrap shadow-sm border border-pink-200 h-[26px] flex items-center leading-[1.6] z-10"
|
||||
>点我获取密码</a
|
||||
id="apiHint"
|
||||
class="hidden sm:block absolute right-3 top-1/2 -translate-y-1/2 text-[11px] text-gray-500 pointer-events-none z-10 transition-opacity duration-200"
|
||||
data-hint-desktop
|
||||
>
|
||||
例如: https://api.searchgal.homes 或 http://127.0.0.1:8898
|
||||
</span>
|
||||
<span
|
||||
id="apiHintMobile"
|
||||
class="sm:hidden absolute left-0 bottom-[-18px] text-[11px] text-gray-500 w-full text-center leading-none transition-opacity duration-200"
|
||||
data-hint-mobile
|
||||
>
|
||||
例如: https://api.searchgal.homes 或 http://127.0.0.1:8898
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative flex-1 min-w-0 mt-2">
|
||||
<span
|
||||
class="absolute left-3 top-1/2 -translate-y-1/2 text-purple-400 pointer-events-none text-base z-10"
|
||||
>
|
||||
<i class="fas fa-code"></i>
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
id="customApi"
|
||||
name="customApi"
|
||||
placeholder="自定义 API 地址(可选)"
|
||||
class="pl-10 pr-4 sm:pr-48 h-9 border border-gray-200 rounded-[6px] focus:outline-none focus:ring-1 focus:ring-purple-400 bg-gray-50 text-sm shadow-xs transition w-full placeholder-gray-400"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
aria-describedby="apiHint"
|
||||
/>
|
||||
<span
|
||||
id="apiHint"
|
||||
class="hidden sm:block absolute right-3 top-1/2 -translate-y-1/2 text-[11px] text-gray-500 pointer-events-none z-10 transition-opacity duration-200"
|
||||
data-hint-desktop
|
||||
>
|
||||
例如: https://api.searchgal.homes 或 http://127.0.0.1:8898
|
||||
</span>
|
||||
<span
|
||||
id="apiHintMobile"
|
||||
class="sm:hidden absolute left-0 bottom-[-18px] text-[11px] text-gray-500 w-full text-center leading-none transition-opacity duration-200"
|
||||
data-hint-mobile
|
||||
>
|
||||
例如: https://api.searchgal.homes 或 http://127.0.0.1:8898
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 w-full mt-4">
|
||||
<button
|
||||
type="submit"
|
||||
id="searchBtn"
|
||||
class="py-3 px-6 bg-indigo-600 hover:bg-indigo-700 active:bg-indigo-800 text-white font-bold transition-all flex items-center justify-center gap-3 shadow-md relative overflow-hidden disabled:bg-indigo-300 disabled:opacity-80 rounded-[8px] text-lg border-0 focus:ring-2 focus:ring-indigo-300"
|
||||
disabled
|
||||
>
|
||||
<span
|
||||
id="progressBar"
|
||||
class="absolute left-0 top-0 h-full bg-pink-400/80 transition-all duration-300 z-0 opacity-0 w-0 rounded-[8px]"
|
||||
></span>
|
||||
<span class="relative z-10 flex items-center gap-2">
|
||||
<i class="fas fa-search"></i>
|
||||
<span id="searchBtnText">开始搜索</span>
|
||||
</span>
|
||||
</button>
|
||||
<div class="flex flex-col gap-2 w-full mt-4">
|
||||
<button
|
||||
type="submit"
|
||||
id="searchBtn"
|
||||
class="py-3 px-6 bg-indigo-600 hover:bg-indigo-700 active:bg-indigo-800 text-white font-bold transition-all flex items-center justify-center gap-3 shadow-md relative overflow-hidden disabled:bg-indigo-300 disabled:opacity-80 rounded-[8px] text-lg border-0 focus:ring-2 focus:ring-indigo-300"
|
||||
disabled
|
||||
>
|
||||
<span
|
||||
id="progressBar"
|
||||
class="absolute left-0 top-0 h-full bg-pink-400/80 transition-all duration-300 z-0 opacity-0 w-0 rounded-[8px]"
|
||||
></span>
|
||||
<span class="relative z-10 flex items-center gap-2">
|
||||
<i class="fas fa-search"></i>
|
||||
<span id="searchBtnText">开始搜索</span>
|
||||
</span>
|
||||
</button>
|
||||
<div
|
||||
class="flex flex-row flex-wrap items-center gap-0 w-full justify-center select-none mt-2"
|
||||
>
|
||||
<div>
|
||||
<input
|
||||
type="radio"
|
||||
id="gameMode"
|
||||
name="searchMode"
|
||||
value="game"
|
||||
class="hidden peer"
|
||||
checked
|
||||
/>
|
||||
<label
|
||||
for="gameMode"
|
||||
class="select-none cursor-pointer px-4 py-2 font-semibold text-xs flex items-center gap-1 transition-all duration-200 bg-white peer-checked:bg-indigo-600 peer-checked:text-white text-indigo-700 hover:bg-indigo-300 animate__animated animate__pulse animate__faster peer-checked:animate__tada rounded-l-[8px] border-0 shadow-sm"
|
||||
>
|
||||
<i class="fas fa-gamepad"></i>游戏</label
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="radio"
|
||||
id="patchMode"
|
||||
name="searchMode"
|
||||
value="patch"
|
||||
class="hidden peer"
|
||||
/>
|
||||
<label
|
||||
for="patchMode"
|
||||
class="select-none cursor-pointer px-4 py-2 font-semibold text-xs flex items-center gap-1 transition-all duration-200 bg-white peer-checked:bg-pink-500 peer-checked:text-white text-pink-700 hover:bg-pink-300 animate__animated animate__pulse animate__faster peer-checked:animate__tada border-l border-gray-100 rounded-none shadow-sm"
|
||||
><i class="fas fa-wrench"></i>补丁</label
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="magicAccess"
|
||||
name="magicAccess"
|
||||
class="hidden peer"
|
||||
/>
|
||||
<label
|
||||
for="magicAccess"
|
||||
class="select-none cursor-pointer px-4 py-2 font-semibold text-xs flex items-center gap-1 transition-all duration-200 bg-white peer-checked:bg-yellow-400 peer-checked:text-white text-yellow-700 hover:bg-yellow-300 animate__animated animate__pulse animate__faster peer-checked:animate__tada border-l border-gray-100 rounded-r-[8px] shadow-sm"
|
||||
><i class="fas fa-hat-wizard"></i>魔搜</label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="error text-red-600 font-semibold px-12" id="error"></div>
|
||||
<div class="results mt-2 px-6" id="results"></div>
|
||||
<section class="w-full max-w-4xl mx-auto mt-8">
|
||||
<div class="bg-white/95 rounded-[8px] p-6 text-gray-700">
|
||||
<h2
|
||||
class="text-xl font-bold text-indigo-700 mb-4 inline-flex items-center gap-2"
|
||||
>
|
||||
<i class="fas fa-info-circle text-blue-500"></i> 咱家的使用须知
|
||||
</h2>
|
||||
<ul class="list-disc list-inside space-y-2 text-sm">
|
||||
<li>
|
||||
首先,衷心感谢
|
||||
<a
|
||||
href="https://saop.cc/"
|
||||
target="_blank"
|
||||
class="font-semibold text-indigo-600 hover:underline"
|
||||
>@Asuna</a
|
||||
>
|
||||
大佬提供的服务器和技术支持!没有大佬的魔法,咱可跑不起来!
|
||||
</li>
|
||||
<li>
|
||||
本程序纯属<strong>爱发电</strong>,仅供绅士们交流学习使用,务必请大家<strong
|
||||
>支持正版 Galgame</strong
|
||||
>!入正不亏哦!
|
||||
</li>
|
||||
<li>
|
||||
本站只做互联网内容的<strong>聚合搬运工</strong>,搜索结果均来自第三方站点,下载前请各位自行判断<strong>资源安全性</strong>,以免翻车。
|
||||
</li>
|
||||
<li>
|
||||
如果想体验“魔法”搜索,记得启用<strong>“魔搜”</strong>,解锁更多神秘站点!
|
||||
</li>
|
||||
<li>
|
||||
搜索时请注意关键词长度!<strong>关键词太短</strong>可能搜不全(部分站点只显示首批结果),<strong>太长</strong>则可能无法精准匹配。建议尝试<strong>适当的关键词</strong>,效果更佳~
|
||||
</li>
|
||||
<li>
|
||||
本程序每次查询完毕即断开连接,<strong>严禁任何形式的爆破或恶意爬取</strong>,做个文明的绅士!
|
||||
</li>
|
||||
<li>
|
||||
万一某个站点搜索挂了,先看看自己的魔法是否到位,也可能是站点维护了,或者咱这边的<strong>爬虫失效</strong>了。
|
||||
</li>
|
||||
<li>
|
||||
关于站点标签:
|
||||
<span
|
||||
class="inline-block px-2 py-0.5 rounded-full bg-green-200 text-green-800 text-xs font-medium"
|
||||
>绿色</span
|
||||
>
|
||||
代表免登录<strong>直冲</strong>;
|
||||
<span
|
||||
class="inline-block px-2 py-0.5 rounded-full bg-yellow-200 text-yellow-800 text-xs font-medium"
|
||||
>金色</span
|
||||
>
|
||||
表示需要<strong>魔法加持</strong>才能访问;
|
||||
<span
|
||||
class="inline-block px-2 py-0.5 rounded-full bg-gray-200 text-gray-800 text-xs font-medium"
|
||||
>白色</span
|
||||
>
|
||||
通常意味着需要<strong>登录/回复</strong>等额外操作才能拿到资源。
|
||||
</li>
|
||||
<li>
|
||||
目前收录的站点多为 PC 平台资源,大部分站点提供 OneDrive
|
||||
或直链下载,速度上比某些国内盘要<strong>给力</strong>不少!
|
||||
</li>
|
||||
<li>
|
||||
为了支持各 Galgame
|
||||
站点能长久运营,还请各位把浏览器的<strong>广告屏蔽插件</strong>关掉,或将这些站点加入白名单。大家建站不易,小小的支持也是大大的动力!
|
||||
</li>
|
||||
<li>
|
||||
<span class="font-bold text-red-600"
|
||||
>郑重呼吁:请务必支持 Galgame 正版!让爱与梦想延续!</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
如果您觉得咱这小工具好用,请移步
|
||||
<a
|
||||
href="https://github.com/Moe-Sakura"
|
||||
target="_blank"
|
||||
class="text-blue-600 hover:underline font-semibold"
|
||||
>GitHub</a
|
||||
>
|
||||
给本项目点个免费的
|
||||
<strong>Star</strong>
|
||||
吧,秋梨膏!你的支持就是咱最大的动力,比心~
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
<div
|
||||
class="flex flex-row flex-wrap items-center gap-0 w-full justify-center select-none mt-2"
|
||||
class="footer text-xs text-gray-500 mt-4 text-center border-t border-gray-100 pt-4 px-12 pb-4 rounded-b-lg"
|
||||
>
|
||||
<div>
|
||||
<input
|
||||
type="radio"
|
||||
id="gameMode"
|
||||
name="searchMode"
|
||||
value="game"
|
||||
class="hidden peer"
|
||||
checked
|
||||
/>
|
||||
<label
|
||||
for="gameMode"
|
||||
class="select-none cursor-pointer px-4 py-2 font-semibold text-xs flex items-center gap-1 transition-all duration-200 bg-white peer-checked:bg-indigo-600 peer-checked:text-white text-indigo-700 hover:bg-indigo-300 animate__animated animate__pulse animate__faster peer-checked:animate__tada rounded-l-[8px] border-0 shadow-sm"
|
||||
<p>
|
||||
<span class="inline-flex items-center gap-1 mr-2"
|
||||
><i class="fas fa-eye"></i>访问:<span id="busuanzi_value_site_pv"
|
||||
>null</span
|
||||
></span
|
||||
>
|
||||
<i class="fas fa-gamepad"></i>游戏</label
|
||||
<span class="inline-flex items-center gap-1"
|
||||
><i class="fas fa-user"></i>访客:<span
|
||||
id="busuanzi_value_site_uv"
|
||||
>null</span
|
||||
></span
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="radio"
|
||||
id="patchMode"
|
||||
name="searchMode"
|
||||
value="patch"
|
||||
class="hidden peer"
|
||||
/>
|
||||
<label
|
||||
for="patchMode"
|
||||
class="select-none cursor-pointer px-4 py-2 font-semibold text-xs flex items-center gap-1 transition-all duration-200 bg-white peer-checked:bg-pink-500 peer-checked:text-white text-pink-700 hover:bg-pink-300 animate__animated animate__pulse animate__faster peer-checked:animate__tada border-l border-gray-100 rounded-none shadow-sm"
|
||||
><i class="fas fa-wrench"></i>补丁</label
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="magicAccess"
|
||||
name="magicAccess"
|
||||
class="hidden peer"
|
||||
/>
|
||||
<label
|
||||
for="magicAccess"
|
||||
class="select-none cursor-pointer px-4 py-2 font-semibold text-xs flex items-center gap-1 transition-all duration-200 bg-white peer-checked:bg-yellow-400 peer-checked:text-white text-yellow-700 hover:bg-yellow-300 animate__animated animate__pulse animate__faster peer-checked:animate__tada border-l border-gray-100 rounded-r-[8px] shadow-sm"
|
||||
><i class="fas fa-hat-wizard"></i>魔搜</label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="error text-red-600 font-semibold px-12" id="error"></div>
|
||||
<div class="results mt-2 px-6" id="results"></div>
|
||||
<section class="w-full max-w-4xl mx-auto mt-8">
|
||||
<div class="bg-white/95 rounded-[8px] p-6 text-gray-700">
|
||||
<h2
|
||||
class="text-xl font-bold text-indigo-700 mb-4 inline-flex items-center gap-2"
|
||||
>
|
||||
<i class="fas fa-info-circle text-blue-500"></i> 咱家的使用须知
|
||||
</h2>
|
||||
<ul class="list-disc list-inside space-y-2 text-sm">
|
||||
<li>
|
||||
首先,衷心感谢
|
||||
<a
|
||||
href="https://saop.cc/"
|
||||
target="_blank"
|
||||
class="font-semibold text-indigo-600 hover:underline"
|
||||
>@Asuna</a
|
||||
>
|
||||
大佬提供的服务器和技术支持!没有大佬的魔法,咱可跑不起来!
|
||||
</li>
|
||||
<li>
|
||||
本程序纯属<strong>爱发电</strong>,仅供绅士们交流学习使用,务必请大家<strong
|
||||
>支持正版 Galgame</strong
|
||||
>!入正不亏哦!
|
||||
</li>
|
||||
<li>
|
||||
本站只做互联网内容的<strong>聚合搬运工</strong>,搜索结果均来自第三方站点,下载前请各位自行判断<strong>资源安全性</strong>,以免翻车。
|
||||
</li>
|
||||
<li>
|
||||
如果想体验“魔法”搜索,记得启用<strong>“魔搜”</strong>,解锁更多神秘站点!
|
||||
</li>
|
||||
<li>
|
||||
搜索时请注意关键词长度!<strong>关键词太短</strong>可能搜不全(部分站点只显示首批结果),<strong>太长</strong>则可能无法精准匹配。建议尝试<strong>适当的关键词</strong>,效果更佳~
|
||||
</li>
|
||||
<li>
|
||||
本程序每次查询完毕即断开连接,<strong>严禁任何形式的爆破或恶意爬取</strong>,做个文明的绅士!
|
||||
</li>
|
||||
<li>
|
||||
万一某个站点搜索挂了,先看看自己的魔法是否到位,也可能是站点维护了,或者咱这边的<strong>爬虫失效</strong>了。
|
||||
</li>
|
||||
<li>
|
||||
关于站点标签:
|
||||
</p>
|
||||
<div class="mt-3 inline-flex items-center gap-3 justify-center">
|
||||
<span
|
||||
class="inline-block px-2 py-0.5 rounded-full bg-green-200 text-green-800 text-xs font-medium"
|
||||
>绿色</span
|
||||
id="version-container"
|
||||
class="inline-flex items-center gap-1 text-xs text-gray-400 bg-gray-100 rounded-[8px] px-2 py-1 font-mono select-none"
|
||||
>
|
||||
代表免登录<strong>直冲</strong>;
|
||||
<span
|
||||
class="inline-block px-2 py-0.5 rounded-full bg-yellow-200 text-yellow-800 text-xs font-medium"
|
||||
>金色</span
|
||||
>
|
||||
表示需要<strong>魔法加持</strong>才能访问;
|
||||
<span
|
||||
class="inline-block px-2 py-0.5 rounded-full bg-gray-200 text-gray-800 text-xs font-medium"
|
||||
>白色</span
|
||||
>
|
||||
通常意味着需要<strong>登录/回复</strong>等额外操作才能拿到资源。
|
||||
</li>
|
||||
<li>
|
||||
目前收录的站点多为 PC 平台资源,大部分站点提供 OneDrive
|
||||
或直链下载,速度上比某些国内盘要<strong>给力</strong>不少!
|
||||
</li>
|
||||
<li>
|
||||
为了支持各 Galgame
|
||||
站点能长久运营,还请各位把浏览器的<strong>广告屏蔽插件</strong>关掉,或将这些站点加入白名单。大家建站不易,小小的支持也是大大的动力!
|
||||
</li>
|
||||
<li>
|
||||
<span class="font-bold text-red-600"
|
||||
>郑重呼吁:请务必支持 Galgame 正版!让爱与梦想延续!</span
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
如果您觉得咱这小工具好用,请移步
|
||||
<i class="fas fa-code-branch"></i>
|
||||
<a
|
||||
id="version-display"
|
||||
href="https://github.com/Moe-Sakura/SearchGal/blob/main/version.md"
|
||||
target="_blank"
|
||||
class="text-gray-600 hover:underline"
|
||||
>版本号加载中</a
|
||||
>
|
||||
</span>
|
||||
<a
|
||||
href="https://github.com/Moe-Sakura"
|
||||
target="_blank"
|
||||
class="text-blue-600 hover:underline font-semibold"
|
||||
>GitHub</a
|
||||
class="inline-flex items-center gap-1 text-black border border-black bg-white/80 rounded-[8px] px-3 py-1 shadow font-semibold no-underline text-sm transition-all duration-200 hover:text-white hover:bg-black hover:underline hover:underline-offset-2 hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-black/40"
|
||||
style="text-decoration: none"
|
||||
>
|
||||
给本项目点个免费的
|
||||
<strong>Star</strong>
|
||||
吧,秋梨膏!你的支持就是咱最大的动力,比心~
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
<div
|
||||
class="footer text-xs text-gray-500 mt-4 text-center border-t border-gray-100 pt-4 px-12 pb-4 rounded-b-lg"
|
||||
>
|
||||
<p>
|
||||
<span class="inline-flex items-center gap-1 mr-2"
|
||||
><i class="fas fa-eye"></i>访问:<span id="busuanzi_value_site_pv"
|
||||
>null</span
|
||||
></span
|
||||
>
|
||||
<span class="inline-flex items-center gap-1"
|
||||
><i class="fas fa-user"></i>访客:<span
|
||||
id="busuanzi_value_site_uv"
|
||||
>null</span
|
||||
></span
|
||||
>
|
||||
</p>
|
||||
<div class="mt-3 inline-flex items-center gap-3 justify-center">
|
||||
<span
|
||||
class="inline-flex items-center gap-1 text-xs text-gray-400 bg-gray-100 rounded-[8px] px-2 py-1 font-mono select-none"
|
||||
>
|
||||
<i class="fas fa-code-branch"></i>
|
||||
<a
|
||||
href="https://github.com/Moe-Sakura/SearchGal/blob/main/version.md"
|
||||
target="_blank"
|
||||
class="text-gray-600 hover:underline"
|
||||
>250714</a
|
||||
>
|
||||
</span>
|
||||
<a
|
||||
href="https://github.com/Moe-Sakura"
|
||||
target="_blank"
|
||||
class="inline-flex items-center gap-1 text-black border border-black bg-white/80 rounded-[8px] px-3 py-1 shadow font-semibold no-underline text-sm transition-all duration-200 hover:text-white hover:bg-black hover:underline hover:underline-offset-2 hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-black/40"
|
||||
style="text-decoration: none"
|
||||
>
|
||||
<i class="fab fa-github"></i>
|
||||
GitHub
|
||||
</a>
|
||||
<i class="fab fa-github"></i>
|
||||
GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<section class="w-full max-w-4xl mx-auto mt-8 mb-8">
|
||||
<div
|
||||
id="Comments"
|
||||
class="bg-white/80 rounded-lg shadow p-4 md:p-6 transition-all duration-300"
|
||||
></div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<section class="w-full max-w-4xl mx-auto mt-8 mb-8">
|
||||
<div
|
||||
id="Comments"
|
||||
class="bg-white/80 rounded-lg shadow p-4 md:p-6 transition-all duration-300"
|
||||
></div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div class="fixed bottom-16 right-3 flex flex-col space-y-3 z-50">
|
||||
@@ -396,6 +410,13 @@
|
||||
>
|
||||
<i class="fas fa-comments"></i>
|
||||
</a>
|
||||
<button
|
||||
id="lock-view-btn"
|
||||
class="bg-green-500 hover:bg-green-600 text-white p-3 rounded-full shadow-lg transition-all duration-300 transform hover:scale-110 focus:outline-none focus:ring-2 focus:ring-green-300 flex items-center justify-center"
|
||||
title="隐藏视图"
|
||||
>
|
||||
<i class="fas fa-lock"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<script
|
||||
|
||||
526
src/main.js
526
src/main.js
@@ -3,6 +3,10 @@ const ITEMS_PER_PAGE = 10;
|
||||
const platformResults = new Map();
|
||||
const SEARCH_COOLDOWN_MS = 30 * 1000; // 30 seconds cooldown
|
||||
let lastSearchTime = 0; // Timestamp of the last search submission
|
||||
let bgmBestMatches = []; // Array to store best matches from VNDB
|
||||
let vndbInfo = {}; // Object to store detailed info from VNDB
|
||||
let cooldownInterval = null; // Timer for cooldown countdown
|
||||
let isFirstSearch = true; // Track if it's the first search to control animations
|
||||
|
||||
// -- DOM 元素获取 --
|
||||
const searchForm = document.getElementById("searchForm");
|
||||
@@ -13,11 +17,19 @@ const searchBtn = document.getElementById("searchBtn");
|
||||
const searchBtnText = document.getElementById("searchBtnText");
|
||||
const searchIcon = searchBtn?.querySelector("i");
|
||||
const customApiInput = document.getElementById("customApi");
|
||||
const scrollableContent = document.getElementById("scrollable-content");
|
||||
const vndbInfoPanel = document.getElementById("vndb-info-panel");
|
||||
const vndbImage = document.getElementById("vndb-image");
|
||||
const vndbDescription = document.getElementById("vndb-description");
|
||||
const vndbTitle = document.getElementById("vndb-title");
|
||||
const backgroundLayer = document.getElementById("background-layer");
|
||||
let originalBackgroundImage = "";
|
||||
|
||||
let siteNavigationDiv;
|
||||
let toggleNavButton;
|
||||
let navLinksContainer;
|
||||
let isNavCollapsed = false; // Track navigation state for mobile (now largely ignored for mobile)
|
||||
let isNavManuallyHidden = false; // Track if user has manually hidden the navigation
|
||||
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
|
||||
@@ -25,6 +37,7 @@ 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
|
||||
const lockViewBtn = document.getElementById("lock-view-btn");
|
||||
|
||||
window.addEventListener("scroll", () => {
|
||||
if (window.scrollY > 200) {
|
||||
@@ -33,11 +46,15 @@ window.addEventListener("scroll", () => {
|
||||
scrollToTopBtn.classList.remove("hidden");
|
||||
scrollToCommentsBtn.classList.add("flex"); // Show comments button
|
||||
scrollToCommentsBtn.classList.remove("hidden");
|
||||
lockViewBtn.classList.add("flex");
|
||||
lockViewBtn.classList.remove("hidden");
|
||||
} else {
|
||||
scrollToTopBtn.classList.add("hidden");
|
||||
scrollToTopBtn.classList.remove("flex");
|
||||
scrollToCommentsBtn.classList.add("hidden"); // Hide comments button
|
||||
scrollToCommentsBtn.classList.remove("flex");
|
||||
lockViewBtn.classList.add("hidden");
|
||||
lockViewBtn.classList.remove("flex");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -49,11 +66,15 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
scrollToTopBtn.classList.remove("hidden");
|
||||
scrollToCommentsBtn.classList.add("flex");
|
||||
scrollToCommentsBtn.classList.remove("hidden");
|
||||
lockViewBtn.classList.add("flex");
|
||||
lockViewBtn.classList.remove("hidden");
|
||||
} else {
|
||||
scrollToTopBtn.classList.add("hidden");
|
||||
scrollToTopBtn.classList.remove("flex");
|
||||
scrollToCommentsBtn.classList.add("hidden");
|
||||
scrollToCommentsBtn.classList.remove("flex");
|
||||
lockViewBtn.classList.add("hidden");
|
||||
lockViewBtn.classList.remove("flex");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -75,10 +96,23 @@ scrollToCommentsBtn.addEventListener("click", (e) => {
|
||||
}
|
||||
});
|
||||
|
||||
lockViewBtn.addEventListener("click", () => {
|
||||
if (siteNavigationDiv) {
|
||||
isNavManuallyHidden = !isNavManuallyHidden; // Toggle the state
|
||||
siteNavigationDiv.classList.toggle("hidden", isNavManuallyHidden); // Apply state
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 页面加载后初始化
|
||||
*/
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
// Store the original background image
|
||||
// The original background is no longer set on load.
|
||||
if (backgroundLayer) {
|
||||
backgroundLayer.style.backgroundImage = 'none';
|
||||
}
|
||||
|
||||
// 从 URL 获取 API 参数并填充输入框
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const apiUrl = urlParams.get("api");
|
||||
@@ -168,6 +202,16 @@ window.addEventListener("DOMContentLoaded", () => {
|
||||
);
|
||||
|
||||
window.addEventListener("scroll", debounce(handleScroll, 10));
|
||||
|
||||
fetchAndDisplayVersion(); // Fetch and display version on page load
|
||||
|
||||
const lockViewBtn = document.getElementById('lock-view-btn');
|
||||
if (lockViewBtn) {
|
||||
lockViewBtn.addEventListener('click', () => {
|
||||
document.body.classList.toggle('locked-mode');
|
||||
lockViewBtn.blur();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -285,6 +329,24 @@ async function handleSearchSubmit(e) {
|
||||
}
|
||||
|
||||
platformResults.clear();
|
||||
bgmBestMatches = []; // Reset best matches on new search
|
||||
vndbInfo = {}; // Reset VNDB info
|
||||
|
||||
// Reset animation state if it's active
|
||||
if (document.body.classList.contains("vndb-mode")) {
|
||||
if (!isFirstSearch) {
|
||||
// On subsequent searches, hide the info panel instantly.
|
||||
// The background will fade out via the class removal below.
|
||||
if (vndbInfoPanel) vndbInfoPanel.classList.add("hidden");
|
||||
if (vndbTitle) vndbTitle.classList.add("hidden"); // Hide title instantly
|
||||
if (vndbDescription) vndbDescription.classList.add("hidden"); // Hide description instantly
|
||||
}
|
||||
document.body.classList.remove("vndb-mode");
|
||||
if (backgroundLayer) {
|
||||
backgroundLayer.style.backgroundImage = "none";
|
||||
}
|
||||
}
|
||||
|
||||
clearUI();
|
||||
|
||||
resultsDiv.classList.add(
|
||||
@@ -330,9 +392,63 @@ async function handleSearchSubmit(e) {
|
||||
|
||||
let totalTasks = 0;
|
||||
|
||||
// Start the main search stream. It will run in the background.
|
||||
searchGameStream(searchParams, {
|
||||
onTotal: (total) => {
|
||||
totalTasks = total;
|
||||
fetchVndbData(gameName).then(vndbResult => {
|
||||
console.log("[DEBUG] VNDB fetch completed. Processing result.");
|
||||
console.log("[DEBUG] Received from VNDB:", vndbResult);
|
||||
|
||||
if (vndbResult && vndbResult.names && vndbResult.names.length > 0) {
|
||||
bgmBestMatches = vndbResult.names;
|
||||
vndbInfo = {
|
||||
mainName: vndbResult.mainName,
|
||||
mainImageUrl: vndbResult.mainImageUrl,
|
||||
screenshotUrl: vndbResult.screenshotUrl,
|
||||
description: vndbResult.description,
|
||||
};
|
||||
// Now that we have the names, immediately re-highlight any existing cards.
|
||||
console.log("[DEBUG] Applying highlights based on VNDB names:", bgmBestMatches);
|
||||
highlightBestMatches();
|
||||
|
||||
// --- Fetch External Links ---
|
||||
if (vndbInfo.mainName) {
|
||||
fetchVndbExtLinks(vndbInfo.mainName);
|
||||
}
|
||||
|
||||
// --- Trigger Animation ---
|
||||
if (vndbInfo.mainImageUrl && vndbImage) {
|
||||
vndbImage.src = vndbInfo.mainImageUrl;
|
||||
}
|
||||
if (vndbInfo.description && vndbDescription) {
|
||||
vndbDescription.textContent = vndbInfo.description;
|
||||
vndbDescription.classList.remove("hidden"); // Make description visible again
|
||||
}
|
||||
if (vndbInfo.mainName && vndbTitle) {
|
||||
vndbTitle.textContent = vndbInfo.mainName;
|
||||
if (vndbTitle) vndbTitle.classList.remove("hidden"); // Make title visible again
|
||||
}
|
||||
if (vndbInfo.screenshotUrl && backgroundLayer) {
|
||||
// Preload the image before fading to it
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
backgroundLayer.style.backgroundImage = `url(${vndbInfo.screenshotUrl})`;
|
||||
};
|
||||
img.src = vndbInfo.screenshotUrl;
|
||||
}
|
||||
// Ensure panel is visible before class is added to trigger animation
|
||||
if (vndbInfoPanel) vndbInfoPanel.classList.remove("hidden");
|
||||
document.body.classList.add("vndb-mode");
|
||||
isFirstSearch = false; // Mark that the first search has happened
|
||||
|
||||
} else {
|
||||
console.log("[DEBUG] No exact match from VNDB or empty names list. Skipping highlight.");
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error("An error occurred during the VNDB fetch:", err);
|
||||
// Don't show error to user for this, as it's an enhancement
|
||||
});
|
||||
},
|
||||
onProgress: (progress) => {
|
||||
if (progressBar && totalTasks > 0) {
|
||||
@@ -347,12 +463,14 @@ async function handleSearchSubmit(e) {
|
||||
}
|
||||
},
|
||||
onResult: (result) => {
|
||||
platformResults.set(result.name, {
|
||||
const platformData = {
|
||||
...result,
|
||||
items: result.items || [],
|
||||
currentPage: 1,
|
||||
});
|
||||
const platformCard = createPlatformCard(result, true);
|
||||
};
|
||||
platformResults.set(result.name, platformData);
|
||||
// The card is created here. It will be highlighted if bgmBestMatches is already populated.
|
||||
const platformCard = createPlatformCard(platformData, true);
|
||||
resultsDiv.appendChild(platformCard);
|
||||
updateSiteNavigation(); // Update navigation visibility and content
|
||||
},
|
||||
@@ -370,9 +488,14 @@ async function handleSearchSubmit(e) {
|
||||
setLoadingState(false);
|
||||
},
|
||||
}).catch((err) => {
|
||||
showError(err.message || "发生未知错误");
|
||||
setLoadingState(false);
|
||||
// This catch is for the searchGameStream promise itself
|
||||
console.error("Error in searchGameStream:", err);
|
||||
if (searchBtn.disabled) {
|
||||
setLoadingState(false);
|
||||
showError(err.message || "流式搜索发生未知错误");
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
@@ -507,7 +630,9 @@ function updateSiteNavigation() {
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
siteNavigationDiv.classList.remove("hidden");
|
||||
if (!isNavManuallyHidden) {
|
||||
siteNavigationDiv.classList.remove("hidden");
|
||||
}
|
||||
siteNavigationDiv.classList.add("animate__fadeInUp"); // Re-add entrance animation
|
||||
siteNavigationDiv.classList.remove("animate__fadeOutDown"); // Ensure fadeOut is removed
|
||||
console.log(
|
||||
@@ -629,7 +754,12 @@ function createPlatformCard(result, withAnimation = true) {
|
||||
item.name === ".bzEmpty" || !item.name
|
||||
? "未知文件"
|
||||
: item.name;
|
||||
return `<li class="group transition hover:bg-indigo-50 flex flex-col px-5 py-3">
|
||||
|
||||
// Check if the current item's name is one of the best matches, ONLY on the first page.
|
||||
const isBestMatch = result.currentPage === 1 && bgmBestMatches.some(matchName => displayName.includes(matchName));
|
||||
const bestMatchClass = isBestMatch ? 'best-match-highlight' : '';
|
||||
|
||||
return `<li class="group transition hover:bg-indigo-50 flex flex-col px-5 py-3 ${bestMatchClass}">
|
||||
<a href="${item.url}" target="_blank" class="font-medium text-gray-800 group-hover:text-indigo-700 text-sm flex items-center gap-1" title="访问具体页面">
|
||||
<span class="flex-1 min-w-0 break-words">${displayName}</span>
|
||||
<i class="fas fa-arrow-up-right-from-square text-gray-300 group-hover:text-indigo-400 ml-1"></i>
|
||||
@@ -649,7 +779,7 @@ function createPlatformCard(result, withAnimation = true) {
|
||||
const prevDisabled = currentPage === 1;
|
||||
const nextDisabled = currentPage === totalPages;
|
||||
paginationHtml = `<div class="px-5 py-3 flex justify-center gap-2 items-center">
|
||||
<button class="prev-page-btn bg-indigo-500 text-white p-2 rounded-full hover:bg-indigo-600 focus:ring-2 focus:ring-indigo-300 transition-all duration-200 ease-in-out ${
|
||||
<button class="prev-page-btn bg-indigo-500 text-white w-8 h-8 rounded-full flex items-center justify-center hover:bg-indigo-600 focus:ring-2 focus:ring-indigo-300 transition-all duration-200 ease-in-out ${
|
||||
prevDisabled
|
||||
? "opacity-50 cursor-not-allowed"
|
||||
: "hover:scale-110 active:scale-90"
|
||||
@@ -659,7 +789,7 @@ function createPlatformCard(result, withAnimation = true) {
|
||||
<span class="text-sm font-semibold text-indigo-700 px-3 py-1 bg-indigo-100 rounded-full shadow-sm">
|
||||
${currentPage} / ${totalPages}
|
||||
</span>
|
||||
<button class="next-page-btn bg-indigo-500 text-white p-2 rounded-full hover:bg-indigo-600 focus:ring-2 focus:ring-indigo-300 transition-all duration-200 ease-in-out ${
|
||||
<button class="next-page-btn bg-indigo-500 text-white w-8 h-8 rounded-full flex items-center justify-center hover:bg-indigo-600 focus:ring-2 focus:ring-indigo-300 transition-all duration-200 ease-in-out ${
|
||||
nextDisabled
|
||||
? "opacity-50 cursor-not-allowed"
|
||||
: "hover:scale-110 active:scale-90"
|
||||
@@ -705,12 +835,45 @@ function createPlatformCard(result, withAnimation = true) {
|
||||
return cardElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlights search result items that are considered "best matches".
|
||||
*/
|
||||
function highlightBestMatches() {
|
||||
if (bgmBestMatches.length === 0) return;
|
||||
|
||||
// Iterate over each platform card that is currently on its first page
|
||||
platformResults.forEach((platformData, platformName) => {
|
||||
if (platformData.currentPage === 1) {
|
||||
const platformCard = resultsDiv.querySelector(`div[data-platform="${platformName}"]`);
|
||||
if (platformCard) {
|
||||
const listItems = platformCard.querySelectorAll("li[class*='group']");
|
||||
listItems.forEach(item => {
|
||||
const titleElement = item.querySelector('a > span');
|
||||
if (titleElement) {
|
||||
const title = titleElement.textContent;
|
||||
const isMatch = bgmBestMatches.some(matchName => title.includes(matchName));
|
||||
if (isMatch) {
|
||||
item.classList.add('best-match-highlight');
|
||||
} else {
|
||||
item.classList.remove('best-match-highlight');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置/清空UI界面
|
||||
*/
|
||||
function clearUI() {
|
||||
resultsDiv.innerHTML = "";
|
||||
|
||||
// Clear external link buttons
|
||||
const extLinksContainer = document.getElementById("ext-links-container");
|
||||
if (extLinksContainer) extLinksContainer.innerHTML = "";
|
||||
|
||||
isNavCollapsed = false;
|
||||
updateNavigationLayout(); // This will call updateSiteNavigation which will hide the nav if platformResults is empty
|
||||
|
||||
@@ -752,6 +915,12 @@ function showError(message) {
|
||||
function setLoadingState(isLoading) {
|
||||
if (!searchBtn || !searchIcon) return;
|
||||
|
||||
// Always clear any existing cooldown interval when state changes
|
||||
if (cooldownInterval) {
|
||||
clearInterval(cooldownInterval);
|
||||
cooldownInterval = null;
|
||||
}
|
||||
|
||||
const originalIconClass = searchIcon.dataset.originalClass || "fas fa-search";
|
||||
if (isLoading) {
|
||||
if (!searchIcon.dataset.originalClass) {
|
||||
@@ -770,25 +939,29 @@ function setLoadingState(isLoading) {
|
||||
searchBtn.disabled = false;
|
||||
searchBtn.classList.remove("active");
|
||||
searchIcon.className = originalIconClass;
|
||||
if (searchBtnText) {
|
||||
|
||||
const updateCooldownText = () => {
|
||||
const currentTime = Date.now();
|
||||
const timeLeft = Math.ceil(
|
||||
(SEARCH_COOLDOWN_MS - (currentTime - lastSearchTime)) / 1000
|
||||
);
|
||||
|
||||
if (timeLeft > 0 && lastSearchTime !== 0) {
|
||||
// Only show cooldown if a search has actually happened
|
||||
searchBtnText.textContent = `冷却中 (${timeLeft}s)`;
|
||||
// Re-enable button after cooldown
|
||||
setTimeout(() => {
|
||||
if (!searchBtn.disabled) {
|
||||
// Check if not already disabled by another process
|
||||
searchBtnText.textContent = "开始搜索";
|
||||
}
|
||||
}, timeLeft * 1000);
|
||||
if (searchBtnText) searchBtnText.textContent = `冷却中 (${timeLeft}s)`;
|
||||
searchBtn.disabled = true; // Ensure button is disabled during cooldown
|
||||
} else {
|
||||
searchBtnText.textContent = "开始搜索";
|
||||
if (searchBtnText) searchBtnText.textContent = "开始搜索";
|
||||
searchBtn.disabled = false;
|
||||
if (cooldownInterval) {
|
||||
clearInterval(cooldownInterval);
|
||||
cooldownInterval = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
updateCooldownText(); // Initial call
|
||||
cooldownInterval = setInterval(updateCooldownText, 1000); // Update every second
|
||||
|
||||
if (progressBar) {
|
||||
progressBar.classList.remove("animate__fadeIn");
|
||||
progressBar.classList.add("animate__fadeOut");
|
||||
@@ -876,8 +1049,8 @@ async function searchGameStream(
|
||||
if (onProgress) onProgress(data.progress);
|
||||
if (data.result && onResult) onResult(data.result);
|
||||
} else if (data.done && onDone) {
|
||||
onDone();
|
||||
return;
|
||||
onDone();
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("无法解析JSON行:", line, e);
|
||||
@@ -893,6 +1066,180 @@ async function searchGameStream(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches data from the VNDB API.
|
||||
* @param {string} gameName The name of the game to search for.
|
||||
* @returns {Promise<object|null>} An object with names and other info, or null.
|
||||
*/
|
||||
async function fetchVndbData(gameName) {
|
||||
console.log(`[DEBUG] Fetching VNDB data for: "${gameName}"`);
|
||||
const url = "https://api.vndb.org/kana/vn";
|
||||
const body = {
|
||||
filters: ["search", "=", gameName],
|
||||
fields: "titles.title, titles.lang, aliases, title, image.url, screenshots.url, description",
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
// Clone the response to log it, so the body can be read again later
|
||||
const responseForLog = response.clone();
|
||||
console.log("[DEBUG] Raw VNDB response:", await responseForLog.text());
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`[DEBUG] VNDB API error! Status: ${response.status}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("[DEBUG] Parsed VNDB JSON data:", data);
|
||||
|
||||
// If 'more' is true, it's not an exact match, so we ignore it.
|
||||
console.log(`[DEBUG] VNDB 'more' flag is: ${data.more}.`);
|
||||
if (data.more || !data.results || data.results.length === 0) {
|
||||
console.log("[DEBUG] VNDB returned no exact match or no results. Aborting.");
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = data.results[0];
|
||||
const names = [];
|
||||
|
||||
// Collect all aliases
|
||||
if (Array.isArray(result.aliases)) {
|
||||
result.aliases.forEach(alias => names.push(String(alias)));
|
||||
}
|
||||
|
||||
// Collect main title
|
||||
if (result.title) {
|
||||
names.push(result.title);
|
||||
}
|
||||
|
||||
// Collect all alternative titles
|
||||
let mainName = result.title || ""; // Default main name
|
||||
let zhName = "";
|
||||
let jaName = "";
|
||||
|
||||
if (Array.isArray(result.titles)) {
|
||||
result.titles.forEach((titleEntry) => {
|
||||
if (titleEntry.title) {
|
||||
names.push(titleEntry.title);
|
||||
if (titleEntry.lang === 'zh-Hans') {
|
||||
zhName = titleEntry.title;
|
||||
} else if (titleEntry.lang === 'ja') {
|
||||
jaName = titleEntry.title;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Determine the main name based on priority
|
||||
if (zhName) {
|
||||
mainName = zhName;
|
||||
} else if (jaName) {
|
||||
mainName = jaName;
|
||||
}
|
||||
|
||||
// Extract image URLs
|
||||
const mainImageUrl = result.image?.url || null;
|
||||
const screenshotUrl = result.screenshots?.[0]?.url || null;
|
||||
const description = result.description || null;
|
||||
|
||||
const finalResult = {
|
||||
names: [...new Set(names)], // Return unique names
|
||||
mainName,
|
||||
mainImageUrl,
|
||||
screenshotUrl,
|
||||
description,
|
||||
};
|
||||
|
||||
console.log("[DEBUG] Extracted Names:", finalResult.names);
|
||||
console.log("[DEBUG] Determined Main Name:", finalResult.mainName);
|
||||
console.log("[DEBUG] Extracted Main Image URL:", finalResult.mainImageUrl);
|
||||
console.log("[DEBUG] Extracted Screenshot URL:", finalResult.screenshotUrl);
|
||||
console.log("[DEBUG] Extracted Description:", finalResult.description);
|
||||
console.log("[DEBUG] Final VNDB result object:", finalResult);
|
||||
|
||||
return finalResult;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch or process VNDB data:", error);
|
||||
return null; // Return null on any error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the latest commit dates from GitHub repos and displays them as version.
|
||||
*/
|
||||
async function fetchAndDisplayVersion() {
|
||||
const versionContainer = document.getElementById("version-container");
|
||||
const versionElement = document.getElementById("version-display");
|
||||
if (!versionElement || !versionContainer) return;
|
||||
|
||||
const backendUrl = "https://api.github.com/repos/Moe-Sakura/SearchGal/commits?per_page=1";
|
||||
const frontendUrl = "https://api.github.com/repos/Moe-Sakura/frontend/commits?per_page=1";
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return "ERROR";
|
||||
const date = new Date(dateString);
|
||||
const year = String(date.getFullYear()).slice(-2);
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}${month}${day}`;
|
||||
};
|
||||
|
||||
try {
|
||||
const [backendResponse, frontendResponse] = await Promise.all([
|
||||
fetch(backendUrl),
|
||||
fetch(frontendUrl)
|
||||
]);
|
||||
|
||||
if (!backendResponse.ok || !frontendResponse.ok) {
|
||||
throw new Error("Failed to fetch version from GitHub API");
|
||||
}
|
||||
|
||||
const backendData = await backendResponse.json();
|
||||
const frontendData = await frontendResponse.json();
|
||||
|
||||
const backendDate = backendData[0]?.commit?.committer?.date;
|
||||
const frontendDate = frontendData[0]?.commit?.committer?.date;
|
||||
|
||||
const backendVersion = formatDate(backendDate);
|
||||
const frontendVersion = formatDate(frontendDate);
|
||||
|
||||
let isShowingBackend = true;
|
||||
|
||||
const updateVersionDisplay = () => {
|
||||
if (isShowingBackend) {
|
||||
versionElement.textContent = `后端 ${backendVersion}`;
|
||||
versionContainer.classList.remove("bg-red-200", "text-red-800");
|
||||
versionContainer.classList.add("bg-green-200", "text-green-800");
|
||||
versionElement.href = "https://github.com/Moe-Sakura/SearchGal/blob/main/version.md";
|
||||
} else {
|
||||
versionElement.textContent = `前端 ${frontendVersion}`;
|
||||
versionContainer.classList.remove("bg-green-200", "text-green-800");
|
||||
versionContainer.classList.add("bg-red-200", "text-red-800");
|
||||
versionElement.href = "https://github.com/Moe-Sakura/frontend/commits/main";
|
||||
}
|
||||
isShowingBackend = !isShowingBackend;
|
||||
};
|
||||
|
||||
// Initial display
|
||||
updateVersionDisplay();
|
||||
|
||||
// Start interval to switch every 5 seconds
|
||||
setInterval(updateVersionDisplay, 5000);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error fetching version:", error);
|
||||
versionElement.textContent = "版本获取失败";
|
||||
}
|
||||
}
|
||||
|
||||
function debounce(func, delay) {
|
||||
let timeout;
|
||||
return function () {
|
||||
@@ -902,3 +1249,136 @@ function debounce(func, delay) {
|
||||
timeout = setTimeout(() => func.apply(context, args), delay);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches external links from VNDB for a given game title.
|
||||
* @param {string} mainName The main title of the game.
|
||||
*/
|
||||
async function fetchVndbExtLinks(mainName) {
|
||||
const url = "https://api.vndb.org/kana/release";
|
||||
const body = {
|
||||
filters: ["search", "=", mainName],
|
||||
fields: "title, extlinks.url",
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`VNDB extlink API request failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log("[DEBUG] Received extlinks from VNDB:", data);
|
||||
|
||||
if (data.results && data.results.length > 0) {
|
||||
const allUrls = data.results.flatMap(
|
||||
(result) => result.extlinks?.map((link) => link.url) || []
|
||||
);
|
||||
renderExtLinkButtons(allUrls);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching VNDB external links:", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders categorized external link buttons based on URLs.
|
||||
* @param {string[]} urls A list of all external URLs.
|
||||
*/
|
||||
function renderExtLinkButtons(urls) {
|
||||
const container = document.getElementById("ext-links-container");
|
||||
if (!container) return;
|
||||
container.innerHTML = ""; // Clear previous buttons
|
||||
|
||||
const steamUrls = urls.filter((url) => url.includes("store.steampowered.com"));
|
||||
const dlsiteUrls = urls.filter((url) => url.includes("dlsite"));
|
||||
const officialUrls = urls.filter(
|
||||
(url) =>
|
||||
url.includes("shiravune.com") ||
|
||||
url.includes("mangagamer.com") ||
|
||||
url.includes("yuzu-soft.com") ||
|
||||
url.includes("hikarifield")
|
||||
);
|
||||
const otherUrls = urls.filter(
|
||||
(url) =>
|
||||
!steamUrls.includes(url) &&
|
||||
!dlsiteUrls.includes(url) &&
|
||||
!officialUrls.includes(url) &&
|
||||
!url.includes("steamdb")
|
||||
);
|
||||
|
||||
const categories = [
|
||||
{
|
||||
name: "Steam",
|
||||
urls: steamUrls,
|
||||
color: "bg-blue-500",
|
||||
icon: "fab fa-steam",
|
||||
},
|
||||
{
|
||||
name: "Dlsite",
|
||||
urls: dlsiteUrls,
|
||||
color: "bg-pink-500",
|
||||
icon: "fas fa-shopping-cart",
|
||||
},
|
||||
{
|
||||
name: "Official",
|
||||
urls: officialUrls,
|
||||
color: "bg-orange-500",
|
||||
icon: "fas fa-globe",
|
||||
},
|
||||
{
|
||||
name: "Other",
|
||||
urls: otherUrls,
|
||||
color: "bg-gray-400",
|
||||
icon: "fas fa-link",
|
||||
},
|
||||
];
|
||||
|
||||
categories.forEach((category) => {
|
||||
if (category.urls.length > 0) {
|
||||
const buttonWrapper = document.createElement("div");
|
||||
buttonWrapper.className = "relative";
|
||||
|
||||
const button = document.createElement("button");
|
||||
button.className = `w-10 h-10 rounded-lg ${category.color} text-white flex items-center justify-center shadow-lg hover:scale-110 transition-transform`;
|
||||
button.innerHTML = `<i class="${category.icon} text-xl"></i>`;
|
||||
|
||||
if (category.urls.length === 1) {
|
||||
button.addEventListener("click", () => {
|
||||
window.open(category.urls[0], "_blank");
|
||||
});
|
||||
} else {
|
||||
const popup = document.createElement("div");
|
||||
popup.className =
|
||||
"absolute left-full top-0 ml-2 w-max bg-white rounded-md shadow-xl p-2 z-20 hidden flex-col gap-1";
|
||||
category.urls.forEach((url) => {
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.target = "_blank";
|
||||
link.className =
|
||||
"flex items-center gap-2 text-sm text-gray-700 hover:bg-gray-100 p-1 rounded";
|
||||
link.innerHTML = `<i class="fas fa-external-link-alt text-gray-400"></i> ${
|
||||
new URL(url).hostname
|
||||
}`;
|
||||
popup.appendChild(link);
|
||||
});
|
||||
buttonWrapper.appendChild(popup);
|
||||
|
||||
button.addEventListener("mouseenter", () => popup.classList.remove("hidden"));
|
||||
button.addEventListener("mouseleave", () => popup.classList.add("hidden"));
|
||||
popup.addEventListener("mouseenter", () => popup.classList.remove("hidden"));
|
||||
popup.addEventListener("mouseleave", () => popup.classList.add("hidden"));
|
||||
}
|
||||
|
||||
buttonWrapper.appendChild(button);
|
||||
container.appendChild(buttonWrapper);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
126
src/style.css
Normal file
126
src/style.css
Normal file
@@ -0,0 +1,126 @@
|
||||
@keyframes glowing-border {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.best-match-highlight {
|
||||
background: linear-gradient(90deg,
|
||||
#ffc1f3,
|
||||
#e0b0ff,
|
||||
#d1c4e9,
|
||||
#ffc1f3);
|
||||
background-size: 400% 400%;
|
||||
animation: glowing-border 8s ease infinite;
|
||||
}
|
||||
|
||||
#version-container {
|
||||
transition: background-color 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
/* Styles for the new VNDB animation */
|
||||
#background-layer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
transition: background-image 1s ease-in-out;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
#background-layer::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
transition: background-color 1s ease-in-out;
|
||||
}
|
||||
|
||||
.vndb-mode #background-layer::before {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
/* Darken by 50% */
|
||||
}
|
||||
|
||||
#scrollable-content {
|
||||
transition: transform 0.8s ease-in-out;
|
||||
}
|
||||
|
||||
#vndb-info-panel {
|
||||
transition: opacity 0.8s ease-in-out, transform 0.8s ease-in-out;
|
||||
}
|
||||
|
||||
/* Custom scrollbar for VNDB description */
|
||||
#vndb-description::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
#vndb-description::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#vndb-description::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 20px;
|
||||
border: transparent;
|
||||
}
|
||||
|
||||
.vndb-mode #scrollable-content {
|
||||
transform: translateX(calc(16.666667% + 4rem));
|
||||
/* Move right by half of panel width + gap */
|
||||
}
|
||||
|
||||
.vndb-mode #vndb-info-panel {
|
||||
width: 50%;
|
||||
/* Corresponds to w-1/3 */
|
||||
opacity: 1;
|
||||
padding: 0.5rem;
|
||||
/* Corresponds to p-6 */
|
||||
position: sticky;
|
||||
top: 8rem;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.locked-mode #content-wrapper,
|
||||
.locked-mode #scrollToTopBtn,
|
||||
.locked-mode #scrollToCommentsBtn {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.locked-mode #background-layer::before {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
#content-wrapper,
|
||||
#scrollToTopBtn,
|
||||
#scrollToCommentsBtn {
|
||||
transition: opacity 0.5s ease-in-out;
|
||||
}
|
||||
/* 手机端适配 */
|
||||
@media (max-width: 768px) {
|
||||
#lock-view-btn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vndb-mode #vndb-info-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vndb-mode #scrollable-content {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user