From 2a5aa51ff5774a2618beaad2195d0ddde244fc9d Mon Sep 17 00:00:00 2001 From: AdingApkgg Date: Mon, 17 Nov 2025 15:48:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E9=A6=96=E9=A1=B5?= =?UTF-8?q?=E7=BB=93=E6=9E=84=E4=B8=8E=E6=A0=B7=E5=BC=8F=EF=BC=8C=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E6=97=A7=E7=89=88=E4=B8=BB=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 在 `index.html` 中添加了新的元数据和样式,以提升SEO和用户体验。 * 移除 `src/main.js` 文件,简化项目结构,集中管理逻辑。 * 新增 `SearchHeader.vue` 组件,重构搜索表单和状态显示,优化用户交互。 * 更新样式以符合 Material 3 设计规范,增强视觉一致性。 --- .gitignore | 2 + env.d.ts | 21 + index.html | 941 +++++----- package.json | 27 +- pnpm-lock.yaml | 1764 ++++++++++++------ public/fonts/material-symbols.css | 25 + public/gamepad-solid.svg | 1 - public/robots.txt | 1 - public/sitemap.xml | 22 +- public/sw.js | 267 +++ src/App.vue | 275 +++ src/api/search.ts | 306 ++++ src/components/CommentsModal.vue | 162 ++ src/components/FloatingButtons.vue | 138 ++ src/components/PageFooter.vue | 209 +++ src/components/PlatformNav.vue | 186 ++ src/components/SearchHeader.vue | 260 +-- src/components/SearchResults.vue | 395 ++++ src/main.js | 2752 ---------------------------- src/main.ts | 98 + src/router/index.ts | 16 + src/stores/search.ts | 123 ++ src/types/pace.d.ts | 23 + src/utils/sitemap.ts | 104 ++ src/views/HomeView.vue | 26 + tsconfig.json | 34 + tsconfig.node.json | 12 + vite.config.ts | 233 ++- 28 files changed, 4560 insertions(+), 3863 deletions(-) create mode 100644 env.d.ts create mode 100644 public/fonts/material-symbols.css delete mode 100644 public/gamepad-solid.svg create mode 100644 public/sw.js create mode 100644 src/App.vue create mode 100644 src/api/search.ts create mode 100644 src/components/CommentsModal.vue create mode 100644 src/components/FloatingButtons.vue create mode 100644 src/components/PageFooter.vue create mode 100644 src/components/PlatformNav.vue create mode 100644 src/components/SearchResults.vue delete mode 100644 src/main.js create mode 100644 src/main.ts create mode 100644 src/router/index.ts create mode 100644 src/stores/search.ts create mode 100644 src/types/pace.d.ts create mode 100644 src/utils/sitemap.ts create mode 100644 src/views/HomeView.vue create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json diff --git a/.gitignore b/.gitignore index b47b3fc..fe63289 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,5 @@ dist-ssr *.sw? .history +.pnpm-store +.pnpm-lock.yaml diff --git a/env.d.ts b/env.d.ts new file mode 100644 index 0000000..9ea7b1a --- /dev/null +++ b/env.d.ts @@ -0,0 +1,21 @@ +/// + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + const component: DefineComponent<{}, {}, any> + export default component +} + +interface Window { + Pace: { + restart(): void + options: { + ajax?: boolean + document?: boolean + eventLag?: boolean + elements?: boolean + restartOnPushState?: boolean + restartOnRequestAfter?: boolean + } + } +} diff --git a/index.html b/index.html index 4edc4e6..97637d0 100644 --- a/index.html +++ b/index.html @@ -1,493 +1,518 @@ - + + + + - - - - SearchGal - Galgame 聚合搜索 - - - - - - @media (max-width: 767px) { /* For mobile viewports */ + + + - - - - - - - - - + md-filled-button { + --md-filled-button-container-color: linear-gradient( + 135deg, + rgb(236, 72, 153), + rgb(219, 39, 119) + ); + --md-filled-button-label-text-color: var(--md-sys-color-on-primary); + --md-filled-button-container-shape: 16px; + border-radius: 16px; + box-shadow: 0 6px 20px rgba(236, 72, 153, 0.35), + 0 3px 8px rgba(0, 0, 0, 0.1); + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; + } - -
-
-
-
- -
-
-
- 随图 -
-
-

- - Galgame 聚合搜索 -

-
-
-
-
- - -
-
- - - - - 例如: https://api.searchgal.homes 或 http://127.0.0.1:8898 - -
-
-
- -
-
- - -
-
- - -
- -
-
-
-
-
-
-
-

- 咱家的使用须知 -

-
    -
  • - 首先,衷心感谢 - @Asuna - 大佬提供的服务器和技术支持!没有大佬的魔法,咱可跑不起来! -
  • -
  • - 本程序纯属爱发电,仅供绅士们交流学习使用,务必请大家支持正版 Galgame!入正不亏哦! -
  • -
  • - 本站只做互联网内容的聚合搬运工,搜索结果均来自第三方站点,下载前请各位自行判断资源安全性,以免翻车。 -
  • -
  • - 搜索时请注意关键词长度!关键词太短可能搜不全(部分站点只显示首批结果),太长则可能无法精准匹配。建议尝试适当的关键词,效果更佳~ -
  • -
  • - 本程序每次查询完毕即断开连接,严禁任何形式的爆破或恶意爬取,做个文明的绅士! -
  • -
  • - 万一某个站点搜索挂了,先看看自己的魔法是否到位,也可能是站点维护了,或者咱这边的爬虫失效了。 -
  • -
  • - 为了支持各 Galgame - 站点能长久运营,还请各位把浏览器的广告屏蔽插件关掉,或将这些站点加入白名单。大家建站不易,小小的支持也是大大的动力! -
  • -
  • 游戏介绍和人物信息数据由 VNDB 提供,由AI大模型翻译,翻译结果不保证准确性,仅作为检索游戏时的参考! -
  • -
  • - 郑重呼吁:请务必支持 Galgame 正版!让爱与梦想延续! -
  • -
  • - 如果您觉得咱这小工具好用,请移步 - GitHub - 给本项目点个免费的 - Star - 吧,秋梨膏!你的支持就是咱最大的动力,比心~ -
  • -
-
-
- -
-
-
-
-
-
-
+ md-filled-button:hover::before { + left: 100%; + } -
-
-
+ md-filled-button:hover { + box-shadow: 0 10px 32px rgba(236, 72, 153, 0.5), + 0 6px 16px rgba(0, 0, 0, 0.15); + transform: translateY(-3px) scale(1.02); + } -
- - - - - -
+ md-filled-button:active { + transform: translateY(-1px) scale(0.98); + box-shadow: 0 4px 16px rgba(236, 72, 153, 0.4); + } - - - - - - + md-fab { + --md-fab-container-color: var(--md-sys-color-primary); + --md-fab-icon-color: var(--md-sys-color-on-primary); + --md-fab-container-shape: 24px; + box-shadow: 0 8px 24px rgba(236, 72, 153, 0.4), + 0 4px 12px rgba(0, 0, 0, 0.15); + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + } - \ No newline at end of file + md-fab:hover { + box-shadow: 0 12px 36px rgba(236, 72, 153, 0.5), + 0 6px 20px rgba(0, 0, 0, 0.2); + transform: translateY(-4px) scale(1.08) rotate(5deg); + } + + md-fab:active { + transform: translateY(-2px) scale(1.02); + box-shadow: 0 6px 20px rgba(236, 72, 153, 0.4); + } + + md-elevated-card, + md-filled-card, + md-outlined-card { + --md-elevated-card-container-color: rgba(255, 255, 255, 0.85); + --md-elevated-card-container-shape: 24px; + border-radius: 24px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08), + 0 4px 16px rgba(0, 0, 0, 0.04); + backdrop-filter: blur(30px) saturate(150%); + -webkit-backdrop-filter: blur(30px) saturate(150%); + border: 1px solid rgba(255, 255, 255, 0.5); + transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; + } + + md-elevated-card::before { + content: ""; + position: absolute; + top: 0; + left: -100%; + width: 100%; + height: 100%; + background: linear-gradient( + 90deg, + transparent, + rgba(236, 72, 153, 0.1), + transparent + ); + transition: left 0.6s ease; + } + + md-elevated-card:hover::before { + left: 100%; + } + + md-elevated-card:hover, + md-filled-card:hover, + md-outlined-card:hover { + box-shadow: 0 16px 48px rgba(236, 72, 153, 0.15), + 0 8px 24px rgba(0, 0, 0, 0.1); + transform: translateY(-4px) scale(1.01); + border-color: rgba(236, 72, 153, 0.3); + } + + md-assist-chip, + md-filter-chip { + --md-assist-chip-container-shape: 16px; + border-radius: 16px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + } + + md-assist-chip:hover, + md-filter-chip:hover { + transform: scale(1.08) translateY(-2px); + box-shadow: 0 4px 16px rgba(236, 72, 153, 0.2); + } + + md-filter-chip[selected] { + box-shadow: 0 4px 16px rgba(236, 72, 153, 0.3); + animation: pulse 2s infinite; + } + + @keyframes pulse { + 0%, + 100% { + box-shadow: 0 4px 16px rgba(236, 72, 153, 0.3); + } + 50% { + box-shadow: 0 6px 24px rgba(236, 72, 153, 0.5); + } + } + + md-list-item { + border-radius: 16px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + margin: 4px 0; + } + + md-list-item:hover { + background-color: rgba(236, 72, 153, 0.08); + transform: translateX(4px); + box-shadow: 0 2px 8px rgba(236, 72, 153, 0.15); + } + + md-icon { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + } + + md-icon:hover { + transform: scale(1.15) rotate(10deg); + filter: drop-shadow(0 2px 4px rgba(236, 72, 153, 0.3)); + } + + /* 对话框优化 */ + md-dialog { + --md-dialog-container-shape: 32px; + border-radius: 32px; + box-shadow: 0 24px 64px rgba(0, 0, 0, 0.25), + 0 12px 32px rgba(0, 0, 0, 0.15); + backdrop-filter: blur(40px); + -webkit-backdrop-filter: blur(40px); + border: 1px solid rgba(255, 255, 255, 0.3); + } + + /* 进度条优化 */ + md-linear-progress { + --md-linear-progress-track-shape: 8px; + border-radius: 8px; + height: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + } + + /* 图标按钮优化 */ + md-icon-button { + --md-icon-button-state-layer-shape: 50%; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + } + + md-icon-button:hover { + transform: scale(1.15) rotate(5deg); + filter: drop-shadow(0 4px 8px rgba(236, 72, 153, 0.3)); + } + + /* 加载动画 */ + @keyframes shimmer { + 0% { + background-position: -1000px 0; + } + 100% { + background-position: 1000px 0; + } + } + + .loading-shimmer { + animation: shimmer 2s infinite linear; + background: linear-gradient( + 90deg, + rgba(255, 255, 255, 0) 0%, + rgba(255, 255, 255, 0.3) 50%, + rgba(255, 255, 255, 0) 100% + ); + background-size: 1000px 100%; + } + + /* 淡入动画 */ + @keyframes fadeIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } + } + + .fade-in { + animation: fadeIn 0.6s cubic-bezier(0.4, 0, 0.2, 1); + } + + /* 弹跳动画 */ + @keyframes bounce { + 0%, + 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-10px); + } + } + + .bounce { + animation: bounce 1s ease-in-out infinite; + } + + + + +
+ + + + + + diff --git a/package.json b/package.json index 54e0d0c..197c8e5 100644 --- a/package.json +++ b/package.json @@ -9,10 +9,29 @@ "preview": "vite preview" }, "devDependencies": { - "vite": "^7.0.3" + "@tailwindcss/vite": "^4.1.17", + "@types/node": "^24.10.1", + "@vitejs/plugin-vue": "^6.0.1", + "tailwindcss": "^4.1.17", + "typescript": "^5.9.3", + "vite": "^7.0.3", + "vue-tsc": "^3.1.4" }, "dependencies": { - "@tailwindcss/vite": "^4.1.11", - "tailwindcss": "^4.1.11" + "@artalk/plugin-lightbox": "^0.2.4", + "@fancyapps/ui": "^6.1.5", + "@fortawesome/fontawesome-free": "^7.1.0", + "@material/web": "^2.4.1", + "@mdui/icons": "^1.0.3", + "artalk": "^2.9.1", + "gsap": "^3.13.0", + "instant.page": "^5.2.0", + "lightgallery": "^2.9.0", + "lozad": "^1.16.0", + "pace-js": "^1.2.4", + "pinia": "^3.0.4", + "quicklink": "^3.0.1", + "vue": "^3.5.24", + "vue-router": "^4.6.3" } -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index db49b49..4aee9cc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,510 +1,489 @@ -lockfileVersion: '6.0' +lockfileVersion: '9.0' settings: autoInstallPeers: true excludeLinksFromLockfile: false -dependencies: - '@tailwindcss/vite': - specifier: ^4.1.11 - version: 4.1.11(vite@7.0.3) - tailwindcss: - specifier: ^4.1.11 - version: 4.1.11 +importers: -devDependencies: - vite: - specifier: ^7.0.3 - version: 7.0.3 + .: + dependencies: + '@artalk/plugin-lightbox': + specifier: ^0.2.4 + version: 0.2.4(artalk@2.9.1(marked@14.1.4))(lightgallery@2.9.0) + '@fancyapps/ui': + specifier: ^6.1.5 + version: 6.1.5 + '@fortawesome/fontawesome-free': + specifier: ^7.1.0 + version: 7.1.0 + '@material/web': + specifier: ^2.4.1 + version: 2.4.1 + '@mdui/icons': + specifier: ^1.0.3 + version: 1.0.3 + artalk: + specifier: ^2.9.1 + version: 2.9.1(marked@14.1.4) + gsap: + specifier: ^3.13.0 + version: 3.13.0 + instant.page: + specifier: ^5.2.0 + version: 5.2.0 + lightgallery: + specifier: ^2.9.0 + version: 2.9.0 + lozad: + specifier: ^1.16.0 + version: 1.16.0 + pace-js: + specifier: ^1.2.4 + version: 1.2.4 + pinia: + specifier: ^3.0.4 + version: 3.0.4(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3)) + quicklink: + specifier: ^3.0.1 + version: 3.0.1 + vue: + specifier: ^3.5.24 + version: 3.5.24(typescript@5.9.3) + vue-router: + specifier: ^4.6.3 + version: 4.6.3(vue@3.5.24(typescript@5.9.3)) + devDependencies: + '@tailwindcss/vite': + specifier: ^4.1.17 + version: 4.1.17(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)) + '@types/node': + specifier: ^24.10.1 + version: 24.10.1 + '@vitejs/plugin-vue': + specifier: ^6.0.1 + version: 6.0.1(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2))(vue@3.5.24(typescript@5.9.3)) + tailwindcss: + specifier: ^4.1.17 + version: 4.1.17 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + vite: + specifier: ^7.0.3 + version: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2) + vue-tsc: + specifier: ^3.1.4 + version: 3.1.4(typescript@5.9.3) packages: - /@ampproject/remapping@2.3.0: - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} - dependencies: - '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 - dev: false + '@artalk/plugin-lightbox@0.2.4': + resolution: {integrity: sha512-cYewbb2rkwofDqWq51MrMO/idbN7mcYTrIicsi8xpoRJdr7hwLzqM6ODih9cnbiS25l8+3PVYPTIn3g5WeqkYg==} + peerDependencies: + artalk: ^2.9.1 + fancybox: ^3.0.1 + lightbox2: ^2.11.4 + lightgallery: ^2.7.2 + photoswipe: ^5.4.4 + peerDependenciesMeta: + fancybox: + optional: true + lightbox2: + optional: true + lightgallery: + optional: true + photoswipe: + optional: true - /@esbuild/aix-ppc64@0.25.6: - resolution: {integrity: sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==} + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + engines: {node: '>=6.9.0'} + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - requiresBuild: true - optional: true - /@esbuild/android-arm64@0.25.6: - resolution: {integrity: sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==} + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - requiresBuild: true - optional: true - /@esbuild/android-arm@0.25.6: - resolution: {integrity: sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==} + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} cpu: [arm] os: [android] - requiresBuild: true - optional: true - /@esbuild/android-x64@0.25.6: - resolution: {integrity: sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==} + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} cpu: [x64] os: [android] - requiresBuild: true - optional: true - /@esbuild/darwin-arm64@0.25.6: - resolution: {integrity: sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==} + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - requiresBuild: true - optional: true - /@esbuild/darwin-x64@0.25.6: - resolution: {integrity: sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==} + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - requiresBuild: true - optional: true - /@esbuild/freebsd-arm64@0.25.6: - resolution: {integrity: sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==} + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - requiresBuild: true - optional: true - /@esbuild/freebsd-x64@0.25.6: - resolution: {integrity: sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==} + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - requiresBuild: true - optional: true - /@esbuild/linux-arm64@0.25.6: - resolution: {integrity: sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==} + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - requiresBuild: true - optional: true - /@esbuild/linux-arm@0.25.6: - resolution: {integrity: sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==} + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - requiresBuild: true - optional: true - /@esbuild/linux-ia32@0.25.6: - resolution: {integrity: sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==} + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - requiresBuild: true - optional: true - /@esbuild/linux-loong64@0.25.6: - resolution: {integrity: sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==} + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - requiresBuild: true - optional: true - /@esbuild/linux-mips64el@0.25.6: - resolution: {integrity: sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==} + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - requiresBuild: true - optional: true - /@esbuild/linux-ppc64@0.25.6: - resolution: {integrity: sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==} + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - requiresBuild: true - optional: true - /@esbuild/linux-riscv64@0.25.6: - resolution: {integrity: sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==} + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - requiresBuild: true - optional: true - /@esbuild/linux-s390x@0.25.6: - resolution: {integrity: sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==} + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - requiresBuild: true - optional: true - /@esbuild/linux-x64@0.25.6: - resolution: {integrity: sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==} + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} cpu: [x64] os: [linux] - requiresBuild: true - optional: true - /@esbuild/netbsd-arm64@0.25.6: - resolution: {integrity: sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==} + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - requiresBuild: true - optional: true - /@esbuild/netbsd-x64@0.25.6: - resolution: {integrity: sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==} + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - requiresBuild: true - optional: true - /@esbuild/openbsd-arm64@0.25.6: - resolution: {integrity: sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==} + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - requiresBuild: true - optional: true - /@esbuild/openbsd-x64@0.25.6: - resolution: {integrity: sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==} + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - requiresBuild: true - optional: true - /@esbuild/openharmony-arm64@0.25.6: - resolution: {integrity: sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==} + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - requiresBuild: true - optional: true - /@esbuild/sunos-x64@0.25.6: - resolution: {integrity: sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==} + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - requiresBuild: true - optional: true - /@esbuild/win32-arm64@0.25.6: - resolution: {integrity: sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==} + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - requiresBuild: true - optional: true - /@esbuild/win32-ia32@0.25.6: - resolution: {integrity: sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==} + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - requiresBuild: true - optional: true - /@esbuild/win32-x64@0.25.6: - resolution: {integrity: sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==} + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} cpu: [x64] os: [win32] - requiresBuild: true - optional: true - /@isaacs/fs-minipass@4.0.1: - resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} - engines: {node: '>=18.0.0'} - dependencies: - minipass: 7.1.2 - dev: false + '@fancyapps/ui@6.1.5': + resolution: {integrity: sha512-uFNm+rlrVMD8vqnthVC0l9ME+V8HRUma5LLpkR/2DACUNKSpE8qppcns0ZITcODdUmV/dGxPHTnymihohp2/Og==} - /@jridgewell/gen-mapping@0.3.12: - resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} - dependencies: - '@jridgewell/sourcemap-codec': 1.5.4 - '@jridgewell/trace-mapping': 0.3.29 - dev: false + '@fortawesome/fontawesome-free@7.1.0': + resolution: {integrity: sha512-+WxNld5ZCJHvPQCr/GnzCTVREyStrAJjisUPtUxG5ngDA8TMlPnKp6dddlTpai4+1GNmltAeuk1hJEkBohwZYA==} + engines: {node: '>=6'} - /@jridgewell/resolve-uri@3.1.2: + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - dev: false - /@jridgewell/sourcemap-codec@1.5.4: - resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} - dev: false + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - /@jridgewell/trace-mapping@0.3.29: - resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.4 - dev: false + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - /@rollup/rollup-android-arm-eabi@4.44.2: - resolution: {integrity: sha512-g0dF8P1e2QYPOj1gu7s/3LVP6kze9A7m6x0BZ9iTdXK8N5c2V7cpBKHV3/9A4Zd8xxavdhK0t4PnqjkqVmUc9Q==} + '@lit-labs/ssr-dom-shim@1.4.0': + resolution: {integrity: sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw==} + + '@lit/reactive-element@2.1.1': + resolution: {integrity: sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==} + + '@material/web@2.4.1': + resolution: {integrity: sha512-0sk9t25acJ72Qv3r0n9r0lgDbPaAKnpm0p+QmEAAwYyZomHxuVbgrrAdtNXaRm7jFyGh+WsTr8bhtvCnpPRFjw==} + + '@mdui/icons@1.0.3': + resolution: {integrity: sha512-Jq9juUqIJMBvIRSTr0qBKiqnlbY5pVUzUP20EHSN8dT7GcqN7bq0AL8MASL5DxKs09kgcERq+z5bHJOkz/VDlA==} + + '@mdui/jq@3.0.3': + resolution: {integrity: sha512-nI8QK9UPHhiIbECrC1aMdLXNxP6WgUtC9XwRPs/e56FtwduePyxPyloXmgU8VYw85i6TtYdgClHS9tW8JweNZA==} + + '@mdui/shared@1.0.8': + resolution: {integrity: sha512-YY863fjHBuk8KtiO4uLDm1YyIVdGrWv4xUxfv8jP32WqIQBkSTbV7HN8jnKXXIej0NFP7pU89yGr4GJYzVszPg==} + + '@rolldown/pluginutils@1.0.0-beta.29': + resolution: {integrity: sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==} + + '@rollup/rollup-android-arm-eabi@4.53.2': + resolution: {integrity: sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==} cpu: [arm] os: [android] - requiresBuild: true - optional: true - /@rollup/rollup-android-arm64@4.44.2: - resolution: {integrity: sha512-Yt5MKrOosSbSaAK5Y4J+vSiID57sOvpBNBR6K7xAaQvk3MkcNVV0f9fE20T+41WYN8hDn6SGFlFrKudtx4EoxA==} + '@rollup/rollup-android-arm64@4.53.2': + resolution: {integrity: sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==} cpu: [arm64] os: [android] - requiresBuild: true - optional: true - /@rollup/rollup-darwin-arm64@4.44.2: - resolution: {integrity: sha512-EsnFot9ZieM35YNA26nhbLTJBHD0jTwWpPwmRVDzjylQT6gkar+zenfb8mHxWpRrbn+WytRRjE0WKsfaxBkVUA==} + '@rollup/rollup-darwin-arm64@4.53.2': + resolution: {integrity: sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==} cpu: [arm64] os: [darwin] - requiresBuild: true - optional: true - /@rollup/rollup-darwin-x64@4.44.2: - resolution: {integrity: sha512-dv/t1t1RkCvJdWWxQ2lWOO+b7cMsVw5YFaS04oHpZRWehI1h0fV1gF4wgGCTyQHHjJDfbNpwOi6PXEafRBBezw==} + '@rollup/rollup-darwin-x64@4.53.2': + resolution: {integrity: sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==} cpu: [x64] os: [darwin] - requiresBuild: true - optional: true - /@rollup/rollup-freebsd-arm64@4.44.2: - resolution: {integrity: sha512-W4tt4BLorKND4qeHElxDoim0+BsprFTwb+vriVQnFFtT/P6v/xO5I99xvYnVzKWrK6j7Hb0yp3x7V5LUbaeOMg==} + '@rollup/rollup-freebsd-arm64@4.53.2': + resolution: {integrity: sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==} cpu: [arm64] os: [freebsd] - requiresBuild: true - optional: true - /@rollup/rollup-freebsd-x64@4.44.2: - resolution: {integrity: sha512-tdT1PHopokkuBVyHjvYehnIe20fxibxFCEhQP/96MDSOcyjM/shlTkZZLOufV3qO6/FQOSiJTBebhVc12JyPTA==} + '@rollup/rollup-freebsd-x64@4.53.2': + resolution: {integrity: sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==} cpu: [x64] os: [freebsd] - requiresBuild: true - optional: true - /@rollup/rollup-linux-arm-gnueabihf@4.44.2: - resolution: {integrity: sha512-+xmiDGGaSfIIOXMzkhJ++Oa0Gwvl9oXUeIiwarsdRXSe27HUIvjbSIpPxvnNsRebsNdUo7uAiQVgBD1hVriwSQ==} + '@rollup/rollup-linux-arm-gnueabihf@4.53.2': + resolution: {integrity: sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==} cpu: [arm] os: [linux] - requiresBuild: true - optional: true - /@rollup/rollup-linux-arm-musleabihf@4.44.2: - resolution: {integrity: sha512-bDHvhzOfORk3wt8yxIra8N4k/N0MnKInCW5OGZaeDYa/hMrdPaJzo7CSkjKZqX4JFUWjUGm88lI6QJLCM7lDrA==} + '@rollup/rollup-linux-arm-musleabihf@4.53.2': + resolution: {integrity: sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==} cpu: [arm] os: [linux] - requiresBuild: true - optional: true - /@rollup/rollup-linux-arm64-gnu@4.44.2: - resolution: {integrity: sha512-NMsDEsDiYghTbeZWEGnNi4F0hSbGnsuOG+VnNvxkKg0IGDvFh7UVpM/14mnMwxRxUf9AdAVJgHPvKXf6FpMB7A==} + '@rollup/rollup-linux-arm64-gnu@4.53.2': + resolution: {integrity: sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==} cpu: [arm64] os: [linux] - requiresBuild: true - optional: true - /@rollup/rollup-linux-arm64-musl@4.44.2: - resolution: {integrity: sha512-lb5bxXnxXglVq+7imxykIp5xMq+idehfl+wOgiiix0191av84OqbjUED+PRC5OA8eFJYj5xAGcpAZ0pF2MnW+A==} + '@rollup/rollup-linux-arm64-musl@4.53.2': + resolution: {integrity: sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==} cpu: [arm64] os: [linux] - requiresBuild: true - optional: true - /@rollup/rollup-linux-loongarch64-gnu@4.44.2: - resolution: {integrity: sha512-Yl5Rdpf9pIc4GW1PmkUGHdMtbx0fBLE1//SxDmuf3X0dUC57+zMepow2LK0V21661cjXdTn8hO2tXDdAWAqE5g==} + '@rollup/rollup-linux-loong64-gnu@4.53.2': + resolution: {integrity: sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==} cpu: [loong64] os: [linux] - requiresBuild: true - optional: true - /@rollup/rollup-linux-powerpc64le-gnu@4.44.2: - resolution: {integrity: sha512-03vUDH+w55s680YYryyr78jsO1RWU9ocRMaeV2vMniJJW/6HhoTBwyyiiTPVHNWLnhsnwcQ0oH3S9JSBEKuyqw==} + '@rollup/rollup-linux-ppc64-gnu@4.53.2': + resolution: {integrity: sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==} cpu: [ppc64] os: [linux] - requiresBuild: true - optional: true - /@rollup/rollup-linux-riscv64-gnu@4.44.2: - resolution: {integrity: sha512-iYtAqBg5eEMG4dEfVlkqo05xMOk6y/JXIToRca2bAWuqjrJYJlx/I7+Z+4hSrsWU8GdJDFPL4ktV3dy4yBSrzg==} + '@rollup/rollup-linux-riscv64-gnu@4.53.2': + resolution: {integrity: sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==} cpu: [riscv64] os: [linux] - requiresBuild: true - optional: true - /@rollup/rollup-linux-riscv64-musl@4.44.2: - resolution: {integrity: sha512-e6vEbgaaqz2yEHqtkPXa28fFuBGmUJ0N2dOJK8YUfijejInt9gfCSA7YDdJ4nYlv67JfP3+PSWFX4IVw/xRIPg==} + '@rollup/rollup-linux-riscv64-musl@4.53.2': + resolution: {integrity: sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==} cpu: [riscv64] os: [linux] - requiresBuild: true - optional: true - /@rollup/rollup-linux-s390x-gnu@4.44.2: - resolution: {integrity: sha512-evFOtkmVdY3udE+0QKrV5wBx7bKI0iHz5yEVx5WqDJkxp9YQefy4Mpx3RajIVcM6o7jxTvVd/qpC1IXUhGc1Mw==} + '@rollup/rollup-linux-s390x-gnu@4.53.2': + resolution: {integrity: sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==} cpu: [s390x] os: [linux] - requiresBuild: true - optional: true - /@rollup/rollup-linux-x64-gnu@4.44.2: - resolution: {integrity: sha512-/bXb0bEsWMyEkIsUL2Yt5nFB5naLAwyOWMEviQfQY1x3l5WsLKgvZf66TM7UTfED6erckUVUJQ/jJ1FSpm3pRQ==} + '@rollup/rollup-linux-x64-gnu@4.53.2': + resolution: {integrity: sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==} cpu: [x64] os: [linux] - requiresBuild: true - optional: true - /@rollup/rollup-linux-x64-musl@4.44.2: - resolution: {integrity: sha512-3D3OB1vSSBXmkGEZR27uiMRNiwN08/RVAcBKwhUYPaiZ8bcvdeEwWPvbnXvvXHY+A/7xluzcN+kaiOFNiOZwWg==} + '@rollup/rollup-linux-x64-musl@4.53.2': + resolution: {integrity: sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==} cpu: [x64] os: [linux] - requiresBuild: true - optional: true - /@rollup/rollup-win32-arm64-msvc@4.44.2: - resolution: {integrity: sha512-VfU0fsMK+rwdK8mwODqYeM2hDrF2WiHaSmCBrS7gColkQft95/8tphyzv2EupVxn3iE0FI78wzffoULH1G+dkw==} + '@rollup/rollup-openharmony-arm64@4.53.2': + resolution: {integrity: sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.53.2': + resolution: {integrity: sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==} cpu: [arm64] os: [win32] - requiresBuild: true - optional: true - /@rollup/rollup-win32-ia32-msvc@4.44.2: - resolution: {integrity: sha512-+qMUrkbUurpE6DVRjiJCNGZBGo9xM4Y0FXU5cjgudWqIBWbcLkjE3XprJUsOFgC6xjBClwVa9k6O3A7K3vxb5Q==} + '@rollup/rollup-win32-ia32-msvc@4.53.2': + resolution: {integrity: sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==} cpu: [ia32] os: [win32] - requiresBuild: true - optional: true - /@rollup/rollup-win32-x64-msvc@4.44.2: - resolution: {integrity: sha512-3+QZROYfJ25PDcxFF66UEk8jGWigHJeecZILvkPkyQN7oc5BvFo4YEXFkOs154j3FTMp9mn9Ky8RCOwastduEA==} + '@rollup/rollup-win32-x64-gnu@4.53.2': + resolution: {integrity: sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==} cpu: [x64] os: [win32] - requiresBuild: true - optional: true - /@tailwindcss/node@4.1.11: - resolution: {integrity: sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==} - dependencies: - '@ampproject/remapping': 2.3.0 - enhanced-resolve: 5.18.2 - jiti: 2.4.2 - lightningcss: 1.30.1 - magic-string: 0.30.17 - source-map-js: 1.2.1 - tailwindcss: 4.1.11 - dev: false + '@rollup/rollup-win32-x64-msvc@4.53.2': + resolution: {integrity: sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==} + cpu: [x64] + os: [win32] - /@tailwindcss/oxide-android-arm64@4.1.11: - resolution: {integrity: sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==} + '@tailwindcss/node@4.1.17': + resolution: {integrity: sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==} + + '@tailwindcss/oxide-android-arm64@4.1.17': + resolution: {integrity: sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==} engines: {node: '>= 10'} cpu: [arm64] os: [android] - requiresBuild: true - dev: false - optional: true - /@tailwindcss/oxide-darwin-arm64@4.1.11: - resolution: {integrity: sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==} + '@tailwindcss/oxide-darwin-arm64@4.1.17': + resolution: {integrity: sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - requiresBuild: true - dev: false - optional: true - /@tailwindcss/oxide-darwin-x64@4.1.11: - resolution: {integrity: sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==} + '@tailwindcss/oxide-darwin-x64@4.1.17': + resolution: {integrity: sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - requiresBuild: true - dev: false - optional: true - /@tailwindcss/oxide-freebsd-x64@4.1.11: - resolution: {integrity: sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==} + '@tailwindcss/oxide-freebsd-x64@4.1.17': + resolution: {integrity: sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==} engines: {node: '>= 10'} cpu: [x64] os: [freebsd] - requiresBuild: true - dev: false - optional: true - /@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11: - resolution: {integrity: sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17': + resolution: {integrity: sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - requiresBuild: true - dev: false - optional: true - /@tailwindcss/oxide-linux-arm64-gnu@4.1.11: - resolution: {integrity: sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==} + '@tailwindcss/oxide-linux-arm64-gnu@4.1.17': + resolution: {integrity: sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - requiresBuild: true - dev: false - optional: true - /@tailwindcss/oxide-linux-arm64-musl@4.1.11: - resolution: {integrity: sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==} + '@tailwindcss/oxide-linux-arm64-musl@4.1.17': + resolution: {integrity: sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - requiresBuild: true - dev: false - optional: true - /@tailwindcss/oxide-linux-x64-gnu@4.1.11: - resolution: {integrity: sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==} + '@tailwindcss/oxide-linux-x64-gnu@4.1.17': + resolution: {integrity: sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - requiresBuild: true - dev: false - optional: true - /@tailwindcss/oxide-linux-x64-musl@4.1.11: - resolution: {integrity: sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==} + '@tailwindcss/oxide-linux-x64-musl@4.1.17': + resolution: {integrity: sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - requiresBuild: true - dev: false - optional: true - /@tailwindcss/oxide-wasm32-wasi@4.1.11: - resolution: {integrity: sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==} + '@tailwindcss/oxide-wasm32-wasi@4.1.17': + resolution: {integrity: sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==} engines: {node: '>=14.0.0'} cpu: [wasm32] - requiresBuild: true - dev: false - optional: true bundledDependencies: - '@napi-rs/wasm-runtime' - '@emnapi/core' @@ -513,352 +492,377 @@ packages: - '@emnapi/wasi-threads' - tslib - /@tailwindcss/oxide-win32-arm64-msvc@4.1.11: - resolution: {integrity: sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==} + '@tailwindcss/oxide-win32-arm64-msvc@4.1.17': + resolution: {integrity: sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - requiresBuild: true - dev: false - optional: true - /@tailwindcss/oxide-win32-x64-msvc@4.1.11: - resolution: {integrity: sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==} + '@tailwindcss/oxide-win32-x64-msvc@4.1.17': + resolution: {integrity: sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - requiresBuild: true - dev: false - optional: true - /@tailwindcss/oxide@4.1.11: - resolution: {integrity: sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==} + '@tailwindcss/oxide@4.1.17': + resolution: {integrity: sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==} engines: {node: '>= 10'} - requiresBuild: true - dependencies: - detect-libc: 2.0.4 - tar: 7.4.3 - optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.1.11 - '@tailwindcss/oxide-darwin-arm64': 4.1.11 - '@tailwindcss/oxide-darwin-x64': 4.1.11 - '@tailwindcss/oxide-freebsd-x64': 4.1.11 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.11 - '@tailwindcss/oxide-linux-arm64-gnu': 4.1.11 - '@tailwindcss/oxide-linux-arm64-musl': 4.1.11 - '@tailwindcss/oxide-linux-x64-gnu': 4.1.11 - '@tailwindcss/oxide-linux-x64-musl': 4.1.11 - '@tailwindcss/oxide-wasm32-wasi': 4.1.11 - '@tailwindcss/oxide-win32-arm64-msvc': 4.1.11 - '@tailwindcss/oxide-win32-x64-msvc': 4.1.11 - dev: false - /@tailwindcss/vite@4.1.11(vite@7.0.3): - resolution: {integrity: sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==} + '@tailwindcss/vite@4.1.17': + resolution: {integrity: sha512-4+9w8ZHOiGnpcGI6z1TVVfWaX/koK7fKeSYF3qlYg2xpBtbteP2ddBxiarL+HVgfSJGeK5RIxRQmKm4rTJJAwA==} peerDependencies: vite: ^5.2.0 || ^6 || ^7 - dependencies: - '@tailwindcss/node': 4.1.11 - '@tailwindcss/oxide': 4.1.11 - tailwindcss: 4.1.11 - vite: 7.0.3 - dev: false - /@types/estree@1.0.8: + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - /chownr@3.0.0: - resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + '@types/node@24.10.1': + resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@vitejs/plugin-vue@6.0.1': + resolution: {integrity: sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + vue: ^3.2.25 + + '@volar/language-core@2.4.23': + resolution: {integrity: sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==} + + '@volar/source-map@2.4.23': + resolution: {integrity: sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==} + + '@volar/typescript@2.4.23': + resolution: {integrity: sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==} + + '@vue/compiler-core@3.5.24': + resolution: {integrity: sha512-eDl5H57AOpNakGNAkFDH+y7kTqrQpJkZFXhWZQGyx/5Wh7B1uQYvcWkvZi11BDhscPgj8N7XV3oRwiPnx1Vrig==} + + '@vue/compiler-dom@3.5.24': + resolution: {integrity: sha512-1QHGAvs53gXkWdd3ZMGYuvQFXHW4ksKWPG8HP8/2BscrbZ0brw183q2oNWjMrSWImYLHxHrx1ItBQr50I/q2zw==} + + '@vue/compiler-sfc@3.5.24': + resolution: {integrity: sha512-8EG5YPRgmTB+YxYBM3VXy8zHD9SWHUJLIGPhDovo3Z8VOgvP+O7UP5vl0J4BBPWYD9vxtBabzW1EuEZ+Cqs14g==} + + '@vue/compiler-ssr@3.5.24': + resolution: {integrity: sha512-trOvMWNBMQ/odMRHW7Ae1CdfYx+7MuiQu62Jtu36gMLXcaoqKvAyh+P73sYG9ll+6jLB6QPovqoKGGZROzkFFg==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/devtools-api@7.7.8': + resolution: {integrity: sha512-BtFcAmDbtXGwurWUFf8ogIbgZyR+rcVES1TSNEI8Em80fD8Anu+qTRN1Fc3J6vdRHlVM3fzPV1qIo+B4AiqGzw==} + + '@vue/devtools-kit@7.7.8': + resolution: {integrity: sha512-4Y8op+AoxOJhB9fpcEF6d5vcJXWKgHxC3B0ytUB8zz15KbP9g9WgVzral05xluxi2fOeAy6t140rdQ943GcLRQ==} + + '@vue/devtools-shared@7.7.8': + resolution: {integrity: sha512-XHpO3jC5nOgYr40M9p8Z4mmKfTvUxKyRcUnpBAYg11pE78eaRFBKb0kG5yKLroMuJeeNH9LWmKp2zMU5LUc7CA==} + + '@vue/language-core@3.1.4': + resolution: {integrity: sha512-n/58wm8SkmoxMWkUNUH/PwoovWe4hmdyPJU2ouldr3EPi1MLoS7iDN46je8CsP95SnVBs2axInzRglPNKvqMcg==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/reactivity@3.5.24': + resolution: {integrity: sha512-BM8kBhtlkkbnyl4q+HiF5R5BL0ycDPfihowulm02q3WYp2vxgPcJuZO866qa/0u3idbMntKEtVNuAUp5bw4teg==} + + '@vue/runtime-core@3.5.24': + resolution: {integrity: sha512-RYP/byyKDgNIqfX/gNb2PB55dJmM97jc9wyF3jK7QUInYKypK2exmZMNwnjueWwGceEkP6NChd3D2ZVEp9undQ==} + + '@vue/runtime-dom@3.5.24': + resolution: {integrity: sha512-Z8ANhr/i0XIluonHVjbUkjvn+CyrxbXRIxR7wn7+X7xlcb7dJsfITZbkVOeJZdP8VZwfrWRsWdShH6pngMxRjw==} + + '@vue/server-renderer@3.5.24': + resolution: {integrity: sha512-Yh2j2Y4G/0/4z/xJ1Bad4mxaAk++C2v4kaa8oSYTMJBJ00/ndPuxCnWeot0/7/qafQFLh5pr6xeV6SdMcE/G1w==} + peerDependencies: + vue: 3.5.24 + + '@vue/shared@3.5.24': + resolution: {integrity: sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A==} + + alien-signals@3.1.0: + resolution: {integrity: sha512-yufC6VpSy8tK3I0lO67pjumo5JvDQVQyr38+3OHqe6CHl1t2VZekKZ7EKKZSqk0cRmE7U7tfZbpXiKNzuc+ckg==} + + artalk@2.9.1: + resolution: {integrity: sha512-IFo9XqWDalsHy8BsmMA5SSB9bozBa/sBhTm/+O5KwA6DnC95lFKv7C6ScMx/Xa4ue5qSQ7VV5vxRgCh/raohkQ==} + peerDependencies: + marked: ^14.1.0 + + birpc@2.8.0: + resolution: {integrity: sha512-Bz2a4qD/5GRhiHSwj30c/8kC8QGj12nNDwz3D4ErQ4Xhy35dsSDvF+RA/tWpjyU0pdGtSDiEk6B5fBGE1qNVhw==} + + copy-anything@4.0.5: + resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} engines: {node: '>=18'} - dev: false - /detect-libc@2.0.4: - resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + csstype@3.2.2: + resolution: {integrity: sha512-D80T+tiqkd/8B0xNlbstWDG4x6aqVfO52+OlSUNIdkTvmNw0uQpJLeos2J/2XvpyidAFuTPmpad+tUxLndwj6g==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - dev: false - /enhanced-resolve@5.18.2: - resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==} + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} - dependencies: - graceful-fs: 4.2.11 - tapable: 2.2.2 - dev: false - /esbuild@0.25.6: - resolution: {integrity: sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} hasBin: true - requiresBuild: true - optionalDependencies: - '@esbuild/aix-ppc64': 0.25.6 - '@esbuild/android-arm': 0.25.6 - '@esbuild/android-arm64': 0.25.6 - '@esbuild/android-x64': 0.25.6 - '@esbuild/darwin-arm64': 0.25.6 - '@esbuild/darwin-x64': 0.25.6 - '@esbuild/freebsd-arm64': 0.25.6 - '@esbuild/freebsd-x64': 0.25.6 - '@esbuild/linux-arm': 0.25.6 - '@esbuild/linux-arm64': 0.25.6 - '@esbuild/linux-ia32': 0.25.6 - '@esbuild/linux-loong64': 0.25.6 - '@esbuild/linux-mips64el': 0.25.6 - '@esbuild/linux-ppc64': 0.25.6 - '@esbuild/linux-riscv64': 0.25.6 - '@esbuild/linux-s390x': 0.25.6 - '@esbuild/linux-x64': 0.25.6 - '@esbuild/netbsd-arm64': 0.25.6 - '@esbuild/netbsd-x64': 0.25.6 - '@esbuild/openbsd-arm64': 0.25.6 - '@esbuild/openbsd-x64': 0.25.6 - '@esbuild/openharmony-arm64': 0.25.6 - '@esbuild/sunos-x64': 0.25.6 - '@esbuild/win32-arm64': 0.25.6 - '@esbuild/win32-ia32': 0.25.6 - '@esbuild/win32-x64': 0.25.6 - /fdir@6.4.6(picomatch@4.0.2): - resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: picomatch: optional: true - dependencies: - picomatch: 4.0.2 - /fsevents@2.3.3: + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - requiresBuild: true - optional: true - /graceful-fs@4.2.11: + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - dev: false - /jiti@2.4.2: - resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + gsap@3.13.0: + resolution: {integrity: sha512-QL7MJ2WMjm1PHWsoFrAQH/J8wUeqZvMtHO58qdekHpCfhvhSL4gSiz6vJf5EeMP0LOn3ZCprL2ki/gjED8ghVw==} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + instant.page@5.2.0: + resolution: {integrity: sha512-DUSwWyoHFOQnmEwJtg9vzDx8Ef8uNNvTxTmHjd0vN9/XEIb5EQkm/itpZMypoH3dJLJvtkrD97WOCKuMqDdMHQ==} + + is-what@5.5.0: + resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} + engines: {node: '>=18'} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true - dev: false - /lightningcss-darwin-arm64@1.30.1: - resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} + lightgallery@2.9.0: + resolution: {integrity: sha512-58Ud1DyhD2ao58t+kPEqSZrjFxg23tGd5ZKr75erm7q31g5xhUtWUJH3sTUkhHzlyJAKHj5eTrJ37HQRXG4Wbg==} + engines: {node: '>=6.0.0'} + + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] - requiresBuild: true - dev: false - optional: true - /lightningcss-darwin-x64@1.30.1: - resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] - requiresBuild: true - dev: false - optional: true - /lightningcss-freebsd-x64@1.30.1: - resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] - requiresBuild: true - dev: false - optional: true - /lightningcss-linux-arm-gnueabihf@1.30.1: - resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] - requiresBuild: true - dev: false - optional: true - /lightningcss-linux-arm64-gnu@1.30.1: - resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - requiresBuild: true - dev: false - optional: true - /lightningcss-linux-arm64-musl@1.30.1: - resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - requiresBuild: true - dev: false - optional: true - /lightningcss-linux-x64-gnu@1.30.1: - resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - requiresBuild: true - dev: false - optional: true - /lightningcss-linux-x64-musl@1.30.1: - resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - requiresBuild: true - dev: false - optional: true - /lightningcss-win32-arm64-msvc@1.30.1: - resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] - requiresBuild: true - dev: false - optional: true - /lightningcss-win32-x64-msvc@1.30.1: - resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] - requiresBuild: true - dev: false - optional: true - /lightningcss@1.30.1: - resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} engines: {node: '>= 12.0.0'} - dependencies: - detect-libc: 2.0.4 - optionalDependencies: - lightningcss-darwin-arm64: 1.30.1 - lightningcss-darwin-x64: 1.30.1 - lightningcss-freebsd-x64: 1.30.1 - lightningcss-linux-arm-gnueabihf: 1.30.1 - lightningcss-linux-arm64-gnu: 1.30.1 - lightningcss-linux-arm64-musl: 1.30.1 - lightningcss-linux-x64-gnu: 1.30.1 - lightningcss-linux-x64-musl: 1.30.1 - lightningcss-win32-arm64-msvc: 1.30.1 - lightningcss-win32-x64-msvc: 1.30.1 - dev: false - /magic-string@0.30.17: - resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} - dependencies: - '@jridgewell/sourcemap-codec': 1.5.4 - dev: false + lit-element@4.2.1: + resolution: {integrity: sha512-WGAWRGzirAgyphK2urmYOV72tlvnxw7YfyLDgQ+OZnM9vQQBQnumQ7jUJe6unEzwGU3ahFOjuz1iz1jjrpCPuw==} - /minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} - engines: {node: '>=16 || 14 >=14.17'} - dev: false + lit-html@3.3.1: + resolution: {integrity: sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA==} - /minizlib@3.0.2: - resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} + lit@3.3.1: + resolution: {integrity: sha512-Ksr/8L3PTapbdXJCk+EJVB78jDodUMaP54gD24W186zGRARvwrsPfS60wae/SSCTCNZVPd1chXqio1qHQmu4NA==} + + lozad@1.16.0: + resolution: {integrity: sha512-JBr9WjvEFeKoyim3svo/gsQPTkgG/mOHJmDctZ/+U9H3ymUuvEkqpn8bdQMFsvTMcyRJrdJkLv0bXqGm0sP72w==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + marked@14.1.4: + resolution: {integrity: sha512-vkVZ8ONmUdPnjCKc5uTRvmkRbx4EAi2OkTOXmfTDhZz3OFqMNBM1oTTWwTr4HY4uAEojhzPf+Fy8F1DWa3Sndg==} engines: {node: '>= 18'} - dependencies: - minipass: 7.1.2 - dev: false - - /mkdirp@3.0.1: - resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} - engines: {node: '>=10'} hasBin: true - dev: false - /nanoid@3.3.11: + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - /picocolors@1.1.1: + pace-js@1.2.4: + resolution: {integrity: sha512-qnCxtvUoY9yHId0AwMQCVmWltb698GiuVArmDbQzonTu9QCo0SgWUVnX9jB9mi+/FUSWvQULBPxUAAo/kLrh1A==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - /picomatch@4.0.2: - resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} - /postcss@8.5.6: + pinia@3.0.4: + resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==} + peerDependencies: + typescript: '>=4.5.0' + vue: ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + + postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - /rollup@4.44.2: - resolution: {integrity: sha512-PVoapzTwSEcelaWGth3uR66u7ZRo6qhPHc0f2uRO9fX6XDVNrIiGYS0Pj9+R8yIIYSD/mCx2b16Ws9itljKSPg==} + quicklink@3.0.1: + resolution: {integrity: sha512-sAMEpcCUCzjet214qVCm1hzxeF0YLo4wyphkIifeemmofk1vMrc5Sg/iNH32SKAIXqYvO6SPZgEP8obi9Ait9g==} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + react-dom: ^16.8.0 || ^17 || ^18 || ^19 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + + regexparam@1.3.0: + resolution: {integrity: sha512-6IQpFBv6e5vz1QAqI+V4k8P2e/3gRrqfCJ9FI+O1FLQTO+Uz6RXZEZOPmTJ6hlGj7gkERzY5BRCv09whKP96/g==} + engines: {node: '>=6'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rollup@4.53.2: + resolution: {integrity: sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - dependencies: - '@types/estree': 1.0.8 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.44.2 - '@rollup/rollup-android-arm64': 4.44.2 - '@rollup/rollup-darwin-arm64': 4.44.2 - '@rollup/rollup-darwin-x64': 4.44.2 - '@rollup/rollup-freebsd-arm64': 4.44.2 - '@rollup/rollup-freebsd-x64': 4.44.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.44.2 - '@rollup/rollup-linux-arm-musleabihf': 4.44.2 - '@rollup/rollup-linux-arm64-gnu': 4.44.2 - '@rollup/rollup-linux-arm64-musl': 4.44.2 - '@rollup/rollup-linux-loongarch64-gnu': 4.44.2 - '@rollup/rollup-linux-powerpc64le-gnu': 4.44.2 - '@rollup/rollup-linux-riscv64-gnu': 4.44.2 - '@rollup/rollup-linux-riscv64-musl': 4.44.2 - '@rollup/rollup-linux-s390x-gnu': 4.44.2 - '@rollup/rollup-linux-x64-gnu': 4.44.2 - '@rollup/rollup-linux-x64-musl': 4.44.2 - '@rollup/rollup-win32-arm64-msvc': 4.44.2 - '@rollup/rollup-win32-ia32-msvc': 4.44.2 - '@rollup/rollup-win32-x64-msvc': 4.44.2 - fsevents: 2.3.3 - /source-map-js@1.2.1: + route-manifest@1.0.0: + resolution: {integrity: sha512-qn0xJr4nnF4caj0erOLLAHYiNyzqhzpUbgDQcEHrmBoG4sWCDLnIXLH7VccNSxe9cWgbP2Kw/OjME+eH3CeRSA==} + engines: {node: '>= 6'} + + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - /tailwindcss@4.1.11: - resolution: {integrity: sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==} - dev: false + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} - /tapable@2.2.2: - resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} + ssr-window@5.0.1: + resolution: {integrity: sha512-WVXlhQsm54HC+FnJfEbccEgNF7mKXtnFUB8Xn7rx2dsWHOlBdqezdX88Vjh6pVGaa0ZvL+PoSu7rEcBuNmxt6g==} + + superjson@2.2.5: + resolution: {integrity: sha512-zWPTX96LVsA/eVYnqOM2+ofcdPqdS1dAF1LN4TS2/MWuUpfitd9ctTa87wt4xrYnZnkLtS69xpBdSxVBP5Rm6w==} + engines: {node: '>=16'} + + tailwindcss@4.1.17: + resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} - dev: false - /tar@7.4.3: - resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} - engines: {node: '>=18'} - dependencies: - '@isaacs/fs-minipass': 4.0.1 - chownr: 3.0.0 - minipass: 7.1.2 - minizlib: 3.0.2 - mkdirp: 3.0.1 - yallist: 5.0.0 - dev: false + throttles@1.0.1: + resolution: {integrity: sha512-fab7Xg+zELr9KOv4fkaBoe/b3L0GMGLd0IBSCn16GoE/Qx6/OfCr1eGNyEcDU2pUA79qQfZ8kPQWlRuok4YwTw==} + engines: {node: '>=6'} - /tinyglobby@0.2.14: - resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} - dependencies: - fdir: 6.4.6(picomatch@4.0.2) - picomatch: 4.0.2 - /vite@7.0.3: - resolution: {integrity: sha512-y2L5oJZF7bj4c0jgGYgBNSdIu+5HF+m68rn2cQXFbGoShdhV1phX9rbnxy9YXj82aS8MMsCLAAFkRxZeWdldrQ==} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + vite@7.2.2: + resolution: {integrity: sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -896,17 +900,707 @@ packages: optional: true yaml: optional: true + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + vue-router@4.6.3: + resolution: {integrity: sha512-ARBedLm9YlbvQomnmq91Os7ck6efydTSpRP3nuOKCvgJOHNrhRoJDSKtee8kcL1Vf7nz6U+PMBL+hTvR3bTVQg==} + peerDependencies: + vue: ^3.5.0 + + vue-tsc@3.1.4: + resolution: {integrity: sha512-GsRJxttj4WkmXW/zDwYPGMJAN3np/4jTzoDFQTpTsI5Vg/JKMWamBwamlmLihgSVHO66y9P7GX+uoliYxeI4Hw==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.24: + resolution: {integrity: sha512-uTHDOpVQTMjcGgrqFPSb8iO2m1DUvo+WbGqoXQz8Y1CeBYQ0FXf2z1gLRaBtHjlRz7zZUBHxjVB5VTLzYkvftg==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + +snapshots: + + '@artalk/plugin-lightbox@0.2.4(artalk@2.9.1(marked@14.1.4))(lightgallery@2.9.0)': dependencies: - esbuild: 0.25.6 - fdir: 6.4.6(picomatch@4.0.2) - picomatch: 4.0.2 - postcss: 8.5.6 - rollup: 4.44.2 - tinyglobby: 0.2.14 + artalk: 2.9.1(marked@14.1.4) optionalDependencies: + lightgallery: 2.9.0 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.28.5': + dependencies: + '@babel/types': 7.28.5 + + '@babel/types@7.28.5': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@fancyapps/ui@6.1.5': {} + + '@fortawesome/fontawesome-free@7.1.0': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@lit-labs/ssr-dom-shim@1.4.0': {} + + '@lit/reactive-element@2.1.1': + dependencies: + '@lit-labs/ssr-dom-shim': 1.4.0 + + '@material/web@2.4.1': + dependencies: + lit: 3.3.1 + tslib: 2.8.1 + + '@mdui/icons@1.0.3': + dependencies: + '@mdui/shared': 1.0.8 + lit: 3.3.1 + tslib: 2.8.1 + + '@mdui/jq@3.0.3': + dependencies: + ssr-window: 5.0.1 + tslib: 2.8.1 + + '@mdui/shared@1.0.8': + dependencies: + '@lit/reactive-element': 2.1.1 + '@mdui/jq': 3.0.3 + lit: 3.3.1 + ssr-window: 5.0.1 + tslib: 2.8.1 + + '@rolldown/pluginutils@1.0.0-beta.29': {} + + '@rollup/rollup-android-arm-eabi@4.53.2': + optional: true + + '@rollup/rollup-android-arm64@4.53.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.53.2': + optional: true + + '@rollup/rollup-darwin-x64@4.53.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.53.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.53.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.53.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.53.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.53.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.53.2': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.53.2': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.53.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.53.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.53.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.53.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.53.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.53.2': + optional: true + + '@rollup/rollup-openharmony-arm64@4.53.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.53.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.53.2': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.53.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.53.2': + optional: true + + '@tailwindcss/node@4.1.17': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.1.17 + + '@tailwindcss/oxide-android-arm64@4.1.17': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.17': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.17': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.17': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.17': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.17': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.17': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.17': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.17': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.17': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.17': + optional: true + + '@tailwindcss/oxide@4.1.17': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.17 + '@tailwindcss/oxide-darwin-arm64': 4.1.17 + '@tailwindcss/oxide-darwin-x64': 4.1.17 + '@tailwindcss/oxide-freebsd-x64': 4.1.17 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.17 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.17 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.17 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.17 + '@tailwindcss/oxide-linux-x64-musl': 4.1.17 + '@tailwindcss/oxide-wasm32-wasi': 4.1.17 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.17 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.17 + + '@tailwindcss/vite@4.1.17(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2))': + dependencies: + '@tailwindcss/node': 4.1.17 + '@tailwindcss/oxide': 4.1.17 + tailwindcss: 4.1.17 + vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2) + + '@types/estree@1.0.8': {} + + '@types/node@24.10.1': + dependencies: + undici-types: 7.16.0 + + '@types/trusted-types@2.0.7': {} + + '@vitejs/plugin-vue@6.0.1(vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2))(vue@3.5.24(typescript@5.9.3))': + dependencies: + '@rolldown/pluginutils': 1.0.0-beta.29 + vite: 7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2) + vue: 3.5.24(typescript@5.9.3) + + '@volar/language-core@2.4.23': + dependencies: + '@volar/source-map': 2.4.23 + + '@volar/source-map@2.4.23': {} + + '@volar/typescript@2.4.23': + dependencies: + '@volar/language-core': 2.4.23 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vue/compiler-core@3.5.24': + dependencies: + '@babel/parser': 7.28.5 + '@vue/shared': 3.5.24 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.24': + dependencies: + '@vue/compiler-core': 3.5.24 + '@vue/shared': 3.5.24 + + '@vue/compiler-sfc@3.5.24': + dependencies: + '@babel/parser': 7.28.5 + '@vue/compiler-core': 3.5.24 + '@vue/compiler-dom': 3.5.24 + '@vue/compiler-ssr': 3.5.24 + '@vue/shared': 3.5.24 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.6 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.24': + dependencies: + '@vue/compiler-dom': 3.5.24 + '@vue/shared': 3.5.24 + + '@vue/devtools-api@6.6.4': {} + + '@vue/devtools-api@7.7.8': + dependencies: + '@vue/devtools-kit': 7.7.8 + + '@vue/devtools-kit@7.7.8': + dependencies: + '@vue/devtools-shared': 7.7.8 + birpc: 2.8.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.5 + + '@vue/devtools-shared@7.7.8': + dependencies: + rfdc: 1.4.1 + + '@vue/language-core@3.1.4(typescript@5.9.3)': + dependencies: + '@volar/language-core': 2.4.23 + '@vue/compiler-dom': 3.5.24 + '@vue/shared': 3.5.24 + alien-signals: 3.1.0 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + picomatch: 4.0.3 + optionalDependencies: + typescript: 5.9.3 + + '@vue/reactivity@3.5.24': + dependencies: + '@vue/shared': 3.5.24 + + '@vue/runtime-core@3.5.24': + dependencies: + '@vue/reactivity': 3.5.24 + '@vue/shared': 3.5.24 + + '@vue/runtime-dom@3.5.24': + dependencies: + '@vue/reactivity': 3.5.24 + '@vue/runtime-core': 3.5.24 + '@vue/shared': 3.5.24 + csstype: 3.2.2 + + '@vue/server-renderer@3.5.24(vue@3.5.24(typescript@5.9.3))': + dependencies: + '@vue/compiler-ssr': 3.5.24 + '@vue/shared': 3.5.24 + vue: 3.5.24(typescript@5.9.3) + + '@vue/shared@3.5.24': {} + + alien-signals@3.1.0: {} + + artalk@2.9.1(marked@14.1.4): + dependencies: + marked: 14.1.4 + + birpc@2.8.0: {} + + copy-anything@4.0.5: + dependencies: + is-what: 5.5.0 + + csstype@3.2.2: {} + + detect-libc@2.1.2: {} + + enhanced-resolve@5.18.3: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + entities@4.5.0: {} + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + estree-walker@2.0.2: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fsevents@2.3.3: + optional: true + + graceful-fs@4.2.11: {} + + gsap@3.13.0: {} + + hookable@5.5.3: {} + + instant.page@5.2.0: {} + + is-what@5.5.0: {} + + jiti@2.6.1: {} + + lightgallery@2.9.0: {} + + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: + optional: true + + lightningcss-darwin-x64@1.30.2: + optional: true + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: + optional: true + + lightningcss-linux-arm64-musl@1.30.2: + optional: true + + lightningcss-linux-x64-gnu@1.30.2: + optional: true + + lightningcss-linux-x64-musl@1.30.2: + optional: true + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss@1.30.2: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + + lit-element@4.2.1: + dependencies: + '@lit-labs/ssr-dom-shim': 1.4.0 + '@lit/reactive-element': 2.1.1 + lit-html: 3.3.1 + + lit-html@3.3.1: + dependencies: + '@types/trusted-types': 2.0.7 + + lit@3.3.1: + dependencies: + '@lit/reactive-element': 2.1.1 + lit-element: 4.2.1 + lit-html: 3.3.1 + + lozad@1.16.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + marked@14.1.4: {} + + mitt@3.0.1: {} + + muggle-string@0.4.1: {} + + nanoid@3.3.11: {} + + pace-js@1.2.4: {} + + path-browserify@1.0.1: {} + + perfect-debounce@1.0.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + pinia@3.0.4(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3)): + dependencies: + '@vue/devtools-api': 7.7.8 + vue: 3.5.24(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + quicklink@3.0.1: + dependencies: + route-manifest: 1.0.0 + throttles: 1.0.1 + + regexparam@1.3.0: {} + + rfdc@1.4.1: {} + + rollup@4.53.2: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.53.2 + '@rollup/rollup-android-arm64': 4.53.2 + '@rollup/rollup-darwin-arm64': 4.53.2 + '@rollup/rollup-darwin-x64': 4.53.2 + '@rollup/rollup-freebsd-arm64': 4.53.2 + '@rollup/rollup-freebsd-x64': 4.53.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.53.2 + '@rollup/rollup-linux-arm-musleabihf': 4.53.2 + '@rollup/rollup-linux-arm64-gnu': 4.53.2 + '@rollup/rollup-linux-arm64-musl': 4.53.2 + '@rollup/rollup-linux-loong64-gnu': 4.53.2 + '@rollup/rollup-linux-ppc64-gnu': 4.53.2 + '@rollup/rollup-linux-riscv64-gnu': 4.53.2 + '@rollup/rollup-linux-riscv64-musl': 4.53.2 + '@rollup/rollup-linux-s390x-gnu': 4.53.2 + '@rollup/rollup-linux-x64-gnu': 4.53.2 + '@rollup/rollup-linux-x64-musl': 4.53.2 + '@rollup/rollup-openharmony-arm64': 4.53.2 + '@rollup/rollup-win32-arm64-msvc': 4.53.2 + '@rollup/rollup-win32-ia32-msvc': 4.53.2 + '@rollup/rollup-win32-x64-gnu': 4.53.2 + '@rollup/rollup-win32-x64-msvc': 4.53.2 fsevents: 2.3.3 - /yallist@5.0.0: - resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} - engines: {node: '>=18'} - dev: false + route-manifest@1.0.0: + dependencies: + regexparam: 1.3.0 + + source-map-js@1.2.1: {} + + speakingurl@14.0.1: {} + + ssr-window@5.0.1: {} + + superjson@2.2.5: + dependencies: + copy-anything: 4.0.5 + + tailwindcss@4.1.17: {} + + tapable@2.3.0: {} + + throttles@1.0.1: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tslib@2.8.1: {} + + typescript@5.9.3: {} + + undici-types@7.16.0: {} + + vite@7.2.2(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.53.2 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.10.1 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + + vscode-uri@3.1.0: {} + + vue-router@4.6.3(vue@3.5.24(typescript@5.9.3)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.24(typescript@5.9.3) + + vue-tsc@3.1.4(typescript@5.9.3): + dependencies: + '@volar/typescript': 2.4.23 + '@vue/language-core': 3.1.4(typescript@5.9.3) + typescript: 5.9.3 + + vue@3.5.24(typescript@5.9.3): + dependencies: + '@vue/compiler-dom': 3.5.24 + '@vue/compiler-sfc': 3.5.24 + '@vue/runtime-dom': 3.5.24 + '@vue/server-renderer': 3.5.24(vue@3.5.24(typescript@5.9.3)) + '@vue/shared': 3.5.24 + optionalDependencies: + typescript: 5.9.3 diff --git a/public/fonts/material-symbols.css b/public/fonts/material-symbols.css new file mode 100644 index 0000000..2f91d04 --- /dev/null +++ b/public/fonts/material-symbols.css @@ -0,0 +1,25 @@ +/* Material Symbols Outlined - 本地字体 */ +@font-face { + font-family: 'Material Symbols Outlined'; + font-style: normal; + font-weight: 100 700; + font-display: block; + src: url('./MaterialSymbolsOutlined.woff2') format('woff2'); +} + +.material-symbols-outlined { + font-family: 'Material Symbols Outlined'; + font-weight: normal; + font-style: normal; + font-size: 24px; + line-height: 1; + letter-spacing: normal; + text-transform: none; + display: inline-block; + white-space: nowrap; + word-wrap: normal; + direction: ltr; + -webkit-font-feature-settings: 'liga'; + -webkit-font-smoothing: antialiased; + font-feature-settings: 'liga'; +} diff --git a/public/gamepad-solid.svg b/public/gamepad-solid.svg deleted file mode 100644 index 932964d..0000000 --- a/public/gamepad-solid.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/robots.txt b/public/robots.txt index 921b2c2..fb8451d 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,4 +1,3 @@ User-agent: * -Disallow: Allow: / Sitemap: https://searchgal.homes/sitemap.xml \ No newline at end of file diff --git a/public/sitemap.xml b/public/sitemap.xml index 7e431cf..f4b9f58 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -1,6 +1,18 @@ - - - https://searchgal.homes/ - - \ No newline at end of file + + + https://searchgal.homes/ + 2025-11-17 + daily + 1.0 + + https://searchgal.homes/og-image.png + SearchGal - Galgame 聚合搜索 + Galgame 资源聚合搜索引擎 + + + diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..3c5f951 --- /dev/null +++ b/public/sw.js @@ -0,0 +1,267 @@ +/** + * Service Worker for SearchGal + * 提供离线缓存和资源预加载功能 + */ + +const CACHE_VERSION = 'searchgal-v1'; +const CACHE_NAMES = { + static: `${CACHE_VERSION}-static`, + dynamic: `${CACHE_VERSION}-dynamic`, + images: `${CACHE_VERSION}-images`, + api: `${CACHE_VERSION}-api` +}; + +// 需要预缓存的静态资源 +const PRECACHE_URLS = [ + '/', + '/index.html', + '/manifest.webmanifest', + '/favicon.ico' +]; + +// 安装事件 - 预缓存静态资源 +self.addEventListener('install', (event) => { + console.log('[SW] Installing Service Worker...'); + + event.waitUntil( + caches.open(CACHE_NAMES.static) + .then(cache => { + console.log('[SW] Precaching static assets'); + return cache.addAll(PRECACHE_URLS); + }) + .then(() => self.skipWaiting()) + ); +}); + +// 激活事件 - 清理旧缓存 +self.addEventListener('activate', (event) => { + console.log('[SW] Activating Service Worker...'); + + event.waitUntil( + caches.keys() + .then(cacheNames => { + return Promise.all( + cacheNames + .filter(cacheName => { + // 删除不属于当前版本的缓存 + return !Object.values(CACHE_NAMES).includes(cacheName); + }) + .map(cacheName => { + console.log('[SW] Deleting old cache:', cacheName); + return caches.delete(cacheName); + }) + ); + }) + .then(() => self.clients.claim()) + ); +}); + +// Fetch 事件 - 缓存策略 +self.addEventListener('fetch', (event) => { + const { request } = event; + const url = new URL(request.url); + + // 跳过非 GET 请求 + if (request.method !== 'GET') { + return; + } + + // 跳过 Chrome 扩展请求 + if (url.protocol === 'chrome-extension:') { + return; + } + + // API 请求 - Network First 策略 + if (url.hostname === 'api.searchgal.homes') { + event.respondWith(networkFirst(request, CACHE_NAMES.api, 5000)); + return; + } + + // 随机图片 - Cache First 策略 + if (url.hostname === 'api.illlights.com') { + event.respondWith(cacheFirst(request, CACHE_NAMES.images)); + return; + } + + // npm 依赖包 - Cache First 策略 + if (url.pathname.includes('/node_modules/') || + url.pathname.includes('/@vite/') || + url.pathname.includes('/.vite/')) { + event.respondWith(cacheFirst(request, CACHE_NAMES.static)); + return; + } + + // 字体 CDN - Cache First 策略 + if (url.hostname === 'fonts.loli.net' || + url.hostname === 'fonts.googleapis.com' || + url.hostname === 'fonts.gstatic.com' || + url.hostname === 'gstatic.loli.net') { + event.respondWith(cacheFirst(request, CACHE_NAMES.static)); + return; + } + + // CDN 资源 - Cache First 策略 + if (url.hostname === 'registry.npmmirror.com' || + url.hostname === 'cdn.jsdelivr.net' || + url.hostname === 'unpkg.com') { + event.respondWith(cacheFirst(request, CACHE_NAMES.static)); + return; + } + + // VNDB 图片 - Cache First 策略 + if (url.hostname.includes('vndb.org')) { + event.respondWith(cacheFirst(request, CACHE_NAMES.images)); + return; + } + + // 静态资源 - Cache First 策略 + if (request.destination === 'script' || + request.destination === 'style' || + request.destination === 'font' || + request.destination === 'image') { + event.respondWith(cacheFirst(request, CACHE_NAMES.static)); + return; + } + + // HTML 页面 - Network First 策略 + if (request.destination === 'document') { + event.respondWith(networkFirst(request, CACHE_NAMES.dynamic, 3000)); + return; + } + + // 其他请求 - Network First 策略 + event.respondWith(networkFirst(request, CACHE_NAMES.dynamic, 5000)); +}); + +/** + * Cache First 策略 + * 优先从缓存读取,缓存未命中则从网络获取并缓存 + */ +async function cacheFirst(request, cacheName) { + const cache = await caches.open(cacheName); + const cached = await cache.match(request); + + if (cached) { + console.log('[SW] Cache hit:', request.url); + return cached; + } + + try { + console.log('[SW] Cache miss, fetching:', request.url); + const response = await fetch(request); + + // 只缓存成功的响应 + if (response.ok) { + cache.put(request, response.clone()); + } + + return response; + } catch (error) { + console.error('[SW] Fetch failed:', error); + + // 返回离线页面或默认响应 + return new Response('离线模式:无法加载资源', { + status: 503, + statusText: 'Service Unavailable', + headers: new Headers({ + 'Content-Type': 'text/plain; charset=utf-8' + }) + }); + } +} + +/** + * Network First 策略 + * 优先从网络获取,网络失败或超时则从缓存读取 + */ +async function networkFirst(request, cacheName, timeout = 5000) { + const cache = await caches.open(cacheName); + + try { + // 使用 Promise.race 实现超时控制 + const networkPromise = fetch(request); + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('Network timeout')), timeout) + ); + + const response = await Promise.race([networkPromise, timeoutPromise]); + + // 缓存成功的响应 + if (response.ok) { + cache.put(request, response.clone()); + } + + console.log('[SW] Network success:', request.url); + return response; + } catch (error) { + console.log('[SW] Network failed, trying cache:', request.url); + + const cached = await cache.match(request); + + if (cached) { + console.log('[SW] Cache fallback hit:', request.url); + return cached; + } + + // 返回离线页面 + console.error('[SW] No cache available:', error); + return new Response('离线模式:无法加载资源', { + status: 503, + statusText: 'Service Unavailable', + headers: new Headers({ + 'Content-Type': 'text/plain; charset=utf-8' + }) + }); + } +} + +// 消息事件 - 处理来自页面的消息 +self.addEventListener('message', (event) => { + console.log('[SW] Message received:', event.data); + + if (event.data && event.data.type === 'SKIP_WAITING') { + self.skipWaiting(); + } + + if (event.data && event.data.type === 'CLEAR_CACHE') { + event.waitUntil( + caches.keys().then(cacheNames => { + return Promise.all( + cacheNames.map(cacheName => caches.delete(cacheName)) + ); + }) + ); + } +}); + +// 推送通知事件(可选) +self.addEventListener('push', (event) => { + console.log('[SW] Push notification received'); + + const options = { + body: event.data ? event.data.text() : 'SearchGal 有新内容', + icon: '/pwa-192x192.png', + badge: '/favicon-32x32.png', + vibrate: [200, 100, 200], + data: { + dateOfArrival: Date.now(), + primaryKey: 1 + } + }; + + event.waitUntil( + self.registration.showNotification('SearchGal', options) + ); +}); + +// 通知点击事件 +self.addEventListener('notificationclick', (event) => { + console.log('[SW] Notification clicked'); + + event.notification.close(); + + event.waitUntil( + clients.openWindow('/') + ); +}); + diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..0e0b2fc --- /dev/null +++ b/src/App.vue @@ -0,0 +1,275 @@ + + + + + diff --git a/src/api/search.ts b/src/api/search.ts new file mode 100644 index 0000000..5c1f6cf --- /dev/null +++ b/src/api/search.ts @@ -0,0 +1,306 @@ +// API 相关常量和类型 +export const VNDB_API_BASE_URL = "https://api.vndb.org/kana" +export const AI_TRANSLATE_API_URL = "https://ai.searchgal.homes/v1/chat/completions" +export const AI_TRANSLATE_API_KEY = "sk-Md5kXePgq6HJjPa1Cf3265511bEe4e4c888232A0837e371e" +export const AI_TRANSLATE_MODEL = "Qwen/Qwen2.5-32B-Instruct" +export const ENABLE_VNDB_IMAGE_PROXY = true +export const VNDB_IMAGE_PROXY_URL = "https://rpx.searchgal.homes/" + +let isProxyAvailable = false + +export interface SearchResult { + platform: string + title: string + url: string + tags?: string[] +} + +export interface PlatformResult { + name: string + color: 'lime' | 'white' | 'gold' | 'red' + items: SearchResult[] + error: string +} + +export interface VndbInfo { + names: string[] + mainName: string + originalTitle: string + mainImageUrl: string | null + screenshotUrl: string | null + description: string | null + va: any[] + vntags: any[] + play_hours: number + length_minute: number + length_votes: number + length_color: string + book_length: string +} + +/** + * 搜索游戏(流式处理) + * 根据 API 文档: https://github.com/Moe-Sakura/SearchGal/blob/main/docs/api.md + */ +export async function searchGameStream( + searchParams: URLSearchParams, + callbacks: { + onTotal?: (total: number) => void + onProgress?: (current: number, total: number) => void + onPlatformResult?: (data: PlatformResult) => void + onComplete?: () => void + onError?: (error: string) => void + } +) { + try { + // 从 searchParams 中获取 API 地址 + const apiUrl = searchParams.get('api') || 'https://api.searchgal.homes' + const gameName = searchParams.get('game') + const searchMode = searchParams.get('mode') || 'game' + + if (!gameName) { + throw new Error('游戏名称不能为空') + } + + // 根据 API 文档,使用 FormData 构建请求体 + const formData = new FormData() + formData.append('game', gameName) + formData.append('magic', 'true') // 启用魔法搜索以获取更多结果 + + // 根据搜索模式选择 API 端点 + const endpoint = searchMode === 'patch' ? '/patch' : '/gal' + + console.log('[DEBUG] API URL:', `${apiUrl}${endpoint}`) + console.log('[DEBUG] Game:', gameName) + console.log('[DEBUG] Mode:', searchMode) + + const response = await fetch(`${apiUrl}${endpoint}`, { + method: 'POST', + body: formData, + mode: 'cors', + credentials: 'omit', + }).catch(err => { + console.error('[DEBUG] Fetch error:', err) + throw new Error('网络连接失败,请检查网络或API地址') + }) + + if (!response.ok) { + if (response.status === 429) { + throw new Error('请求过于频繁,请稍后再试') + } + const errorData = await response.json().catch(() => ({})) + throw new Error(errorData.error || `HTTP error! status: ${response.status}`) + } + + const reader = response.body?.getReader() + const decoder = new TextDecoder() + + if (!reader) { + throw new Error('无法获取响应流') + } + + let buffer = '' + let totalCount = 0 + + while (true) { + const { done, value } = await reader.read() + + if (done) break + + buffer += decoder.decode(value, { stream: true }) + const lines = buffer.split('\n') + buffer = lines.pop() || '' + + for (const line of lines) { + if (!line.trim()) continue + + try { + const data = JSON.parse(line) + + // 根据 API 文档的响应格式处理数据 + if (data.total !== undefined) { + // 初始事件:{"total": 10} + totalCount = data.total + callbacks.onTotal?.(totalCount) + } else if (data.progress && data.result) { + // 进度事件:{"progress": {...}, "result": {...}} + callbacks.onProgress?.(data.progress.completed, data.progress.total) + + // 转换为我们的格式,保留完整平台信息 + const platformResult: PlatformResult = { + name: data.result.name, + color: data.result.color || 'white', + items: data.result.items.map((item: any) => ({ + platform: data.result.name, + title: item.name, + url: item.url + })), + error: data.result.error || '' + } + + callbacks.onPlatformResult?.(platformResult) + } else if (data.done === true) { + // 完成事件:{"done": true} + callbacks.onComplete?.() + } + } catch (e) { + console.error('解析 JSON 失败:', line, e) + } + } + } + } catch (error) { + console.error('搜索失败:', error) + callbacks.onError?.(error instanceof Error ? error.message : '搜索失败') + } +} + +/** + * 获取 VNDB 数据 + */ +export async function fetchVndbData(gameName: string): Promise { + try { + console.log(`[DEBUG] Fetching VNDB data for: "${gameName}"`) + + const response = await fetch(VNDB_API_BASE_URL, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + filters: ['search', '=', gameName], + fields: 'title, titles{lang,title}, description, image{url,sexual,violence}, screenshots{url,sexual,violence,votecount}, va{character{id,name,original,image{url,sexual,violence},description,traits{id,name,spoiler},vns{id,role,spoiler}}}, length_minutes, length_votes' + }) + }) + + if (!response.ok) { + throw new Error(`VNDB API error: ${response.status}`) + } + + const data = await response.json() + + if (!data.results || data.results.length === 0) { + return null + } + + const result = data.results[0] + + // 提取名称 + let zhName = '' + let jaName = '' + const names: string[] = [] + + if (result.title) names.push(result.title) + + if (result.titles && Array.isArray(result.titles)) { + result.titles.forEach((titleEntry: any) => { + if (titleEntry.title) { + names.push(titleEntry.title) + if (titleEntry.lang === 'zh-Hans' || titleEntry.lang === 'zh-Hant') { + zhName = titleEntry.title + } else if (titleEntry.lang === 'ja') { + jaName = titleEntry.title + } + } + }) + } + + const mainName = zhName || jaName || result.title + const mainImageUrl = result.image?.sexual <= 1 && result.image?.violence === 0 ? result.image.url : null + + const sortedScreenshots = result.screenshots + ? [...result.screenshots].sort((a: any, b: any) => (b.votecount || 0) - (a.votecount || 0)) + : [] + + console.log('[DEBUG] Screenshots:', sortedScreenshots.map((s: any) => ({ url: s.url, sexual: s.sexual, violence: s.violence, votecount: s.votecount }))) + + const screenshotUrl = sortedScreenshots.find((s: any) => s.sexual <= 1 && s.violence === 0)?.url || null + + console.log('[DEBUG] Selected screenshot URL:', screenshotUrl) + + // 计算游戏时长 + const length_minute = result.length_minutes || 0 + const length_votes = result.length_votes || 0 + const play_hours = Math.round(length_minute / 60) + + let book_length = 'Unknown' + let length_color = 'text-gray-500' + + if (play_hours < 2) { + book_length = 'Very short' + length_color = 'text-green-500' + } else if (play_hours < 10) { + book_length = 'Short' + length_color = 'text-blue-500' + } else if (play_hours < 30) { + book_length = 'Medium' + length_color = 'text-yellow-500' + } else if (play_hours < 50) { + book_length = 'Long' + length_color = 'text-orange-500' + } else { + book_length = 'Very long' + length_color = 'text-red-500' + } + + const finalResult: VndbInfo = { + names: [...new Set(names)], + mainName, + originalTitle: result.title, + mainImageUrl, + screenshotUrl, + description: result.description || null, + va: result.va || [], + vntags: [], + play_hours, + length_minute, + length_votes, + length_color, + book_length + } + + // 检查代理并替换 URL + if (ENABLE_VNDB_IMAGE_PROXY) { + await checkProxyAvailability() + console.log('[DEBUG] Proxy available:', isProxyAvailable) + if (isProxyAvailable) { + replaceVndbUrls(finalResult) + console.log('[DEBUG] After proxy replacement:', { + screenshotUrl: finalResult.screenshotUrl, + mainImageUrl: finalResult.mainImageUrl + }) + } + } + + console.log('[DEBUG] Final VNDB result:', finalResult) + + return finalResult + } catch (error) { + console.error('Failed to fetch VNDB data:', error) + return null + } +} + +async function checkProxyAvailability() { + try { + const response = await fetch(VNDB_IMAGE_PROXY_URL, { method: 'HEAD' }) + isProxyAvailable = response.ok + } catch { + isProxyAvailable = false + } +} + +function replaceVndbUrls(obj: any) { + if (!ENABLE_VNDB_IMAGE_PROXY || !isProxyAvailable || obj === null || typeof obj !== 'object') { + return + } + + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + const value = obj[key] + if (typeof value === 'string' && value.startsWith('https://t.vndb.org/')) { + obj[key] = VNDB_IMAGE_PROXY_URL + value + } else if (typeof value === 'object') { + replaceVndbUrls(value) + } + } + } +} + diff --git a/src/components/CommentsModal.vue b/src/components/CommentsModal.vue new file mode 100644 index 0000000..0867fca --- /dev/null +++ b/src/components/CommentsModal.vue @@ -0,0 +1,162 @@ + + + + + diff --git a/src/components/FloatingButtons.vue b/src/components/FloatingButtons.vue new file mode 100644 index 0000000..00a54e9 --- /dev/null +++ b/src/components/FloatingButtons.vue @@ -0,0 +1,138 @@ + + + + + diff --git a/src/components/PageFooter.vue b/src/components/PageFooter.vue new file mode 100644 index 0000000..d0e2c1b --- /dev/null +++ b/src/components/PageFooter.vue @@ -0,0 +1,209 @@ + + + + + + diff --git a/src/components/PlatformNav.vue b/src/components/PlatformNav.vue new file mode 100644 index 0000000..679dac1 --- /dev/null +++ b/src/components/PlatformNav.vue @@ -0,0 +1,186 @@ + + + + + diff --git a/src/components/SearchHeader.vue b/src/components/SearchHeader.vue index cb42df8..06ed01d 100644 --- a/src/components/SearchHeader.vue +++ b/src/components/SearchHeader.vue @@ -1,38 +1,49 @@ + + diff --git a/src/components/SearchResults.vue b/src/components/SearchResults.vue new file mode 100644 index 0000000..e2efb8a --- /dev/null +++ b/src/components/SearchResults.vue @@ -0,0 +1,395 @@ + + + + + diff --git a/src/main.js b/src/main.js deleted file mode 100644 index 86440de..0000000 --- a/src/main.js +++ /dev/null @@ -1,2752 +0,0 @@ -// TailwindCSS v4 (与线上版本一致) -import "tailwindcss"; - -// Pace.js 页面加载进度条 -import 'pace-js'; -import 'pace-js/themes/blue/pace-theme-flash.css'; - -// Animate.css 动画库 -import 'animate.css'; - -// Font Awesome 图标 -import '@fortawesome/fontawesome-free/css/all.min.css'; - -// Artalk 评论系统 -import Artalk from 'artalk'; -import 'artalk/dist/Artalk.css'; - -// Artalk Lightbox 插件 (无需单独导入 CSS) -import { ArtalkLightboxPlugin } from '@artalk/plugin-lightbox'; - -// LightGallery -import lightGallery from 'lightgallery'; -import 'lightgallery/css/lightgallery.css'; - -// Quicklink 预加载 -import { listen } from 'quicklink'; - -// Instant.page 预加载 -import 'instant.page'; - -// Fancybox 样式和脚本 -import { Fancybox } from "@fancyapps/ui"; -import "@fancyapps/ui/dist/fancybox/fancybox.css"; - -// Lozad.js 懒加载 -import lozad from 'lozad'; - -// -- 全局常量与状态 -- -const VNDB_API_BASE_URL = "https://api.vndb.org/kana"; - -// 针对项目调试者与开发者的提示: -// -- 下列预置的 API 接口与 ApiKey 仅用于为 SearchGal.Homes 网站正常访客提供 LLM 服务 -// -- 如需进行项目调试,请修改 AI_TRANSLATE 变量为自己的 API 接口与 ApiKey -// -- 除此以外,ai.searchgal.homes 接口无法为其他任何非正当请求提供 LLM 服务 -const AI_TRANSLATE_API_URL = "https://ai.searchgal.homes/v1/chat/completions"; -const AI_TRANSLATE_API_KEY = - "sk-Md5kXePgq6HJjPa1Cf3265511bEe4e4c888232A0837e371e"; -const AI_TRANSLATE_MODEL = "Qwen/Qwen2.5-32B-Instruct"; - -// -- VNDB 图片代理配置 -- -// 在代理vndb前会先发送请求到VNDB_IMAGE_PROXY_URL, 返回200才会进行代理 -const ENABLE_VNDB_IMAGE_PROXY = true; // 设置为 true 启用vndb图片代理, false 则不启用 -const VNDB_IMAGE_PROXY_URL = "https://rpx.searchgal.homes/"; - -let isProxyAvailable = false; - -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 -let hasShownViewToggleToast = false; // Track if the view toggle toast has been shown -let isViewTransitioning = false; // Lock to prevent spamming the view toggle - -// -- 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"); -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 - -// 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"); - -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", - }); - } -}); - -/** - * 页面加载后初始化 - */ -function initializePage() { - // 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"); - if (apiUrl && customApiInput) { - customApiInput.value = decodeURIComponent(apiUrl); - } - - if (searchBtn) searchBtn.disabled = false; - - const magicCheckbox = document.getElementById("magicAccess"); - if (magicCheckbox) { - magicCheckbox.checked = true; - } - - listen({ priority: true }); - Artalk.init({ - el: "#Comments", - pageKey: "https://searchgal.homes", // Original domain from user's file - server: "https://artalk.saop.cc", - site: "Galgame 聚合搜索", - useBackendConf: false, - plugins: [ArtalkLightboxPlugin], - }); - - // 初始化 Fancybox - Fancybox.bind("[data-fancybox]", { - // Fancybox 配置选项 - Toolbar: { - display: { - left: ["infobar"], - middle: [ - "zoomIn", - "zoomOut", - "toggle1to1", - "rotateCCW", - "rotateCW", - "flipX", - "flipY", - ], - right: ["slideshow", "thumbs", "close"], - }, - }, - Images: { - zoom: true, - Panzoom: { - maxScale: 3, - }, - }, - // 启用键盘导航 - Keyboard: true, - // 启用滚轮缩放 - wheel: "zoom", - // 中文本地化 - l10n: { - CLOSE: "关闭", - NEXT: "下一个", - PREV: "上一个", - MODAL: "您可以使用键盘关闭此模态", - ERROR: "出错了,请稍后再试", - IMAGE_ERROR: "未找到图片", - ELEMENT_NOT_FOUND: "未找到 HTML 元素", - AJAX_NOT_FOUND: "加载 AJAX 时出错:未找到", - AJAX_FORBIDDEN: "加载 AJAX 时出错:禁止访问", - IFRAME_ERROR: "加载页面时出错", - TOGGLE_ZOOM: "切换缩放级别", - TOGGLE_THUMBS: "切换缩略图", - TOGGLE_SLIDESHOW: "切换幻灯片", - TOGGLE_FULLSCREEN: "切换全屏", - DOWNLOAD: "下载", - }, - }); - - // 初始化 Lozad.js 懒加载 - const observer = lozad('.lozad', { - rootMargin: '200px 0px', // 提前 200px 开始加载 - threshold: 0.01, - enableAutoReload: true, - loaded: function(el) { - // 图片加载完成后添加淡入动画 - el.classList.add('opacity-0'); - setTimeout(() => { - el.classList.remove('opacity-0'); - el.classList.add('transition-opacity', 'duration-300', 'opacity-100'); - }, 10); - } - }); - observer.observe(); - - 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"; - 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 - updateAiViewPosition(); // Update AI view position on resize - }, 200) - ); - - window.addEventListener("scroll", debounce(handleScroll, 10)); - - fetchAndDisplayVersion(); // Fetch and display version on page load - checkLlmStatus(); // Check and display LLM status - - const lockViewBtn = document.getElementById("lock-view-btn"); - if (lockViewBtn) { - lockViewBtn.disabled = true; // Disable by default - lockViewBtn.addEventListener("click", handleLockViewToggle); - } - - // Add spacebar listener for view toggle - window.addEventListener("keydown", (e) => { - // Check if spacebar is pressed and the active element is not an input field - if (e.code === "Space" && document.activeElement.tagName !== "INPUT") { - e.preventDefault(); // Prevent default spacebar action (e.g., scrolling) - // Only trigger if the button is actually visible and enabled - if (lockViewBtn && !lockViewBtn.disabled) { - handleLockViewToggle(); - } - } - }); -} - -// 立即调用初始化函数(支持已经加载完的情况) -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initializePage); -} else { - // 文档已经加载完成,立即执行 - initializePage(); -} - -let isViewLocked = false; - -async function handleLockViewToggle() { - if (isViewTransitioning) return; // Prevent action if animation is in progress - isViewTransitioning = true; - - if (isViewLocked) { - document.body.classList.remove("ai-view-active"); - await showMainContent(); - } else { - document.body.classList.add("ai-view-active"); - await hideMainContent(); - } - isViewLocked = !isViewLocked; - lockViewBtn.blur(); - - // Release the lock after the animation duration (approx 1.3s) - setTimeout(() => { - isViewTransitioning = false; - }, 1300); -} - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -/** - * Parses the AI's XML response and renders it into a formatted HTML view. - * @param {string} xmlString The raw XML string from the AI. - */ -function renderAiView(xmlString) { - const aiResponseBox = document.getElementById("ai-response-box"); - if (!aiResponseBox) return; - aiResponseBox.innerHTML = ""; // Always re-render from scratch - - // Helper to get all content within a major block, even if incomplete - const getBlockContent = (tagName, xml) => { - const match = xml.match(new RegExp(`<${tagName}>([\\s\\S]*)`)); - if (!match) return null; - return match[1].split(new RegExp(``))[0]; - }; - - // Helper to extract a single value from a block, can be partial - const getPartialValue = (tagName, block) => { - const match = block.match(new RegExp(`<${tagName}>([\\s\\S]*)`)); - if (!match) return null; - return match[1].split(/<\//)[0]; // Get content until the next closing tag starts - }; - - // Helper to extract all complete values from a block - const getCompleteValues = (tagName, block) => { - const regex = new RegExp(`<${tagName}>([\\s\\S]*?)`, "g"); - return [...block.matchAll(regex)].map((m) => m[1]); - }; - - // 1. Game Description - const descriptionContent = getBlockContent( - "game_description_translated", - xmlString - ); - if (descriptionContent) { - const descriptionWrapper = document.createElement("div"); - descriptionWrapper.className = "mt-8"; // Add some margin top - aiResponseBox.appendChild(descriptionWrapper); - - const titleElement = document.createElement("h2"); - titleElement.className = "text-2xl font-bold text-white mb-4"; - titleElement.innerHTML = `游戏介绍        `; // "游戏介绍" followed by 8 spaces - descriptionWrapper.appendChild(titleElement); - - // Render all complete paragraphs - getCompleteValues("p", descriptionContent).forEach((pText) => { - const pElement = document.createElement("p"); - pElement.classList.add("mt-1"); - pElement.innerHTML = `        ${pText}`; - descriptionWrapper.appendChild(pElement); - }); - // Check for and render an incomplete paragraph at the end - const lastPOpen = descriptionContent.lastIndexOf("

"); - const lastPClosed = descriptionContent.lastIndexOf("

"); - if (lastPOpen > lastPClosed) { - const pElement = document.createElement("p"); - pElement.innerHTML = `        ${descriptionContent.substring( - lastPOpen + 3 - )}`; - descriptionWrapper.appendChild(pElement); - } - - // Append play_time to description - const playTimeContent = getCompleteValues("play_time", descriptionContent)[0]; - if (playTimeContent) { - const playTimeWrapper = document.createElement("div"); - playTimeWrapper.className = "mt-8"; // Add some margin top - playTimeWrapper.id = "play-time-wrapper"; // Add a unique ID - aiResponseBox.appendChild(playTimeWrapper); - - const pElement = document.createElement("p"); - pElement.innerHTML = `        ${playTimeContent}`; - playTimeWrapper.appendChild(pElement); - } - } - - // 3. Tag列表 - const tagsContent = getBlockContent("tag_translated", xmlString); - // console.log("[DEBUG] tagsContent:", tagsContent); // 添加日志 - if (tagsContent) { - const tagsWrapper = document.createElement("div"); - tagsWrapper.className = "mt-8"; // Add some margin top - aiResponseBox.appendChild(tagsWrapper); - - const titleElement = document.createElement("h2"); - titleElement.className = "text-2xl font-bold text-white mb-4"; - titleElement.innerHTML = `Tag        `; // "Tag" followed by 8 spaces - tagsWrapper.appendChild(titleElement); - - const tagsContainer = document.createElement("div"); - tagsContainer.className = "flex flex-col space-y-2"; // Arrange tags vertically with spacing - tagsWrapper.appendChild(tagsContainer); - - const renderTagLine = (tagXmlNamePrimary, tagXmlNameFallback, colorClass, isBold, isItalic, commaBoldWhite) => { - let tagValue = getPartialValue(tagXmlNamePrimary, tagsContent); - if (tagValue === null || tagValue.trim() === "") { - tagValue = getPartialValue(tagXmlNameFallback, tagsContent); - } - // console.log(`[DEBUG] Tag ${tagXmlNamePrimary}/${tagXmlNameFallback} value:`, tagValue); // 添加日志 - if (tagValue !== null && tagValue.trim() !== "") { // 增加对空字符串的检查 - const tagsArray = tagValue.split(',').map(tag => tag.trim()).filter(tag => tag !== ""); // 过滤掉空标签 - // console.log(`[DEBUG] Tag ${tagXmlNamePrimary}/${tagXmlNameFallback} array:`, tagsArray); // 添加日志 - if (tagsArray.length === 0) { // 如果过滤后没有有效标签,则不渲染 - return; - } - const pElement = document.createElement("p"); - // 添加8个空格 - pElement.innerHTML = `        `; - let formattedHtml = ""; - - tagsArray.forEach((tag, index) => { - if (index > 0) { - formattedHtml += `, `; - } - let tagStyle = ""; - if (isBold) tagStyle += "font-bold "; - // 确保所有tag文字都设置为斜体 - tagStyle += "italic "; - formattedHtml += `${tag}`; - }); - pElement.innerHTML += formattedHtml; - tagsContainer.appendChild(pElement); - } - }; - - renderTagLine("tags1", "0", "text-red-500", true, true, true); // 粗体红色, 逗号白色粗体, 强制斜体 - renderTagLine("tags2", "1", "text-orange-500", true, true, true); // 斜粗体橙色, 逗号白色粗体, 强制斜体 - renderTagLine("tags3", "2", "text-green-500", true, true, false); // 普通字体绿色, 逗号普通白色, 强制斜体, 强制粗体 - renderTagLine("tags4", "3", "text-blue-500", true, true, false); // 斜体蓝色, 逗号普通白色, 强制斜体, 强制粗体 - } - - // 4. Characters - const charactersContent = getBlockContent("characters_translated", xmlString); - if (charactersContent) { - const charactersWrapper = document.createElement("div"); - charactersWrapper.className = "mt-8"; - aiResponseBox.appendChild(charactersWrapper); - - const roleMap = { - main: { title: "主人公", order: 1 }, - primary: { title: "主要角色", order: 2 }, - side: { title: "次要角色", order: 3 }, - appears: { title: "配角", order: 4 }, - }; - - const characterBlocks = charactersContent.split("").slice(1); - const characters = characterBlocks - .map((block) => ({ - imageUrl: getPartialValue("image_url", block), - translatedName: getPartialValue("translated_name", block), - originalName: getPartialValue("original_name", block), - description: getPartialValue("description", block), - role: getPartialValue("role", block), - })) - .filter((c) => c.translatedName !== null && c.role !== null); - - const groupedCharacters = characters.reduce((acc, char) => { - (acc[char.role] = acc[char.role] || []).push(char); - return acc; - }, {}); - - const sortedRoles = Object.keys(groupedCharacters).sort( - (a, b) => (roleMap[a]?.order || 99) - (roleMap[b]?.order || 99) - ); - - sortedRoles.forEach((role) => { - const roleInfo = roleMap[role]; - if (roleInfo && groupedCharacters[role].length > 0) { - if (!charactersWrapper.querySelector(`[data-role-title="${role}"]`)) { - const titleElement = document.createElement("h2"); - titleElement.className = "text-2xl font-bold text-white mt-8 mb-4"; - titleElement.textContent = roleInfo.title; - titleElement.dataset.roleTitle = role; - charactersWrapper.appendChild(titleElement); - } - } - - groupedCharacters[role].forEach((char, index) => { - if (index > 0) { - const hr = document.createElement("hr"); - hr.className = "my-4 border-gray-600"; - charactersWrapper.appendChild(hr); - } - const charContainer = document.createElement("div"); - charContainer.className = "flex items-center my-4"; - if (char.imageUrl) { - const imgElement = document.createElement("img"); - imgElement.src = char.imageUrl; - imgElement.alt = char.translatedName; - imgElement.className = - "w-24 h-32 object-cover rounded-lg mr-4 flex-shrink-0"; - charContainer.appendChild(imgElement); - } - const textContainer = document.createElement("div"); - textContainer.className = "flex-grow"; - const nameElement = document.createElement("div"); - nameElement.innerHTML = `${ - char.translatedName - } (${ - char.originalName || "" - })`; - textContainer.appendChild(nameElement); - if (char.description !== null) { - const descElement = document.createElement("p"); - descElement.className = "mt-1"; - descElement.innerHTML = `        ${char.description}`; - textContainer.appendChild(descElement); - } - charContainer.appendChild(textContainer); - charactersWrapper.appendChild(charContainer); - }); - }); - } - - // 5. Summary - const summaryContent = getBlockContent("summary_and_insight", xmlString); - if (summaryContent) { - const questionContent = getPartialValue("question", summaryContent); - if (questionContent !== null) { - const summaryElement = document.createElement("p"); - // Check if it's the fallback message and style accordingly - if (questionContent === "AI翻译服务当前不可用,以上为原始信息。") { - summaryElement.className = "mt-16 text-lg text-red-500 font-bold"; - } else { - summaryElement.className = "mt-16 text-lg italic"; - } - summaryElement.innerHTML = `        ${questionContent}`; - aiResponseBox.appendChild(summaryElement); - } - } -} - -/** - * Dynamically sets the position and size of the AI view container - * to match the horizontal alignment of the main content container. - */ -function updateAiViewPosition() { - const mainContent = document.getElementById("scrollable-content"); - const aiContainer = document.getElementById("ai-response-container"); - - if (mainContent && aiContainer) { - const rect = mainContent.getBoundingClientRect(); - aiContainer.style.left = `${rect.left}px`; - aiContainer.style.width = `${rect.width}px`; - aiContainer.style.height = "80vh"; // Keep a fixed height - } -} - -function createAliasButton() { - if (document.getElementById("alias-btn")) return; - - const extLinksContainer = document.getElementById("ext-links-container"); - if (!extLinksContainer) return; - - const aliasBtn = document.createElement("button"); - aliasBtn.id = "alias-btn"; - // Removed title attribute and focus ring styles, changed to purple - aliasBtn.className = - "w-10 h-10 rounded-lg bg-purple-600 text-white flex items-center justify-center shadow-lg hover:bg-purple-700 transition-all duration-300 transform hover:scale-110"; - aliasBtn.innerHTML = ''; - - const aliasTooltip = document.getElementById("alias-tooltip"); - const aliasList = document.getElementById("alias-list"); - - if (!aliasTooltip || !aliasList) return; - - let hideTimeout; - - const showTooltip = () => { - clearTimeout(hideTimeout); - const aliases = vndbInfo.names.filter((name) => name !== vndbInfo.mainName); - - if (aliases.length > 0) { - aliasList.innerHTML = ""; - aliases.forEach((alias) => { - const li = document.createElement("li"); - li.textContent = alias; - aliasList.appendChild(li); - }); - - const btnRect = aliasBtn.getBoundingClientRect(); - aliasTooltip.style.left = `${btnRect.right + 5}px`; // Reduced gap - aliasTooltip.style.top = `${btnRect.top}px`; - aliasTooltip.classList.remove("hidden"); - setTimeout(() => (aliasTooltip.style.opacity = "1"), 10); - } - }; - - const hideTooltip = () => { - hideTimeout = setTimeout(() => { - aliasTooltip.style.opacity = "0"; - setTimeout(() => aliasTooltip.classList.add("hidden"), 300); - }, 300); - }; - - aliasBtn.addEventListener("mouseenter", showTooltip); - aliasBtn.addEventListener("mouseleave", hideTooltip); - aliasTooltip.addEventListener("mouseenter", () => clearTimeout(hideTimeout)); - aliasTooltip.addEventListener("mouseleave", hideTooltip); - - extLinksContainer.appendChild(aliasBtn); -} - -async function hideMainContent() { - document.body.classList.add("noscroll"); - const mainContainer = document.getElementById("main-container"); - const siteNavigation = document.getElementById("siteNavigation"); - const scrollToTopBtn = document.getElementById("scrollToTopBtn"); - const scrollToCommentsBtn = document.getElementById("scrollToCommentsBtn"); - const vndbInfoPanel = document.getElementById("vndb-info-panel"); - const vndbDescription = document.getElementById("vndb-description"); - const aiResponseContainer = document.getElementById("ai-response-container"); - const commentsSection = document.getElementById("Comments"); - const extLinksContainer = document.getElementById("ext-links-container"); - - // 1. Hide main container and buttons - mainContainer.style.opacity = "0"; - mainContainer.style.pointerEvents = "none"; // Allow clicks to pass through - scrollToTopBtn.style.opacity = "0"; - scrollToCommentsBtn.style.opacity = "0"; - if (extLinksContainer) extLinksContainer.style.opacity = "0"; - if (commentsSection) { - commentsSection.style.opacity = "0"; - commentsSection.style.pointerEvents = "none"; - } - - // Delay hiding the site navigation to sync with the main container - setTimeout(() => { - siteNavigation.style.opacity = "0"; - }, 0); - - // 2. Hide game description - vndbDescription.style.opacity = "0"; - - // Wait for the 0.5s fade-out animations to complete - await sleep(500); - - // 3. Slide down game info - // Set a fixed height before animation to prevent wobbling from translateY(-50%) - const infoPanelRect = vndbInfoPanel.getBoundingClientRect(); - vndbInfoPanel.style.height = `${infoPanelRect.height}px`; - vndbInfoPanel.style.transform = "translateY(47vh) translateY(-50%)"; - - // Wait for the 0.8s slide animation to complete - await sleep(800); - - // 4. Position, populate, and show AI response container - updateAiViewPosition(); - if (vndbInfo.aiRawResponse) { - renderAiView(vndbInfo.aiRawResponse); - } - aiResponseContainer.style.opacity = "1"; - aiResponseContainer.style.pointerEvents = "auto"; -} - -async function showMainContent() { - document.body.classList.remove("noscroll"); - const mainContainer = document.getElementById("main-container"); - const siteNavigation = document.getElementById("siteNavigation"); - const scrollToTopBtn = document.getElementById("scrollToTopBtn"); - const scrollToCommentsBtn = document.getElementById("scrollToCommentsBtn"); - const vndbInfoPanel = document.getElementById("vndb-info-panel"); - const vndbDescription = document.getElementById("vndb-description"); - const aiResponseContainer = document.getElementById("ai-response-container"); - const commentsSection = document.getElementById("Comments"); - - // 1. Hide AI response container - aiResponseContainer.style.opacity = "0"; - aiResponseContainer.style.pointerEvents = "none"; - - // Wait for AI container to fade out (0.5s) - await sleep(500); - - // 2. Slide up game info - vndbInfoPanel.style.transform = "translateY(0)"; - // Restore original height behavior after animation - setTimeout(() => { - vndbInfoPanel.style.height = ""; - }, 800); // Match transition duration - - // Wait for slide up animation (0.8s) - await sleep(800); - - // 3. Show game description and main container first - vndbDescription.style.opacity = "1"; - mainContainer.style.opacity = "1"; - mainContainer.style.pointerEvents = ""; // Restore pointer events - if (commentsSection) { - commentsSection.style.opacity = "1"; - commentsSection.style.pointerEvents = ""; - } - - // 4. Then, show the platform buttons and other floating buttons at the same time - siteNavigation.style.opacity = "1"; - scrollToTopBtn.style.opacity = "1"; - scrollToCommentsBtn.style.opacity = "1"; - const extLinksContainer = document.getElementById("ext-links-container"); - if (extLinksContainer) extLinksContainer.style.opacity = "1"; -} - -/** - * Calculates the width of the scrollbar. - * @returns {number} The width of the scrollbar in pixels. - */ -function showToast(message, duration = 4000) { - const toast = document.getElementById("toast-notification"); - if (!toast) return; - - const toastText = toast.querySelector("span"); - if (toastText) { - toastText.textContent = message; - } - - toast.classList.remove("hide"); - toast.classList.add("show"); - - setTimeout(() => { - toast.classList.remove("show"); - toast.classList.add("hide"); - }, duration); -} - -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", - "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"); - // If we are in mobile view, find and remove the vndb-info-panel if it exists - const panel = document.getElementById("vndb-info-panel"); - if (panel) { - panel.remove(); - } - } 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(); - - // 让输入框失焦 - const searchInput = searchForm.querySelector('input[name="game"]'); - if (searchInput) { - searchInput.blur(); - } - - if (lockViewBtn) { - lockViewBtn.disabled = true; // Disable on new search - lockViewBtn.classList.remove("visible"); - } - - const currentTime = Date.now(); - if (lastSearchTime && currentTime - lastSearchTime < SEARCH_COOLDOWN_MS) { - const timeLeft = Math.ceil( - (SEARCH_COOLDOWN_MS - (currentTime - lastSearchTime)) / 1000 - ); - showError(`请等待 ${timeLeft} 秒后再搜索。`); - return; - } - - 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"); - const mainContainer = document.getElementById("main-container"); - if (mainContainer) { - mainContainer.classList.remove("bg-white"); - mainContainer.classList.add("bg-white/95"); - } - if (backgroundLayer) { - backgroundLayer.style.backgroundImage = "none"; - } - } - - 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 searchMode = formData.get("searchMode"); - const magic = document.getElementById("magicAccess")?.checked || false; - const customApi = customApiInput ? customApiInput.value.trim() : ""; - - if (!gameName) { - showError("游戏名称不能为空"); - setLoadingState(false); - return; - } - - // Set last search time AFTER the check and BEFORE setting loading state - // This ensures cooldown is applied even if the search takes time to initialize. - lastSearchTime = Date.now(); - setLoadingState(true); - - const searchParams = { - gameName, - magic, - patchMode: searchMode === "patch", - customApi, - }; - - let totalTasks = 0; - - // Start the main search stream. It will run in the background. - searchGameStream(searchParams, { - onTotal: (total) => { - totalTasks = total; - fetchVndbData(gameName) - .then(async (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 = { - names: vndbResult.names, // Add the names array to vndbInfo - mainName: vndbResult.mainName, - originalTitle: vndbResult.originalTitle, // Store the original title - mainImageUrl: vndbResult.mainImageUrl, - screenshotUrl: vndbResult.screenshotUrl, - description: vndbResult.description, - va: vndbResult.va, - vntags: vndbResult.vntags, // Add vntags to vndbInfo - play_hours: vndbResult.play_hours, - length_minute: vndbResult.length_minute, - length_votes: vndbResult.length_votes, - length_color: vndbResult.length_color, - book_length: vndbResult.book_length, - aiRawResponse: "", // Add a field to store the full AI response - }; - console.log( - "[DEBUG] Stored VNDB Info with characters:", - vndbInfo - ); - // 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.originalTitle) { - await fetchVndbExtLinks(vndbInfo.originalTitle); - } - - // --- Trigger Animation (only if panel exists) --- - if (vndbInfoPanel) { - if (vndbInfo.mainImageUrl && vndbImage) { - vndbImage.src = vndbInfo.mainImageUrl; - vndbImage.classList.remove("hidden"); - } else if (vndbImage) { - vndbImage.classList.add("hidden"); - } - - if (vndbInfo.description && vndbDescription) { - // New: Clear previous content and show the element - vndbDescription.innerHTML = ""; - vndbDescription.classList.remove("hidden"); - // New: Call the streaming translation function - translateAndStreamDescription( - vndbInfo.description, - vndbInfo.va, - vndbInfo.vntags, - vndbInfo.play_hours, - vndbInfo.length_minute, - vndbInfo.length_votes, - vndbInfo.length_color, - vndbInfo.book_length - ); - } else if (vndbDescription) { - vndbDescription.classList.add("hidden"); - } - - if (vndbInfo.mainName && vndbTitle) { - vndbTitle.textContent = vndbInfo.mainName; - vndbTitle.classList.remove("hidden"); - createAliasButton(); // Create the alias button - } else if (vndbTitle) { - vndbTitle.classList.add("hidden"); - } - - if (vndbInfo.screenshotUrl && backgroundLayer) { - const img = new Image(); - img.onload = () => { - backgroundLayer.style.backgroundImage = `url(${vndbInfo.screenshotUrl})`; - document.body.classList.add("vndb-mode"); - const mainContainer = - document.getElementById("main-container"); - if (mainContainer) { - mainContainer.classList.remove("bg-white/95"); - mainContainer.classList.add("bg-white"); - } - }; - img.src = vndbInfo.screenshotUrl; - } else { - backgroundLayer.style.backgroundImage = "none"; - document.body.classList.remove("vndb-mode"); - const mainContainer = - document.getElementById("main-container"); - if (mainContainer) { - mainContainer.classList.remove("bg-white"); - mainContainer.classList.add("bg-white/95"); - } - } - - // Show panel only if there is something to display - const hasContent = - vndbInfo.mainImageUrl || - vndbInfo.description || - vndbInfo.mainName; - vndbInfoPanel.classList.toggle("hidden", !hasContent); - // Hide ext links until the second VNDB fetch is complete - const extLinksContainer = document.getElementById( - "ext-links-container" - ); - if (extLinksContainer) { - extLinksContainer.style.opacity = "0"; - extLinksContainer.style.pointerEvents = "none"; - } - } - isFirstSearch = false; - } 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) { - const percent = Math.min( - 100, - Math.round((progress.completed / totalTasks) * 100) - ); - progressBar.style.width = `${percent}%`; - } - if (searchBtnText) { - searchBtnText.textContent = `进度: ${progress.completed} / ${totalTasks}`; - } - }, - onResult: (result) => { - const platformData = { - ...result, - items: result.items || [], - currentPage: 1, - }; - 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 - }, - 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) => { - // This catch is for the searchGameStream promise itself - console.error("Error in searchGameStream:", err); - if (searchBtn.disabled) { - setLoadingState(false); - showError(err.message || "流式搜索发生未知错误"); - } - }); - }, - { 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 { - 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( - "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 { - // This 'item' is not defined in this scope. It should be result.items[0] if you intend to get from the first item. - // Given the logic, this block might not be correctly extracting home/domain for the platform as a whole. - // If the intention is to show a home/domain for the platform based on its results, - // you might need to find a representative URL or re-evaluate. - // For now, I'll assume 'item' was meant to be the first item in 'result.items' if available. - const url = new URL(result.items[0].url); // Assuming first item's URL for platform domain - home = url.origin; - domain = url.hostname; - } catch (e) { - // console.error("Error parsing URL for platform home/domain:", e); - home = "#"; // Fallback if URL parsing fails - domain = "未知域名"; - } - } - - const allTags = { - // 访问限制 - NoReq: '无需登录', - Rep: '需回复', - Login: '需登录', - LoginRep: '需登录+回复', - LoginPay: '需登录+投币', - - // 资源类型/网盘 - SuDrive: '自建盘', - NoSplDrive: '不限速网盘', - SplDrive: '限速网盘', - MixDrive: '混合网盘', - BTmag: 'BT磁力', - - // 特殊 - magic: '需要魔法', - - // 旧版兼容 - gold: '需要魔法', - lime: '无需登录', - red: '错误', - white: '需要登录', - default: - '综合', - }; - - const tagTooltips = { - NoReq: "该平台无需登录即可下载", - Login: "该平台需要注册/登录账户才能下载", - Rep: "该平台需要简单评论后才能下载,无需登录", - LoginRep: "该平台需要登录并回复帖子才能下载", - LoginPay: "该平台需要登录并投币(积分)帖子才能下载,币(积分)可通过免费途径获取(如签到等),步骤较为繁琐", - SuDrive: "资源存储在自建的服务器或对象存储上,下载速度最快", - NoSplDrive: "资源存储在不会限制下载速度的第三方网盘,如Onedrive/Mega/123网盘等", - SplDrive: "资源存储在会限制非会员下载速度的第三方网盘,如百度网盘/夸克网盘/飞猫盘等,速度最慢", - MixDrive: "资源可能存储在限速或非限速的第三方网盘中,通常受限于普通用户发布的资源", - BTmag: "资源通过BT磁力链接或种子文件分享,需要第三方BT磁力下载工具(如qBittorrent/迅雷),不推荐新手下载", - magic: "访问该平台需要使用网络代理服务,请确保已经启用魔法代理,否则无法进入该网站", - gold: "访问该网站需要使用网络代理服务。", - lime: "该资源无需登录即可访问。", - red: "搜索该平台时发生错误", - white: "该资源需要登录网站账户才能访问。", - default: "这是一个综合性平台或资源。", - }; - - const generateTagHtml = (tagName) => { - const html = allTags[tagName]; - const tooltip = tagTooltips[tagName]; - if (!html) return ""; - - // Create a temporary DOM element to safely add the title attribute - const tempEl = document.createElement('div'); - tempEl.innerHTML = html.trim(); - const span = tempEl.firstChild; - - if (span) { - if (tooltip) { - span.setAttribute('title', tooltip); - } - // Add classes for vertical alignment adjustment - span.classList.add('inline-block', 'transform', '-translate-y-px'); - } - - return span ? span.outerHTML : ""; - }; - - let tagHtml = ""; - if (result.tags && Array.isArray(result.tags) && result.tags.length > 0 && colorKey != "red") { - tagHtml = result.tags - .map(generateTagHtml) - .join(""); - } else { - tagHtml = generateTagHtml(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 = `
    - ${paginatedItems - .map((item) => { - let decodedPath = ""; - try { - const urlObj = new URL(item.url); - decodedPath = decodeURIComponent( - urlObj.pathname + (urlObj.search || "") - ); - } catch {} - const displayName = - item.name === ".bzEmpty" || !item.name - ? "未知文件" - : item.name; - - // 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 `
  1. - - ${displayName} - - - ${decodedPath} -
  2. `; - }) - .join("")} -
`; - } else if (!result.error) { - itemsHtml = '
暂无结果
'; - } - - let paginationHtml = ""; - if (result.items.length > ITEMS_PER_PAGE) { - const totalPages = Math.ceil(result.items.length / ITEMS_PER_PAGE); - const prevDisabled = currentPage === 1; - const nextDisabled = currentPage === totalPages; - paginationHtml = `
- - - ${currentPage} / ${totalPages} - - -
`; - } - - const cardHtml = ` - - ${ - result.error - ? `
${result.error}
` - : "" - } - ${itemsHtml} - ${paginationHtml} - `; - - const cardElement = document.createElement("div"); - cardElement.dataset.platform = result.name; - cardElement.id = result.name; - cardElement.className = `mb-6 rounded-xl shadow-lg rounded-t-2xl ${ - color.bg - } border ${color.border} overflow-hidden ${ - withAnimation ? "animate__animated animate__fadeInUp animate__faster" : "" - }`; - cardElement.innerHTML = cardHtml; - - 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 - - siteNavigationDiv.style.width = ""; - resultsDiv.style.marginTop = ""; - resultsDiv.style.marginBottom = ""; // Clear bottom margin as well - siteNavigationDiv.classList.remove("fixed-nav"); // Ensure fixed-nav class is removed - - errorDiv.textContent = ""; - if (progressBar) { - progressBar.style.width = "0%"; - progressBar.style.opacity = "0"; - progressBar.classList.remove("animate__fadeIn", "animate__fadeOut"); - } - adjustBodyPaddingForScrollbar(); // Adjust padding after clearing UI -} - -function showError(message) { - errorDiv.textContent = message; - errorDiv.classList.add( - "animate__animated", - "animate__shakeX", - "animate__faster" - ); - errorDiv.addEventListener( - "animationend", - function handler() { - this.classList.remove( - "animate__animated", - "animate__shakeX", - "animate__faster" - ); - this.removeEventListener("animationend", handler); - }, - { once: true } - ); -} - -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) { - searchIcon.dataset.originalClass = searchIcon.className; - } - searchBtn.disabled = true; - searchBtn.classList.add("active"); - searchIcon.className = "fas fa-spinner fa-spin"; - if (searchBtnText) searchBtnText.textContent = "正在初始化..."; - if (progressBar) { - progressBar.style.opacity = "1"; - progressBar.classList.remove("hidden", "animate__fadeOut"); - progressBar.classList.add("animate__animated", "animate__fadeIn"); - } - } else { - searchBtn.disabled = false; - searchBtn.classList.remove("active"); - searchIcon.className = originalIconClass; - - const updateCooldownText = () => { - const currentTime = Date.now(); - const timeLeft = Math.ceil( - (SEARCH_COOLDOWN_MS - (currentTime - lastSearchTime)) / 1000 - ); - - if (timeLeft > 0 && lastSearchTime !== 0) { - if (searchBtnText) searchBtnText.textContent = `冷却中 (${timeLeft}s)`; - searchBtn.disabled = true; // Ensure button is disabled during cooldown - } else { - 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"); - progressBar.addEventListener( - "animationend", - function handler() { - this.style.opacity = "0"; - this.classList.remove("animate__animated", "animate__fadeOut"); - this.removeEventListener("animationend", handler); - }, - { once: true } - ); - } - } -} - -async function searchGameStream( - { - gameName, - magic = false, - patchMode = false, - customApi = "", - }, - { onTotal, onProgress, onResult, onDone, onError } -) { - const defaultSite = "cfapi.searchgal.homes"; - const protocol = "https"; - - let baseUrl = ""; - - if (customApi) { - try { - const urlObj = new URL(customApi); - baseUrl = urlObj.origin; - } catch (e) { - onError(new Error("自定义 API 地址无效。请确保其是有效的 URL。")); - return; - } - } else { - baseUrl = `${protocol}://${defaultSite}`; - } - - const url = patchMode ? `${baseUrl}/patch` : `${baseUrl}/gal`; - - const formData = new FormData(); - formData.append("game", gameName); - formData.append("magic", true); - - try { - const response = await fetch(url, { - method: "POST", - body: formData, - }); - - if (!response.ok) { - const errorData = await response.json().catch(() => ({})); - throw new Error( - errorData.error || `HTTP 错误!状态码: ${response.status}` - ); - } - - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - let buffer = ""; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split("\n"); - buffer = lines.pop(); - - for (const line of lines) { - if (line.trim() === "") continue; - try { - const data = JSON.parse(line); - if (data.total && onTotal) { - onTotal(data.total); - } else if (data.progress) { - 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); - } - } - } - } catch (error) { - if (onError) { - onError(error); - } else { - throw error; - } - } -} - -/** - * Recursively traverses an object or array and removes spoiler tags from all string values. - * @param {any} obj The object or array to process. - */ -function removeSpoilersRecursively(obj) { - if (obj === null || typeof obj !== "object") { - return; - } - - for (const key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key)) { - const value = obj[key]; - if (typeof value === "string") { - // Also trims whitespace that might be left after removal - obj[key] = value - .replace(/\[spoiler\][\s\S]*?\[\/spoiler\]/g, "") - .trim(); - } else if (typeof value === "object") { - removeSpoilersRecursively(value); - } - } - } -} - -/** - * Fetches data from the VNDB API. - * @param {string} gameName The name of the game to search for. - * @returns {Promise} An object with names and other info, or null. - */ -async function fetchVndbData(gameName) { - console.log(`[DEBUG] Fetching VNDB data for: "${gameName}"`); - const url = `${VNDB_API_BASE_URL}/vn`; - const body = { - filters: ["search", "=", gameName], - sort: "searchrank", - fields: - "titles.title, titles.lang, aliases, title, length_minutes, length_votes, image.url, image.sexual, image.violence, image.votecount, screenshots.url, screenshots.sexual, screenshots.violence, screenshots.votecount, description, va.character.name, va.character.description, va.character.original, va.character.image.url, va.character.image.sexual, va.character.image.violence, va.character.traits.name, va.character.traits.spoiler, va.character.vns.role, va.character.vns.spoiler, tags.spoiler, tags.name, tags.rating, tags.category", - }; - - 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); - - // Remove all spoiler tags from the response data - removeSpoilersRecursively(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 - const originalTitle = result.title; - if (originalTitle) { - names.push(originalTitle); - } - - // Collect all alternative titles - let mainName = originalTitle || ""; // 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 && result.image.sexual <= 1 && result.image.violence === 0 - ? result.image.url - : null; - const sortedScreenshots = result.screenshots - ? [...result.screenshots].sort((a, b) => b.votecount - a.votecount) - : []; - const screenshotUrl = - sortedScreenshots.find((s) => s.sexual <= 1 && s.violence === 0)?.url || - null; - const description = result.description || null; - - // --- Process VA/Character Data --- - if (result.va && Array.isArray(result.va)) { - console.log("[DEBUG] Processing character data (VA)..."); - - // 1. Extract characters - let characters = result.va - .map((item) => item.character) - .filter(Boolean) - .filter((char) => { - if (!char.vns || char.vns.length === 0) return false; - const gameAppearance = char.vns.find((vn) => vn.id === result.id); - return gameAppearance && gameAppearance.spoiler === 0; - }); - - // 2. Define role weights and sort characters - const roleWeights = { main: 1, primary: 2, side: 3, appears: 4 }; - characters.sort((a, b) => { - const roleA = a.vns.find((vn) => vn.id === result.id)?.role; - const roleB = b.vns.find((vn) => vn.id === result.id)?.role; - const weightA = roleWeights[roleA] || Infinity; - const weightB = roleWeights[roleB] || Infinity; - return weightA - weightB; - }); - - console.log("[DEBUG] Sorted characters by role:", characters); - - // 3. Process each character (traits, names, images, and new role logic) - characters.forEach((character) => { - // Process traits into a single 'tag' string - if (character.traits && Array.isArray(character.traits)) { - character.tag = character.traits - .filter( - (trait) => - trait.spoiler === 0 && trait.name !== "Not Sexually Involved" - ) - .map((trait) => trait.name) - .join(", "); - delete character.traits; - } else { - character.tag = ""; - } - - // Rename 'original' to 'originalName' - if (character.original) { - character.originalName = character.original; - delete character.original; - } - - // Delete the original character ID - delete character.id; - - // Process character image based on safety flags - if (character.image && character.image.url) { - if (character.image.sexual <= 1 && character.image.violence === 0) { - character.image = character.image.url; - } else { - character.image = ""; - } - } else { - character.image = ""; - } - - // Add 'role' and delete 'vns' - const gameAppearance = character.vns.find((vn) => vn.id === result.id); - if (gameAppearance) { - character.role = gameAppearance.role; - } else { - character.role = "unknown"; // Should not happen due to earlier filter - } - delete character.vns; - }); - - // 4. Filter for unique characters after processing - const uniqueCharacters = characters.reduce((acc, current) => { - if (!acc.some((item) => item.name === current.name)) { - acc.push(current); - } - return acc; - }, []); - - result.va = uniqueCharacters; // Replace original va with processed, sorted, and unique characters - console.log( - "[DEBUG] Final processed character data (before assigning to finalResult):", - result.va - ); - } - // --- End of VA Processing --- - - // 处理标签 - // 处理标签 - let vntags = []; - if (result.tags) { - const filteredAndSortedTags = result.tags - .filter((tag) => tag.spoiler === 0 && tag.category !== 'ero') - .sort((a, b) => b.rating - a.rating); - - const vntagsRating3 = []; - const vntagsRating2 = []; - const vntagsRating1 = []; - const vntagsRating0 = []; - - filteredAndSortedTags.forEach(tag => { - if (tag.rating === 3) { - vntagsRating3.push(tag.name); - } else if (tag.rating < 3 && tag.rating >= 2) { - vntagsRating2.push(tag.name); - } else if (tag.rating < 2 && tag.rating >= 1) { - vntagsRating1.push(tag.name); - } else if (tag.rating < 1) { - vntagsRating0.push(tag.name); - } - }); - vntags = [vntagsRating3, vntagsRating2, vntagsRating1, vntagsRating0]; - } - - const length_minutes = result.length_minutes || 0; - const length_votes = result.length_votes || 0; - const play_hours = Math.floor(length_minutes / 60); - const length_minute = length_minutes % 60; - - let length_color = "red"; - if (play_hours < 10) { - length_color = "green"; - } else if (play_hours < 30) { - length_color = "blue"; - } else if (play_hours < 40) { - length_color = "orange"; - } - - let book_length = "overlength"; - if (play_hours < 10) { - book_length = "Short"; - } else if (play_hours < 30) { - book_length = "Medium"; - } else if (play_hours < 40) { - book_length = "Long"; - } - - const finalResult = { - names: [...new Set(names)], // Return unique names - mainName, - originalTitle, - mainImageUrl, - screenshotUrl, - description, - va: result.va, // Pass processed character data - vntags: vntags, - play_hours, - length_minute, - length_votes, - length_color, - book_length, - }; - - 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); - - // Recursively replace all vndb URLs if proxy is enabled - // Recursively replace all vndb URLs if proxy is enabled and available - if (ENABLE_VNDB_IMAGE_PROXY) { - await checkProxyAvailability(); - if (isProxyAvailable) { - replaceVndbUrls(finalResult); - console.log( - "[DEBUG] Final VNDB result object after URL replacement:", - finalResult - ); - } - } - - return finalResult; - } catch (error) { - console.error("Failed to fetch or process VNDB data:", error); - return null; // Return null on any error - } -} - -/** - * Recursively traverses an object or array and replaces all instances of - * "https://t.vndb.org/" with a proxy URL in string values. - * @param {any} obj The object or array to process. - */ -/** - * Checks if the proxy server is available by sending a HEAD request. - * Updates the global `isProxyAvailable` state. - */ -async function checkProxyAvailability() { - try { - const response = await fetch(VNDB_IMAGE_PROXY_URL, { method: "HEAD" }); - if (response.ok) { - isProxyAvailable = true; - console.log("[DEBUG] Proxy server is available."); - } else { - isProxyAvailable = false; - console.warn( - `[DEBUG] Proxy server check failed with status: ${response.status}` - ); - } - } catch (error) { - isProxyAvailable = false; - console.error("[DEBUG] Proxy server check failed with error:", error); - } -} - -function replaceVndbUrls(obj) { - if ( - !ENABLE_VNDB_IMAGE_PROXY || - !isProxyAvailable || - obj === null || - typeof obj !== "object" - ) { - return; - } - - for (const key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key)) { - const value = obj[key]; - if (typeof value === "string") { - if (value.startsWith("https://t.vndb.org/")) { - obj[key] = VNDB_IMAGE_PROXY_URL + value; - } - } else if (typeof value === "object") { - replaceVndbUrls(value); // Recurse into nested objects/arrays - } - } - } -} - -/** - * 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/Wrangler-API/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 () { - const context = this; - const args = arguments; - clearTimeout(timeout); - 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 = `${VNDB_API_BASE_URL}/release`; - const body = { - filters: ["vn", "=", ["search", "=", mainName]], - fields: "title, official, 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) { - // Pass the entire results array to the rendering function - renderExtLinkButtons(data.results); - } - } catch (error) { - console.error("Error fetching VNDB external links:", error); - } -} - -/** - * Renders categorized external link buttons based on release data. - * @param {object[]} releases A list of release objects from VNDB. - */ -function renderExtLinkButtons(releases) { - const allUrls = releases.flatMap( - (release) => release.extlinks?.map((link) => link.url) || [] - ); - console.log("获取到的所有VNDB链接:", allUrls); - - const container = document.getElementById("ext-links-container"); - if (!container) return; - container.innerHTML = ""; // Clear previous buttons - - // --- URL Categorization & Deduplication --- - const steamUrls = [ - ...new Set( - allUrls.filter((url) => url.includes("store.steampowered.com")) - ), - ]; - const dlsiteUrls = [ - ...new Set(allUrls.filter((url) => url.includes("dlsite"))), - ]; - - // Create a set of already categorized URLs for quick lookup - const categorizedUrls = new Set([...steamUrls, ...dlsiteUrls]); - - // Find official URLs from releases marked as 'official'. - // This logic also prevents duplicates within the official list and against other lists. - const officialUrls = releases - .filter((release) => release.official) - .flatMap((release) => release.extlinks?.map((link) => link.url) || []) - .filter((url) => { - if (categorizedUrls.has(url)) { - return false; // Exclude if already in Steam or DLsite - } - categorizedUrls.add(url); // Add to the set to avoid duplicates in "Other" - return true; - }); - - // "Other" URLs are everything not yet categorized, with duplicates removed. - const otherUrls = [ - ...new Set( - allUrls.filter( - (url) => !categorizedUrls.has(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 = ``; - - 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 opacity-0 transition-opacity duration-300"; - 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-white hover:bg-gray-100 p-1 rounded"; - link.innerHTML = ` ${ - new URL(url).hostname - }`; - popup.appendChild(link); - }); - buttonWrapper.appendChild(popup); - - let hideTimeout; - const showPopup = () => { - clearTimeout(hideTimeout); - popup.classList.remove("hidden"); - setTimeout(() => (popup.style.opacity = "1"), 10); - }; - const hidePopup = () => { - hideTimeout = setTimeout(() => { - popup.style.opacity = "0"; - setTimeout(() => popup.classList.add("hidden"), 300); - }, 300); - }; - - button.addEventListener("mouseenter", showPopup); - button.addEventListener("mouseleave", hidePopup); - popup.addEventListener("mouseenter", () => clearTimeout(hideTimeout)); - popup.addEventListener("mouseleave", hidePopup); - } - - buttonWrapper.appendChild(button); - container.appendChild(buttonWrapper); - } - }); - - // Use a short timeout to ensure the browser has rendered the initial hidden state - // before applying the transition, allowing the fade-in to work correctly. - setTimeout(() => { - container.style.transition = "opacity 0.5s ease-in-out"; - container.style.opacity = "1"; - container.style.pointerEvents = "auto"; - }, 10); // A small delay is enough -} - -/** - * Fetches a translated version of the description from an AI service and streams it. - * @param {string} description The original description text. - */ -async function translateAndStreamDescription( - description, - characters, - vntags, - play_hours, - length_minute, - length_votes, - length_color, - book_length -) { - if (!vndbDescription) return; - - // Show the lock view button with a ripple effect when AI response starts - const lockViewBtn = document.getElementById("lock-view-btn"); - if (lockViewBtn && !lockViewBtn.classList.contains("visible")) { - lockViewBtn.classList.add("visible"); - // Create 3 staggered ripples for a more noticeable effect - for (let i = 0; i < 3; i++) { - setTimeout(() => { - const ripple = document.createElement("span"); - ripple.className = "ripple"; - lockViewBtn.appendChild(ripple); - setTimeout(() => ripple.remove(), 1000); // Animation duration is 1s - }, i * 500); // Stagger the start of each ripple - } - // Show the one-time toast notification - if (!hasShownViewToggleToast && !isMobileView) { - const toastMessage = "按下空格键进入游戏详情视图"; - showToast(toastMessage); - hasShownViewToggleToast = true; - // hasShownViewToggleToast = true; // This line is redundant - } - } - - let characterInfoString = ""; - try { - if (isMobileView) { - console.log("[DEBUG] Mobile view detected, skipping AI translation."); - vndbDescription.innerHTML = `

${description.replace( - /\n/g, - "
" - )}

`; - return; - } - - const userLanguage = navigator.language || "zh-CN"; - - if (characters && characters.length > 0) { - characterInfoString = characters - .map((c) => `Name: ${c.name}, Role: ${c.role}`) - .join("\n"); - } - - const messages = [ - { - role: "system", - content: ` -作为专业的视觉小说游戏内容翻译、格式化与主题分析专家,请将提供的视觉小说游戏信息(游戏介绍, 游戏标签, 人物信息)精确翻译成'${userLanguage}',按指定XML格式输出,并提出一个引人深思的总结问题。 - -输入处理要求: - -1.游戏介绍: - 格式化移除:翻译前,彻底移除所有非内容性格式代码/标签(HTML, Markdown, XML等),只保留纯文本内容。 - 人名校对:介绍中出现的人名,按4.人物信息规则翻译。 - 来源移除:移除介绍末尾的来源引用(如“来源:XXX”)。 - -2.游玩时长: - 模板化:按照给定的模板给出翻译后相同格式的输出。 - -3.游戏标签: - 专业领域化:标签的解释局限于视觉小说游戏中 - 简短且精确:翻译后的tag释义必须简短,避免冗长以及模糊不清的描述,但是不得翻译成设计剧透的内容 - 唯一性:每个tag只需要给出唯一的翻译后释义 - -4.人物信息: - 描述翻译:将每个人物描述翻译成'${userLanguage}'。为空时,根据角色的其他信息尝试生成该角色的人物介绍,严禁输出不雅内容。 - 人名翻译:优先使用'中文名'或'日文名'作为'original_name'。如无,则翻译原始非中日名称。 - 日译中直译:人名翻译(日译中)时,请直译日文名,而非英/罗马名。 - 全部输出:确保输出所有角色信息。 - - -输出结构与格式要求: - 最终输出必须是严格的XML格式,所有内容包裹在根元素''中。 - -1.根元素: \`\` - -2.游戏介绍部分: - \`\`:包括\`

\`与\`\`。 - \`

\`: 翻译后的游戏介绍,游戏介绍的每个段落用\`

\`分段,但禁止其他复杂HTML/样式标签。 - \`\`:包含翻译后的模板文本。模板如下: - \`{book_length}, 平均游玩时长为 {play_hours}小时{length_minute}分钟, 共{length_votes}名玩家参与投票\` - 其中的标签与代码严禁翻译或改动。 - 以中文为例, {book_length}应该被根据响应的值翻译为: "短篇", "中篇", "长篇", "超长篇"。 - 如果length_minute=0, 模板则无需输出游玩分钟。 - -3.Tag列表: - \`\`:包含\`\`~\`\`子元素。 - \`\`: 翻译用户输入的tags1里面的所有tag,tag之间必须使用\`, \`分隔。若tags1为空,则该标签为空标签。 - \`\`~\`\`:同上,翻译输入的tags2~tags4里面的所有tag。 - -4.人物信息列表: - \`\`:包含所有\`\`子元素。 - 每个\`\`包含以下子元素(严格按顺序): - \`\`:若有则包含URL,否则输出\`\`空标签。 - \`\`:人物翻译后的姓名。 - \`\`:原始值(main/primary/side/appears),勿翻译。 - \`\`:原始姓名(优先中文/日文名), 如无, 则输出未翻译的主姓名。 - \`\`:人物描述。结合游戏介绍与角色'tag'('tag'本身勿输出),灵活撰写以突出特点,严禁输出不雅内容。 - -5.总结与思考: - \`\`:包含\`\`子元素。 - \`\`:基于翻译后的故事与人物,提出一个引人深思/好奇的问题。勿用总结性开场白(如“总体来说”)。 - -最终输出约束: - 纯XML内容,严格遵循XML标准及结构,所有标签正确嵌套/闭合。勿含任何额外文字/评论。使用\`\`\`xml \`\`\`的代码块来包裹。`, - }, - { - role: "user", - content: `Game Description:\n${description}\n\nPlay Time:\nplay_hours: ${play_hours}\nlength_minute: ${length_minute}\nlength_votes: ${length_votes}\nlength_color: ${length_color}\nbook_length: ${book_length}\n\nGame Tags:\n${( - vntags || [] - ) - .map((tags, index) => { - const tagContent = - tags && tags.length > 0 ? tags.join(", ") : ""; - return `Tag${index + 1}: ${tagContent}`; - }) - .join("\n")}\n\nCharacter Details:\n${JSON.stringify( - characters, - null, - 2 - )}`, - }, - ]; - - console.log("Sending AI request with messages:", messages); - - const response = await fetch(AI_TRANSLATE_API_URL, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${AI_TRANSLATE_API_KEY}`, - }, - body: JSON.stringify({ - model: AI_TRANSLATE_MODEL, - messages: messages, - stream: true, - }), - }); - - if (response.ok) { - const reader = response.body.getReader(); - const decoder = new TextDecoder("utf-8"); - let buffer = ""; - let isFirstChunk = true; - vndbInfo.aiRawResponse = ""; // Reset before streaming - - while (true) { - const { done, value } = await reader.read(); - if (done) { - console.log("Stream finished."); - console.log("AI响应原文:", vndbInfo.aiRawResponse); - break; - } - - buffer += decoder.decode(value, { stream: true }); - let boundary = buffer.indexOf("\n"); - while (boundary !== -1) { - const line = buffer.substring(0, boundary).trim(); - buffer = buffer.substring(boundary + 1); - - if (line.startsWith("data: ")) { - const jsonStr = line.substring(6); - if (jsonStr !== "[DONE]") { - try { - const chunk = JSON.parse(jsonStr); - if ( - chunk.choices && - chunk.choices[0].delta && - chunk.choices[0].delta.content - ) { - if (isFirstChunk) { - if (lockViewBtn) lockViewBtn.disabled = false; - isFirstChunk = false; - } - vndbInfo.aiRawResponse += chunk.choices[0].delta.content; - renderAiView(vndbInfo.aiRawResponse); - - const startIndexDesc = vndbInfo.aiRawResponse.indexOf( - "" - ); - if (startIndexDesc !== -1) { - const endIndexDesc = vndbInfo.aiRawResponse.indexOf( - "" - ); - let contentToRenderDesc = - endIndexDesc !== -1 - ? vndbInfo.aiRawResponse.substring( - startIndexDesc + - "".length, - endIndexDesc - ) - : vndbInfo.aiRawResponse.substring( - startIndexDesc + - "".length - ); - // Remove tag and its content from description - const playTimeRegex = /[\s\S]*?<\/play_time>/; - contentToRenderDesc = contentToRenderDesc.replace(playTimeRegex, ""); - contentToRenderDesc = contentToRenderDesc.replace( - /

/g, - "

        " - ); - vndbDescription.innerHTML = contentToRenderDesc; - // Show the one-time toast notification here - if (!hasShownViewToggleToast && !isMobileView) { - const toastMessage = "按下空格键进入游戏详情视图"; - showToast(toastMessage); - hasShownViewToggleToast = true; - } - } - } - } catch (e) { - // Ignore JSON parsing errors - } - } - } - boundary = buffer.indexOf("\n"); - } - } - } else { - // Fallback for non-200 responses - throw new Error(`HTTP error! status: ${response.status}`); - } - } catch (error) { - console.error("Error or fallback during AI translation:", error); - vndbDescription.innerHTML = description.replace(/\n/g, "
"); - - // Construct fallback XML for AI view - let fallbackXml = ""; - fallbackXml += `

${description.replace( - /\n/g, - "

" - )}

`; - - // Construct fallback play_time content - let fallbackBookLength = "Overlength"; - if (play_hours < 10) { - fallbackBookLength = "Short"; - } else if (play_hours < 30) { - fallbackBookLength = "Medium"; - } else if (play_hours < 40) { - fallbackBookLength = "Long"; - } - - let fallbackPlayTimeContent = `${fallbackBookLength}, Average play time is ${play_hours} hours`; - if (length_minute !== 0) { - fallbackPlayTimeContent += ` ${length_minute} minutes`; - } - fallbackPlayTimeContent += `, with ${length_votes} players voting`; - - fallbackXml += `${fallbackPlayTimeContent}`; - fallbackXml += `
`; - - // Add tags to fallback XML - if (vntags) { - fallbackXml += ""; - const tagsXml = Object.entries(vntags) - .map(([key, value]) => `<${key}>${value}`) - .join(""); - fallbackXml += tagsXml; - fallbackXml += ""; - } - - // Add tags to fallback XML - if (vntags && vntags.length > 0) { - fallbackXml += ""; - vntags.forEach((tagArray, index) => { - if (index < 4) { // Only process up to tags4 - const tagName = `tags${index + 1}`; - const tagValue = tagArray.join(", "); - fallbackXml += `<${tagName}>${tagValue}`; - } - }); - fallbackXml += ""; - } - - if (characters && characters.length > 0) { - fallbackXml += ""; - characters.forEach((c) => { - fallbackXml += ""; - fallbackXml += `${c.image || ""}`; - fallbackXml += `${c.name}`; - fallbackXml += `${c.role}`; - fallbackXml += `${ - c.originalName || c.name - }`; - fallbackXml += `${ - c.description || "无可用描述" - }`; - fallbackXml += ""; - }); - fallbackXml += ""; - } - fallbackXml += - "AI翻译服务当前不可用,以上为原始信息。"; - fallbackXml += "
"; - - vndbInfo.aiRawResponse = fallbackXml; - renderAiView(vndbInfo.aiRawResponse); - if (lockViewBtn) lockViewBtn.disabled = false; - } -} - -async function checkLlmStatus() { - const statusBtn = document.getElementById("llm-status-btn"); - const statusText = document.getElementById("llm-status-text"); - - if (!statusBtn || !statusText) { - return; - } - - try { - const response = await fetch( - "https://api.pulsetic.com/public/status/status.searchgal.homes", - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ password: null }), - } - ); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - const data = await response.json(); - const llmMonitor = data.data.monitors.find( - (monitor) => monitor.name === "后端搜索 API (无实际搜索)" - ); - - if (llmMonitor) { - statusBtn.classList.remove("bg-white", "text-gray-500"); - statusBtn.classList.add("text-white"); - if (llmMonitor.status === "online") { - statusBtn.classList.add("bg-green-500"); - statusText.textContent = "正常"; - } else { - statusBtn.classList.add("bg-red-700"); - statusText.textContent = "异常"; - } - } else { - throw new Error("LLM monitor not found"); - } - } catch (error) { - console.error("Error fetching LLM status:", error); - statusBtn.classList.remove("bg-white", "text-gray-500"); - statusBtn.classList.add("bg-gray-500", "text-white"); - statusText.textContent = "未知"; - } -} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..3527715 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,98 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import App from './App.vue' +import router from './router' + +// GSAP 插件 +import gsap from 'gsap' +import { ScrollToPlugin } from 'gsap/ScrollToPlugin' + +// 注册 GSAP 插件 +gsap.registerPlugin(ScrollToPlugin) + +// Font Awesome +import '@fortawesome/fontawesome-free/css/all.min.css' + +// Material 3 Web Components +import '@material/web/button/filled-button.js' +import '@material/web/button/outlined-button.js' +import '@material/web/button/text-button.js' +import '@material/web/textfield/filled-text-field.js' +import '@material/web/textfield/outlined-text-field.js' +import '@material/web/fab/fab.js' +import '@material/web/dialog/dialog.js' +import '@material/web/list/list.js' +import '@material/web/list/list-item.js' +import '@material/web/chips/chip-set.js' +import '@material/web/chips/assist-chip.js' +import '@material/web/chips/filter-chip.js' +import '@material/web/progress/linear-progress.js' +import '@material/web/progress/circular-progress.js' +import '@material/web/icon/icon.js' +import '@material/web/iconbutton/icon-button.js' +// Card 组件在 labs 目录(实验性功能) +import '@material/web/labs/card/elevated-card.js' +import '@material/web/labs/card/filled-card.js' +import '@material/web/labs/card/outlined-card.js' +import '@material/web/divider/divider.js' +import '@material/web/ripple/ripple.js' + +// Pace.js 页面加载进度条 +import Pace from 'pace-js' +import 'pace-js/themes/blue/pace-theme-flash.css' + +// 配置 Pace.js:禁用自动启动 +Pace.options = { + ajax: false, // 不监听 AJAX 请求 + document: false, // 不监听文档加载 + eventLag: false, // 不监听事件延迟 + elements: false, // 不监听元素 + restartOnPushState: false, + restartOnRequestAfter: false +} + +// Artalk 评论系统 +import 'artalk/dist/Artalk.css' + +// LightGallery +import 'lightgallery/css/lightgallery.css' + +// Fancybox +import "@fancyapps/ui/dist/fancybox/fancybox.css" + +// Instant.page 预加载 +import 'instant.page' + +const app = createApp(App) +const pinia = createPinia() + +app.use(pinia) +app.use(router) + +app.mount('#app') + +// 注册 Service Worker (PWA) +if ('serviceWorker' in navigator) { + window.addEventListener('load', () => { + navigator.serviceWorker.register('/sw.js', { scope: '/' }) + .then(registration => { + console.log('[SW] Service Worker registered:', registration.scope) + + // 检查更新 + registration.addEventListener('updatefound', () => { + const newWorker = registration.installing + console.log('[SW] New Service Worker found, installing...') + + newWorker?.addEventListener('statechange', () => { + if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { + console.log('[SW] New content available, please refresh') + // 可以在这里显示更新提示 + } + }) + }) + }) + .catch(error => { + console.error('[SW] Service Worker registration failed:', error) + }) + }) +} diff --git a/src/router/index.ts b/src/router/index.ts new file mode 100644 index 0000000..0a5140f --- /dev/null +++ b/src/router/index.ts @@ -0,0 +1,16 @@ +import { createRouter, createWebHistory } from 'vue-router' +import HomeView from '@/views/HomeView.vue' + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { + path: '/', + name: 'home', + component: HomeView + } + ] +}) + +export default router + diff --git a/src/stores/search.ts b/src/stores/search.ts new file mode 100644 index 0000000..3e39178 --- /dev/null +++ b/src/stores/search.ts @@ -0,0 +1,123 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' + +export interface VndbInfo { + names: string[] + mainName: string + originalTitle: string + mainImageUrl: string | null + screenshotUrl: string | null + description: string | null + va: any[] + vntags: any[] + play_hours: number + length_minute: number + length_votes: number + length_color: string + book_length: string +} + +export interface SearchResult { + platform: string + title: string + url: string + tags?: string[] +} + +export interface PlatformData { + name: string + color: 'lime' | 'white' | 'gold' | 'red' + items: SearchResult[] + error: string + currentPage: number + itemsPerPage: number +} + +export const useSearchStore = defineStore('search', () => { + // 状态 + const searchQuery = ref('') + const searchMode = ref<'game' | 'patch'>('game') + const customApi = ref('') + const platformResults = ref>(new Map()) + const vndbInfo = ref(null) + const isSearching = ref(false) + const searchProgress = ref({ current: 0, total: 0 }) + const errorMessage = ref('') + const isFirstSearch = ref(true) + const lastSearchTime = ref(0) + const isCommentsModalOpen = ref(false) + + // 计算属性 + const hasResults = computed(() => platformResults.value.size > 0) + const isVndbMode = computed(() => !!vndbInfo.value?.screenshotUrl) + const searchDisabled = computed(() => { + const now = Date.now() + const COOLDOWN_MS = 30 * 1000 + return isSearching.value || (now - lastSearchTime.value < COOLDOWN_MS) + }) + + // 方法 + function clearResults() { + platformResults.value.clear() + vndbInfo.value = null + errorMessage.value = '' + } + + function setSearchQuery(query: string) { + searchQuery.value = query + } + + function setSearchMode(mode: 'game' | 'patch') { + searchMode.value = mode + } + + function setCustomApi(api: string) { + customApi.value = api + } + + function setPlatformResult(name: string, data: PlatformData) { + // 确保有分页信息 + if (!data.currentPage) data.currentPage = 1 + if (!data.itemsPerPage) data.itemsPerPage = 10 + platformResults.value.set(name, data) + } + + function setPlatformPage(platformName: string, page: number) { + const platform = platformResults.value.get(platformName) + if (platform) { + platform.currentPage = page + platformResults.value.set(platformName, { ...platform }) + } + } + + function toggleCommentsModal() { + isCommentsModalOpen.value = !isCommentsModalOpen.value + } + + return { + // 状态 + searchQuery, + searchMode, + customApi, + platformResults, + vndbInfo, + isSearching, + searchProgress, + errorMessage, + isFirstSearch, + lastSearchTime, + isCommentsModalOpen, + // 计算属性 + hasResults, + isVndbMode, + searchDisabled, + // 方法 + clearResults, + setSearchQuery, + setSearchMode, + setCustomApi, + setPlatformResult, + setPlatformPage, + toggleCommentsModal + } +}) diff --git a/src/types/pace.d.ts b/src/types/pace.d.ts new file mode 100644 index 0000000..552e619 --- /dev/null +++ b/src/types/pace.d.ts @@ -0,0 +1,23 @@ +// Pace.js 类型声明 +interface PaceOptions { + ajax?: boolean + document?: boolean + eventLag?: boolean + elements?: boolean + restartOnPushState?: boolean + restartOnRequestAfter?: boolean +} + +interface Pace { + start(): void + stop(): void + restart(): void + on(event: string, callback: () => void): void + off(event: string, callback?: () => void): void + options: PaceOptions +} + +interface Window { + Pace?: Pace +} + diff --git a/src/utils/sitemap.ts b/src/utils/sitemap.ts new file mode 100644 index 0000000..1cb081f --- /dev/null +++ b/src/utils/sitemap.ts @@ -0,0 +1,104 @@ +/** + * Sitemap 生成工具 + * 用于生成动态的 sitemap.xml + */ + +export interface SitemapUrl { + loc: string; + lastmod?: string; + changefreq?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never'; + priority?: number; +} + +export function generateSitemap(urls: SitemapUrl[]): string { + const xmlHeader = ''; + const urlsetOpen = ''; + const urlsetClose = ''; + + const urlEntries = urls.map(url => { + let entry = ' \n'; + entry += ` ${escapeXml(url.loc)}\n`; + + if (url.lastmod) { + entry += ` ${url.lastmod}\n`; + } + + if (url.changefreq) { + entry += ` ${url.changefreq}\n`; + } + + if (url.priority !== undefined) { + entry += ` ${url.priority.toFixed(1)}\n`; + } + + entry += ' '; + return entry; + }).join('\n'); + + return `${xmlHeader}\n${urlsetOpen}\n${urlEntries}\n${urlsetClose}`; +} + +function escapeXml(unsafe: string): string { + return unsafe.replace(/[<>&'"]/g, (c) => { + switch (c) { + case '<': return '<'; + case '>': return '>'; + case '&': return '&'; + case "'": return '''; + case '"': return '"'; + default: return c; + } + }); +} + +/** + * 获取当前日期的 ISO 格式字符串(用于 lastmod) + */ +export function getCurrentDate(): string { + return new Date().toISOString().split('T')[0]; +} + +/** + * 默认的 sitemap URLs + */ +export function getDefaultSitemapUrls(): SitemapUrl[] { + const baseUrl = 'https://searchgal.homes'; + const currentDate = getCurrentDate(); + + return [ + { + loc: `${baseUrl}/`, + lastmod: currentDate, + changefreq: 'daily', + priority: 1.0 + }, + { + loc: `${baseUrl}/about`, + lastmod: currentDate, + changefreq: 'monthly', + priority: 0.8 + }, + { + loc: `${baseUrl}/rss.xml`, + lastmod: currentDate, + changefreq: 'weekly', + priority: 0.5 + } + ]; +} + +/** + * 下载 sitemap.xml 文件 + */ +export function downloadSitemap(content: string, filename: string = 'sitemap.xml'): void { + const blob = new Blob([content], { type: 'application/xml' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); +} + diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue new file mode 100644 index 0000000..534dd01 --- /dev/null +++ b/src/views/HomeView.vue @@ -0,0 +1,26 @@ + + + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..37a1965 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "preserve", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + /* Vue */ + "types": ["node"], + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"], + "references": [{ "path": "./tsconfig.node.json" }] +} + diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..41cdb7d --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["vite.config.ts"] +} + diff --git a/vite.config.ts b/vite.config.ts index f77e08b..501d6c7 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,9 +1,238 @@ import { defineConfig } from "vite"; +import vue from "@vitejs/plugin-vue"; import tailwindcss from "@tailwindcss/vite"; +import { VitePWA } from 'vite-plugin-pwa'; +import { fileURLToPath, URL } from 'node:url'; + export default defineConfig({ server: { - host: "0.0.0.0", + host: "localhost", port: 5500, }, - plugins: [tailwindcss()], + plugins: [ + vue({ + template: { + compilerOptions: { + // 将 Material Web 组件标签视为自定义元素 + isCustomElement: (tag) => tag.startsWith('md-') + } + } + }), + tailwindcss(), + VitePWA({ + registerType: 'autoUpdate', + includeAssets: ['favicon.ico', 'robots.txt', 'apple-touch-icon.png'], + manifest: { + name: 'SearchGal - Galgame 聚合搜索', + short_name: 'SearchGal', + description: 'Galgame 资源聚合搜索引擎,支持多站点搜索、游戏信息查询、补丁下载', + theme_color: '#ec4899', + background_color: '#fffbfe', + display: 'standalone', + orientation: 'portrait', + scope: '/', + start_url: '/', + icons: [ + { + src: '/pwa-192x192.png', + sizes: '192x192', + type: 'image/png', + purpose: 'any' + }, + { + src: '/pwa-512x512.png', + sizes: '512x512', + type: 'image/png', + purpose: 'any' + }, + { + src: '/pwa-maskable-192x192.png', + sizes: '192x192', + type: 'image/png', + purpose: 'maskable' + }, + { + src: '/pwa-maskable-512x512.png', + sizes: '512x512', + type: 'image/png', + purpose: 'maskable' + } + ], + categories: ['entertainment', 'utilities'], + screenshots: [ + { + src: '/screenshot-wide.png', + sizes: '1280x720', + type: 'image/png', + form_factor: 'wide' + }, + { + src: '/screenshot-narrow.png', + sizes: '750x1334', + type: 'image/png', + form_factor: 'narrow' + } + ] + }, + workbox: { + // 自动缓存所有构建产物(包括所有 npm 依赖) + globPatterns: [ + '**/*.{js,css,html,ico,png,svg,jpg,jpeg,gif,webp,woff,woff2,ttf,eot}' + ], + // 包含 node_modules 中的依赖 + globDirectory: 'dist', + // 最大缓存大小(50MB) + maximumFileSizeToCacheInBytes: 50 * 1024 * 1024, + // 清理过期缓存 + cleanupOutdatedCaches: true, + // 跳过等待,立即激活新的 Service Worker + skipWaiting: true, + clientsClaim: true, + // 运行时缓存策略 + runtimeCaching: [ + { + // npm 依赖包(从 node_modules 加载的资源) + urlPattern: /^https?:\/\/.*\/node_modules\/.*/i, + handler: 'CacheFirst', + options: { + cacheName: 'npm-dependencies-cache', + expiration: { + maxEntries: 100, + maxAgeSeconds: 60 * 60 * 24 * 365 // 1 year + }, + cacheableResponse: { + statuses: [0, 200] + } + } + }, + { + // Vite 开发服务器的模块 + urlPattern: /\/@vite\/|\/node_modules\//, + handler: 'CacheFirst', + options: { + cacheName: 'vite-modules-cache', + expiration: { + maxEntries: 200, + maxAgeSeconds: 60 * 60 * 24 * 30 // 30 days + } + } + }, + { + // 字体 CDN + urlPattern: /^https:\/\/(fonts\.loli\.net|fonts\.googleapis\.com|fonts\.gstatic\.com|gstatic\.loli\.net)\/.*/i, + handler: 'CacheFirst', + options: { + cacheName: 'fonts-cache', + expiration: { + maxEntries: 30, + maxAgeSeconds: 60 * 60 * 24 * 365 // 1 year + }, + cacheableResponse: { + statuses: [0, 200] + } + } + }, + { + // API 请求 + urlPattern: /^https:\/\/api\.searchgal\.homes\/.*/i, + handler: 'NetworkFirst', + options: { + cacheName: 'api-cache', + expiration: { + maxEntries: 50, + maxAgeSeconds: 60 * 5 // 5 minutes + }, + networkTimeoutSeconds: 10, + cacheableResponse: { + statuses: [0, 200] + } + } + }, + { + // 随机图片 API + urlPattern: /^https:\/\/api\.illlights\.com\/.*/i, + handler: 'CacheFirst', + options: { + cacheName: 'random-images-cache', + expiration: { + maxEntries: 20, + maxAgeSeconds: 60 * 60 * 24 // 1 day + }, + cacheableResponse: { + statuses: [0, 200] + } + } + }, + { + // VNDB 图片 + urlPattern: /^https:\/\/.*vndb\.org\/.*/i, + handler: 'CacheFirst', + options: { + cacheName: 'vndb-images-cache', + expiration: { + maxEntries: 50, + maxAgeSeconds: 60 * 60 * 24 * 7 // 7 days + }, + cacheableResponse: { + statuses: [0, 200] + } + } + }, + { + // CDN 资源(busuanzi 等) + urlPattern: /^https:\/\/(registry\.npmmirror\.com|cdn\.jsdelivr\.net|unpkg\.com)\/.*/i, + handler: 'CacheFirst', + options: { + cacheName: 'cdn-cache', + expiration: { + maxEntries: 50, + maxAgeSeconds: 60 * 60 * 24 * 30 // 30 days + }, + cacheableResponse: { + statuses: [0, 200] + } + } + }, + { + // 图片资源 + urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp|ico)$/i, + handler: 'CacheFirst', + options: { + cacheName: 'images-cache', + expiration: { + maxEntries: 100, + maxAgeSeconds: 60 * 60 * 24 * 30 // 30 days + }, + cacheableResponse: { + statuses: [0, 200] + } + } + }, + { + // JS/CSS 资源 + urlPattern: /\.(?:js|css)$/i, + handler: 'StaleWhileRevalidate', + options: { + cacheName: 'static-resources-cache', + expiration: { + maxEntries: 100, + maxAgeSeconds: 60 * 60 * 24 * 7 // 7 days + }, + cacheableResponse: { + statuses: [0, 200] + } + } + } + ] + }, + devOptions: { + enabled: true + } + }) + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + } });