PageSourceSearch

https://www.erisdev.com/assets/js/app.js

js erisdev.com collected 2026-10-04 01:42:34 UTC 7,846 bytes, 250 lines download raw bytes

1/**
2 * 应用入口(原生 ES Module)
3 *
4 * 取代原先的 <script src="main.js" defer>。
5 * 负责:等待 DOM → 加载视图片段 → 按原始顺序初始化各功能模块 → 隐藏加载动画。
6 *
7 * 初始化顺序与原 main.js 中 ErisPulseApp.init() 严格一致,以保证行为不变。
8 */
9
10import { I18n } from "./i18n.js";
11import { loadUserSettings } from "./core/state.js";
12import { loadViews } from "./core/views.js";
13import * as settings from "./modules/settings.js";
14import * as nav from "./modules/nav.js";
15import * as market from "./modules/marketplace.js";
16import * as docs from "./modules/docs.js";
17import { renderFriendLinks, renderDependencies } from "./modules/about.js";
18import * as home from "./modules/home.js";
19import { SubmitModuleManager } from "./modules/submit.js";
20import { CommunityManager } from "./modules/community.js";
21import { AuthManager } from "./core/auth.js";
22
23/**
24 * GitHub 风格 .md 深链归一化:
25 *  - https://www.erisdev.com/?md=api-reference/xxx.md
26 *    (Worker / 404.html 重定向落点,浏览器会自动保留原始 fragment)
27 *  - https://www.erisdev.com/api-reference/xxx.md#章节
28 *    (SPA 直接服务该路径时的兜底)
29 * 统一转换为站内路由 #docs/<path>#<section>。
30 */
31(function normalizeDocDeepLink() {
32  try {
33    let mdPath = null;
34    const params = new URLSearchParams(window.location.search);
35
36    if (params.has("md")) {
37      mdPath = (params.get("md") || "").replace(/^\/+/, "");
38      params.delete("md");
39    } else if (/\.md$/i.test(window.location.pathname)) {
40      mdPath = window.location.pathname.replace(/^\/+/, "");
41    }
42
43    if (!mdPath) return;
44
45    const query = params.toString();
46    const frag = window.location.hash.replace(/^#+/, "");
47    let target = "/#docs/" + mdPath;
48    if (frag && !frag.startsWith("docs")) {
49      target += "#" + frag;
50    }
51    history.replaceState(null, "", "/" + (query ? "?" + query : "") + target);
52  } catch (e) {
53    console.warn("文档深链归一化失败:", e);
54  }
55})();
56
57/**
58 * Logo 图片加载检测:加载完成后淡入,加载中显示文字占位
59 */
60(function setupLoaderLogo() {
61  const img = document.querySelector(".loader-icon");
62  if (!img) return;
63  const reveal = () => img.classList.add("loaded");
64  if (img.complete && img.naturalWidth > 0) reveal();
65  else img.addEventListener("load", reveal, { once: true });
66})();
67
68/**
69 * 等价于原 ErisPulseApp.init()
70 * 注意:setupStorage() 与 loadUserSettings() 在原实现中功能完全重复,
71 * 此处仅调用 loadUserSettings() 一次即可达到相同效果。
72 */
73function runInit() {
74  I18n.init();
75  loadUserSettings();
76  settings.applyUserSettings();
77  nav.registerServiceWorker();
78  settings.setupThemeToggle();
79  nav.setupHamburgerMenu();
80  // 账户与社区模块先于视图切换初始化:
81  // setupViewSwitching 会按 __INITIAL_VIEW__ 触发 community 视图的数据加载
82  AuthManager.init();
83  SubmitModuleManager.init();
84  CommunityManager.init();
85  nav.setupViewSwitching();
86  settings.setupGlobalLangSwitcher();
87  market.setupMarketplace();
88  docs.setupDocumentation();
89  market.setupModals();
90  settings.setupSettings();
91  renderFriendLinks();
92  renderDependencies();
93  home.setupHomeAnimations();
94  home.initBannerCarousel();
95  home.initInstallOverlay();
96  home.initHeroStats();
97  home.initCommunityTeaser();
98  nav.setupOnlineOffline();
99}
100
101/**
102 * 气泡条带揭幕(海绵宝宝风格):
103 * 一条气泡带从底部扫向顶部,带下方的页面逐步揭示。
104 * 气泡带有可见轮廓 + 高光,收束后消失。
105 */
106function hideLoader() {
107  const loader = document.getElementById("page-loader");
108  if (!loader) return;
109  const reduceMotion =
110    document.body.classList.contains("no-animations") ||
111    window.matchMedia("(prefers-reduced-motion: reduce)").matches;
112  if (reduceMotion) {
113    loader.style.display = "none";
114    return;
115  }
116
117  const W = window.innerWidth;
118  const H = window.innerHeight;
119
120  const canvas = document.createElement("canvas");
121  canvas.className = "reveal-bubble";
122  canvas.width = W;
123  canvas.height = H;
124  const ctx = canvas.getContext("2d");
125  document.body.appendChild(canvas);
126
127  const css = getComputedStyle(document.documentElement);
128  const bgRGB = css.getPropertyValue("--bg-rgb").trim();
129  const pRGB = css.getPropertyValue("--primary-rgb").trim();
130
131  loader.style.display = "none";
132
133  // 气泡条带配置
134  const bandH = 120;
135  const softEdge = 45;
136  const bubbles = [];
137  for (let i = 0; i < 70; i++) {
138    bubbles.push({
139      x: Math.random() * W,
140      yOff: (Math.random() - 0.5) * bandH,
141      r: 2 + Math.random() * 16,
142      wobble: Math.random() * Math.PI * 2,
143      wSpeed: 0.001 + Math.random() * 0.002,
144      alpha: 0.5 + Math.random() * 0.5,
145    });
146  }
147
148  const duration = 750;
149  const startT = performance.now();
150
151  function frame(now) {
152    const t = Math.min(1, (now - startT) / duration);
153    // easeInOutCubic
154    const eased = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
155
156    // 条带中心从底部扫向顶部
157    const sweepY = H + bandH - eased * (H + bandH * 2);
158
159    // 收束:最后 30% 气泡向中心汇聚、缩小
160    const converge = Math.max(0, (t - 0.7) / 0.3);
161
162    ctx.clearRect(0, 0, W, H);
163
164    // 实心填充(条带上方覆盖页面)
165    const opaqueEnd = sweepY - softEdge;
166    if (opaqueEnd > 0) {
167      ctx.fillStyle = `rgb(${bgRGB})`;
168      ctx.fillRect(0, 0, W, Math.min(H, opaqueEnd));
169    }
170
171    // 软边缘渐变(条带下沿)
172    if (sweepY > -softEdge && opaqueEnd < H) {
173      const gTop = Math.max(0, sweepY - softEdge);
174      const gBot = Math.min(H, sweepY + softEdge);
175      if (gBot > gTop) {
176        const grad = ctx.createLinearGradient(0, sweepY - softEdge, 0, sweepY + softEdge);
177        grad.addColorStop(0, `rgba(${bgRGB}, 1)`);
178        grad.addColorStop(1, `rgba(${bgRGB}, 0)`);
179        ctx.fillStyle = grad;
180        ctx.fillRect(0, gTop, W, gBot - gTop);
181      }
182    }
183
184    // 气泡(沿条带分布,可见轮廓 + 高光)
185    for (const b of bubbles) {
186      const yOff = b.yOff * (1 - converge * 0.7);
187      const r = b.r * (1 - converge * 0.5);
188      if (r < 2) continue;
189      const bx = b.x + Math.sin(now * b.wSpeed + b.wobble) * 6;
190      const by = sweepY + yOff + Math.cos(now * b.wSpeed + b.wobble) * 4;
191      if (by + r < 0 || by - r > H) continue;
192
193      // 填充(极淡肥皂膜)
194      ctx.fillStyle = `rgba(255, 255, 255, ${0.03 * b.alpha})`;
195      ctx.beginPath();
196      ctx.arc(bx, by, r, 0, Math.PI * 2);
197      ctx.fill();
198
199      // 轮廓(细线)
200      ctx.strokeStyle = `rgba(${pRGB}, ${0.12 * b.alpha})`;
201      ctx.lineWidth = 0.8;
202      ctx.stroke();
203
204      // 高光(仅较大气泡泡)
205      if (r > 4) {
206        ctx.fillStyle = `rgba(255, 255, 255, ${0.2 * b.alpha})`;
207        ctx.beginPath();
208        ctx.arc(bx - r * 0.3, by - r * 0.3, Math.max(1, r * 0.15), 0, Math.PI * 2);
209        ctx.fill();
210      }
211    }
212
213    if (t < 1) {
214      requestAnimationFrame(frame);
215    } else {
216      canvas.remove();
217    }
218  }
219  requestAnimationFrame(frame);
220}
221
222/**
223 * 启动流程:加载视图 → 初始化模块 → 隐藏加载动画
224 */
225async function bootstrap() {
226  try {
227    await loadViews();
228    runInit();
229  } catch (err) {
230    console.error("Failed to bootstrap application:", err);
231  } finally {
232    // 即使初始化失败也要隐藏加载层,避免卡在空白遮罩
233    // 等待两帧确保 DOM 已绘制,再开始波浪退潮
234    requestAnimationFrame(() => requestAnimationFrame(hideLoader));
235  }
236}
237
238/**
239 * module script 默认 defer,执行时 DOM 已解析完成;
240 * 但稳妥起见仍检查 readyState。
241 */
242function start() {
243  bootstrap();
244}
245
246if (document.readyState === "loading") {
247  document.addEventListener("DOMContentLoaded", start);
248} else {
249  start();
250}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.