PageSourceSearch

https://electerm.org/js/home.EKOF5S3I.js

js electerm.org collected 2026-10-03 23:13:37 UTC 19,453 bytes, 622 lines download raw bytes

1import "./chunk.T7SNOZFH.js";
2import "./chunk.V2LBPZHK.js";
3
4// src/js/parts/animate.js
5var HeroAnimate = class {
6  constructor(el) {
7    this.el = el;
8    this.canvas = document.createElement("canvas");
9    this.canvas.style.cssText = "display:block;width:100%;height:100%";
10    this.el.appendChild(this.canvas);
11    this.ctx = this.canvas.getContext("2d");
12    this.animId = null;
13    this.w = 0;
14    this.h = 0;
15    this.PARTICLE_COUNT = 60;
16    this.CONNECT_DIST = 150;
17    this.DROP_COUNT = 30;
18    this.particles = [];
19    this.drops = [];
20    this.hexNodes = [];
21    this.scanY = 0;
22    this.featurePhrases = [
23      "Terminal",
24      "SSH",
25      "SFTP",
26      "Telnet",
27      "Serialport",
28      "RDP",
29      "VNC",
30      "Spice",
31      "FTP",
32      "Multi-platform",
33      "Multi-language",
34      "AI assistant",
35      "MCP",
36      "Deep link",
37      "Zmodem",
38      "Trzsz",
39      "SSH tunnel",
40      "Quick commands",
41      "Themes",
42      "Sync bookmarks",
43      "Proxy",
44      "Transparent window",
45      "\u7EC8\u7AEF",
46      "\u6587\u4EF6\u7BA1\u7406",
47      "\u591A\u5E73\u53F0",
48      "\u591A\u8BED\u8A00",
49      "\u5FEB\u6377\u952E",
50      "\u8FDC\u7A0B\u7F16\u8F91",
51      "\u5BC6\u7801\u767B\u5F55",
52      "\u5BC6\u5319\u767B\u5F55",
53      "\u96A7\u9053",
54      "\u4EE3\u7406\u670D\u52A1\u5668",
55      "\u5FEB\u6377\u547D\u4EE4",
56      "\u4E3B\u9898",
57      "\u540C\u6B65\u6570\u636E",
58      "\u5FEB\u901F\u8F93\u5165",
59      "AI\u52A9\u624B",
60      "\u6DF1\u5EA6\u94FE\u63A5",
61      "\u900F\u660E\u7A97\u53E3",
62      "\u80CC\u666F\u56FE\u7247",
63      "\u6587\u4EF6\u4F20\u8F93"
64    ];
65    this._resize = this.resize.bind(this);
66    this.init();
67  }
68  init() {
69    this.resize();
70    this.animate();
71    window.addEventListener("resize", this._resize);
72  }
73  resize() {
74    const dpr = window.devicePixelRatio || 1;
75    this.w = this.el.offsetWidth;
76    this.h = this.el.offsetHeight;
77    this.canvas.width = this.w * dpr;
78    this.canvas.height = this.h * dpr;
79    this.canvas.style.width = this.w + "px";
80    this.canvas.style.height = this.h + "px";
81    this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
82    this.initParticles();
83    this.initDrops();
84    this.initHexNodes();
85  }
86  initParticles() {
87    this.particles = [];
88    for (let i = 0; i < this.PARTICLE_COUNT; i++) {
89      this.particles.push({
90        x: Math.random() * this.w,
91        y: Math.random() * this.h,
92        vx: (Math.random() - 0.5) * 0.6,
93        vy: (Math.random() - 0.5) * 0.6,
94        r: Math.random() * 2 + 1,
95        opacity: Math.random() * 0.5 + 0.2
96      });
97    }
98  }
99  initDrops() {
100    this.drops = [];
101    for (let i = 0; i < this.DROP_COUNT; i++) {
102      this.drops.push({
103        x: Math.random() * this.w,
104        y: Math.random() * this.h * -1 - Math.random() * 300,
105        speed: Math.random() * 0.8 + 0.3,
106        phrase: this.featurePhrases[Math.floor(Math.random() * this.featurePhrases.length)],
107        opacity: Math.random() * 0.1 + 0.04,
108        fontSize: Math.random() * 4 + 11
109      });
110    }
111  }
112  initHexNodes() {
113    this.hexNodes = [];
114    for (let i = 0; i < 8; i++) {
115      this.hexNodes.push({
116        x: Math.random() * this.w,
117        y: Math.random() * this.h,
118        vx: (Math.random() - 0.5) * 0.3,
119        vy: (Math.random() - 0.5) * 0.3,
120        size: Math.random() * 12 + 8,
121        rotation: Math.random() * Math.PI * 2,
122        rotSpeed: (Math.random() - 0.5) * 0.01,
123        opacity: Math.random() * 0.08 + 0.03
124      });
125    }
126  }
127  drawGrid() {
128    const ctx = this.ctx;
129    const gridSize = 60;
130    ctx.strokeStyle = "rgba(37, 99, 235, 0.04)";
131    ctx.lineWidth = 0.5;
132    for (let x = 0; x < this.w; x += gridSize) {
133      ctx.beginPath();
134      ctx.moveTo(x, 0);
135      ctx.lineTo(x, this.h);
136      ctx.stroke();
137    }
138    for (let y = 0; y < this.h; y += gridSize) {
139      ctx.beginPath();
140      ctx.moveTo(0, y);
141      ctx.lineTo(this.w, y);
142      ctx.stroke();
143    }
144  }
145  drawParticles() {
146    const ctx = this.ctx;
147    for (const p of this.particles) {
148      p.x += p.vx;
149      p.y += p.vy;
150      if (p.x < 0 || p.x > this.w) p.vx *= -1;
151      if (p.y < 0 || p.y > this.h) p.vy *= -1;
152      ctx.beginPath();
153      ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
154      ctx.fillStyle = "rgba(37, 99, 235, " + p.opacity + ")";
155      ctx.fill();
156    }
157    for (let i = 0; i < this.particles.length; i++) {
158      for (let j = i + 1; j < this.particles.length; j++) {
159        const dx = this.particles[i].x - this.particles[j].x;
160        const dy = this.particles[i].y - this.particles[j].y;
161        const dist = Math.sqrt(dx * dx + dy * dy);
162        if (dist < this.CONNECT_DIST) {
163          const alpha = (1 - dist / this.CONNECT_DIST) * 0.15;
164          ctx.beginPath();
165          ctx.moveTo(this.particles[i].x, this.particles[i].y);
166          ctx.lineTo(this.particles[j].x, this.particles[j].y);
167          ctx.strokeStyle = "rgba(37, 99, 235, " + alpha + ")";
168          ctx.lineWidth = 0.8;
169          ctx.stroke();
170        }
171      }
172    }
173  }
174  drawMatrixRain() {
175    const ctx = this.ctx;
176    ctx.textBaseline = "top";
177    for (const drop of this.drops) {
178      ctx.font = "500 " + drop.fontSize + 'px "SF Mono", "Fira Code", monospace';
179      ctx.fillStyle = "rgba(37, 99, 235, " + drop.opacity + ")";
180      ctx.fillText(drop.phrase, drop.x, drop.y);
181      drop.y += drop.speed;
182      if (drop.y > this.h + 20) {
183        drop.y = Math.random() * -300 - 50;
184        drop.x = Math.random() * this.w;
185        drop.phrase = this.featurePhrases[Math.floor(Math.random() * this.featurePhrases.length)];
186      }
187    }
188  }
189  drawHex(x, y, size, rotation, opacity) {
190    const ctx = this.ctx;
191    ctx.beginPath();
192    for (let i = 0; i < 6; i++) {
193      const angle = rotation + Math.PI / 3 * i;
194      const px = x + size * Math.cos(angle);
195      const py = y + size * Math.sin(angle);
196      if (i === 0) ctx.moveTo(px, py);
197      else ctx.lineTo(px, py);
198    }
199    ctx.closePath();
200    ctx.strokeStyle = "rgba(37, 99, 235, " + opacity + ")";
201    ctx.lineWidth = 1;
202    ctx.stroke();
203  }
204  drawHexNodes() {
205    for (const node of this.hexNodes) {
206      node.x += node.vx;
207      node.y += node.vy;
208      node.rotation += node.rotSpeed;
209      if (node.x < -50 || node.x > this.w + 50) node.vx *= -1;
210      if (node.y < -50 || node.y > this.h + 50) node.vy *= -1;
211      this.drawHex(node.x, node.y, node.size, node.rotation, node.opacity);
212    }
213  }
214  drawScanLine() {
215    this.scanY += 0.5;
216    if (this.scanY > this.h) this.scanY = 0;
217    const grad = this.ctx.createLinearGradient(0, this.scanY - 30, 0, this.scanY + 30);
218    grad.addColorStop(0, "rgba(37, 99, 235, 0)");
219    grad.addColorStop(0.5, "rgba(37, 99, 235, 0.03)");
220    grad.addColorStop(1, "rgba(37, 99, 235, 0)");
221    this.ctx.fillStyle = grad;
222    this.ctx.fillRect(0, this.scanY - 30, this.w, 60);
223  }
224  animate() {
225    this.ctx.clearRect(0, 0, this.w, this.h);
226    this.drawGrid();
227    this.drawScanLine();
228    this.drawMatrixRain();
229    this.drawHexNodes();
230    this.drawParticles();
231    this.animId = requestAnimationFrame(() => this.animate());
232  }
233  destroy() {
234    cancelAnimationFrame(this.animId);
235    window.removeEventListener("resize", this._resize);
236    this.canvas.remove();
237  }
238};
239
240// src/js/parts/os-highlight.js
241var KEYWORDS = [
242  "Windows 7",
243  "macOS 10+",
244  "PowerPC 64-bit little-endian",
245  "PowerPC 64-Bit-Little-Endian",
246  "old-world",
247  "new-world",
248  "LoongArch",
249  "riscv64",
250  "RISC-V",
251  "RISC V",
252  "ppc64le",
253  "PowerPC",
254  "HarmonyOS",
255  "Windows",
256  "macOS",
257  "Android",
258  "Ubuntu 18",
259  "Ubuntu",
260  "Kylin",
261  "UOS",
262  "Linux",
263  "iOS",
264  // CJK tokens (zh-cn / zh-tw)
265  "\u94F6\u6CB3\u9E92\u9E9F",
266  "\u7EDF\u4FE1",
267  "\u9F99\u82AF",
268  "\u9E92\u9E9F",
269  "\u65B0\u65E7\u4E16\u754C"
270];
271var EMOJI = [
272  { re: /windows 7/i, emoji: "\u{1FA9F}" },
273  { re: /windows/i, emoji: "\u{1FA9F}" },
274  { re: /macos/i, emoji: "\u{1F34E}" },
275  { re: /^mac$/i, emoji: "\u{1F34E}" },
276  { re: /linux|ubuntu|uos|kylin|麒麟|统信/i, emoji: "\u{1F427}" },
277  { re: /android/i, emoji: "\u{1F916}" },
278  { re: /harmony/i, emoji: "\u{1F338}" },
279  { re: /ios/i, emoji: "\u{1F4F1}" },
280  { re: /loongarch|龙芯|old-world|new-world|新旧世界/i, emoji: "\u{1F409}" },
281  { re: /risc/i, emoji: "\u{1F9EC}" },
282  { re: /ppc|powerpc/i, emoji: "\u2699\uFE0F" }
283];
284function emojiFor(text) {
285  for (const m of EMOJI) {
286    if (m.re.test(text)) return m.emoji;
287  }
288  return "\u{1F4BB}";
289}
290function tabFor(text) {
291  const t = text.toLowerCase();
292  if (t.includes("windows")) return "windows";
293  if (t.includes("macos") || t === "mac" || t.includes("mac os")) return "mac";
294  if (t.includes("android")) return "android";
295  if (t.includes("harmony")) return "harmony";
296  if (t.includes("ios")) return "ios";
297  return "linux";
298}
299function escapeRe(s) {
300  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
301}
302var PATTERN = KEYWORDS.slice().sort((a, b) => b.length - a.length).map(escapeRe).join("|");
303var RE = new RegExp("(" + PATTERN + ")", "gi");
304function escapeHtml(s) {
305  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
306}
307function jumpToDownload(tab) {
308  const btn = document.querySelector('.download-tab-btn[data-tab="' + tab + '"]');
309  if (btn) btn.click();
310  const anchor = document.getElementById("downloads");
311  if (anchor) {
312    anchor.scrollIntoView({ behavior: "smooth", block: "start" });
313  } else {
314    window.location.hash = "#downloads";
315  }
316}
317function setActive(chip, on) {
318  if (on) {
319    const orig = chip.dataset.orig || chip.textContent;
320    if (!chip.querySelector(".os-emoji")) {
321      const em = document.createElement("span");
322      em.className = "os-emoji";
323      em.setAttribute("aria-hidden", "true");
324      em.textContent = emojiFor(orig);
325      chip.insertBefore(em, chip.firstChild);
326    }
327    if (!chip.title) chip.title = "See " + orig + " downloads \u2193";
328    chip.classList.remove("os-pop");
329    chip.getBoundingClientRect();
330    chip.classList.add("os-active", "os-pop");
331  } else {
332    chip.classList.remove("os-active", "os-pop");
333    chip.removeAttribute("title");
334    const em = chip.querySelector(".os-emoji");
335    if (em) em.remove();
336  }
337}
338function initOsHighlight() {
339  const el = document.querySelector(".hero-supports");
340  if (!el || el.dataset.osAnimated) return;
341  const raw = el.textContent;
342  if (!raw || !RE.test(raw)) return;
343  RE.lastIndex = 0;
344  el.dataset.osAnimated = "1";
345  el.innerHTML = escapeHtml(raw).replace(RE, function(m) {
346    const tab = tabFor(m);
347    return '<span class="os-chip" data-tab="' + tab + '" data-orig="' + escapeHtml(m) + '">' + escapeHtml(m) + "</span>";
348  });
349  const chips = Array.from(el.querySelectorAll(".os-chip"));
350  if (chips.length === 0) return;
351  chips.forEach(function(chip) {
352    chip.addEventListener("click", function() {
353      if (!chip.classList.contains("os-active")) return;
354      jumpToDownload(chip.dataset.tab || "linux");
355    });
356  });
357  const reduceMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
358  if (reduceMotion) {
359    return;
360  }
361  let current = -1;
362  let timer = null;
363  let hover = false;
364  let inView = true;
365  function pickNext() {
366    if (chips.length === 1) return 0;
367    let next = Math.floor(Math.random() * chips.length);
368    if (next === current) next = (next + 1 + Math.floor(Math.random() * (chips.length - 1))) % chips.length;
369    return next;
370  }
371  function spotlight() {
372    if (current >= 0 && chips[current]) setActive(chips[current], false);
373    current = pickNext();
374    const chip = chips[current];
375    if (chip) setActive(chip, true);
376  }
377  function schedule() {
378    clearTimeout(timer);
379    timer = setTimeout(function() {
380      if (!hover && inView && !document.hidden) spotlight();
381      schedule();
382    }, 900 + Math.random() * 1e3);
383  }
384  el.addEventListener("mouseenter", function() {
385    hover = true;
386  });
387  el.addEventListener("mouseleave", function() {
388    hover = false;
389  });
390  if (window.IntersectionObserver) {
391    const io = new IntersectionObserver(function(entries) {
392      inView = entries[0].isIntersecting;
393    }, { threshold: 0.1 });
394    io.observe(el);
395  }
396  setTimeout(spotlight, 600);
397  schedule();
398}
399if (document.readyState === "loading") {
400  document.addEventListener("DOMContentLoaded", initOsHighlight);
401} else {
402  initOsHighlight();
403}
404
405// src/js/parts/hero.js
406var heroEl = document.getElementById("hero-anim");
407if (heroEl) {
408  try {
409    heroEl.__heroAnimate = new HeroAnimate(heroEl);
410  } catch (e) {
411    console.warn("[hero] animate init failed", e);
412  }
413}
414var logo = document.querySelector(".hero-logo");
415if (logo) {
416  import("./chunk.C7LACAKJ.js").then(function(m) {
417    const { default: Hero3DLogo } = m;
418    try {
419      logo.__hero3DLogo = new Hero3DLogo(logo, { modelUrl: "/electerm.glb" });
420    } catch (e) {
421      console.warn("[hero-3d-logo] init failed", e);
422    }
423  }).catch(function(err) {
424    console.warn("[hero-3d-logo] init failed, keeping PNG logo", err);
425  });
426}
427
428// src/js/parts/download.js
429function detectOS() {
430  const p = navigator.platform.toLowerCase();
431  const u = navigator.userAgent.toLowerCase();
432  if (p.includes("mac") || u.includes("mac")) return "mac";
433  if (p.includes("win") || u.includes("windows")) return "windows";
434  if (p.includes("linux") || u.includes("linux")) return "linux";
435  if (p.includes("android") || u.includes("android")) return "android";
436  return "windows";
437}
438function switchTab(os) {
439  document.querySelectorAll(".download-tab-btn").forEach(function(b) {
440    b.classList.toggle("active", b.getAttribute("data-tab") === os);
441  });
442  document.querySelectorAll(".download-panel").forEach(function(p) {
443    p.classList.toggle("active", p.getAttribute("data-tab") === os);
444  });
445}
446function switchArch(btn, arch) {
447  const container = btn.closest(".arch-bar").parentElement;
448  container.querySelectorAll(".arch-btn").forEach(function(b) {
449    b.classList.toggle("active", b.getAttribute("data-arch") === arch);
450  });
451  container.querySelectorAll(".arch-panel").forEach(function(p) {
452    p.classList.toggle("active", p.getAttribute("data-arch") === arch);
453  });
454}
455function isAndroidDevice() {
456  const u = navigator.userAgent.toLowerCase();
457  const p = (navigator.platform || "").toLowerCase();
458  return p.includes("android") || u.includes("android");
459}
460var qrLibLoaded = false;
461function loadQrLib(cb) {
462  if (qrLibLoaded) {
463    cb();
464    return;
465  }
466  if (window.qrcode) {
467    qrLibLoaded = true;
468    cb();
469    return;
470  }
471  const s = document.createElement("script");
472  s.src = "/js/qrcode-generator.min.js";
473  s.onload = function() {
474    qrLibLoaded = true;
475    cb();
476  };
477  s.onerror = function() {
478    cb(new Error("Failed to load QR library"));
479  };
480  document.head.appendChild(s);
481}
482function showQrModal(url, source) {
483  const modal = document.getElementById("androidQrModal");
484  const codeEl = document.getElementById("androidQrCode");
485  const urlEl = document.getElementById("androidQrUrl");
486  const dlEl = document.getElementById("androidQrDownload");
487  const srcEl = document.getElementById("androidQrSource");
488  if (!modal) return;
489  urlEl.textContent = url;
490  dlEl.href = url;
491  if (srcEl && source) srcEl.textContent = source;
492  codeEl.innerHTML = '<div class="android-qr-loading">Generating QR code\u2026</div>';
493  modal.classList.add("active");
494  modal.setAttribute("aria-hidden", "false");
495  loadQrLib(function(err) {
496    if (err) {
497      codeEl.innerHTML = '<div class="android-qr-error">Failed to generate QR code</div>';
498      return;
499    }
500    try {
501      const qr = window.qrcode(0, "M");
502      qr.addData(url);
503      qr.make();
504      codeEl.innerHTML = qr.createSvgTag({
505        cellSize: 4,
506        margin: 4,
507        scalable: true,
508        alt: "QR code for " + url
509      });
510    } catch (e) {
511      codeEl.innerHTML = '<div class="android-qr-error">Failed to generate QR code</div>';
512    }
513  });
514}
515function hideQrModal() {
516  const modal = document.getElementById("androidQrModal");
517  if (modal) {
518    modal.classList.remove("active");
519    modal.setAttribute("aria-hidden", "true");
520  }
521}
522function copyAltCmd(btn) {
523  const item = btn.closest(".alt-install-item");
524  const code = item && item.querySelector(".alt-install-code code");
525  if (!code) return;
526  const text = code.textContent;
527  const done = function() {
528    const old = btn.textContent;
529    btn.textContent = btn.dataset.copied;
530    setTimeout(function() {
531      btn.textContent = old;
532    }, 1500);
533  };
534  if (navigator.clipboard && navigator.clipboard.writeText) {
535    navigator.clipboard.writeText(text).then(done, function() {
536      fallbackCopy(text);
537      done();
538    });
539  } else {
540    fallbackCopy(text);
541    done();
542  }
543}
544function fallbackCopy(text) {
545  const ta = document.createElement("textarea");
546  ta.value = text;
547  ta.style.position = "fixed";
548  ta.style.opacity = "0";
549  document.body.appendChild(ta);
550  ta.select();
551  try {
552    document.execCommand("copy");
553  } catch (e) {
554  }
555  document.body.removeChild(ta);
556}
557function firstPresentTab() {
558  const b = document.querySelector(".download-tab-btn");
559  return b ? b.getAttribute("data-tab") : "windows";
560}
561document.addEventListener("DOMContentLoaded", function() {
562  const detected = detectOS();
563  const detectedPanel = document.querySelector('.download-panel[data-tab="' + detected + '"]');
564  switchTab(detectedPanel ? detected : firstPresentTab());
565  document.querySelectorAll(".download-tab-btn").forEach(function(b) {
566    b.addEventListener("click", function() {
567      switchTab(this.getAttribute("data-tab"));
568    });
569  });
570  document.querySelectorAll(".arch-bar").forEach(function(bar) {
571    bar.querySelectorAll(".arch-btn").forEach(function(b) {
572      b.addEventListener("click", function() {
573        switchArch(this, this.getAttribute("data-arch"));
574      });
575    });
576  });
577  document.querySelectorAll(".alt-copy").forEach(function(b) {
578    b.addEventListener("click", function() {
579      copyAltCmd(this);
580    });
581  });
582  if (!isAndroidDevice()) {
583    const qrModal = document.getElementById("androidQrModal");
584    if (qrModal) {
585      const androidPanel = document.querySelector('.download-panel[data-tab="android"]');
586      if (androidPanel) {
587        androidPanel.querySelectorAll(".download-card-link").forEach(function(link) {
588          link.addEventListener("click", function(e) {
589            e.preventDefault();
590            showQrModal(this.href, this.textContent.trim());
591          });
592        });
593      }
594      const closeBtn = document.getElementById("androidQrClose");
595      if (closeBtn) closeBtn.addEventListener("click", hideQrModal);
596      const overlay = document.querySelector(".android-qr-overlay");
597      if (overlay) overlay.addEventListener("click", hideQrModal);
598      document.addEventListener("keydown", function(e) {
599        if (e.key === "Escape") hideQrModal();
600      });
601    }
602  }
603});
604
605// src/js/parts/demo.js
606function switchMode(mode) {
607  const wrapper = document.querySelector(".demo-wrapper");
608  if (!wrapper) return;
609  wrapper.classList.toggle("mobile", mode === "mobile");
610  document.querySelectorAll(".demo-switch-btn").forEach(function(b) {
611    const active = b.getAttribute("data-mode") === mode;
612    b.classList.toggle("active", active);
613    b.setAttribute("aria-selected", active ? "true" : "false");
614  });
615}
616document.addEventListener("DOMContentLoaded", function() {
617  document.querySelectorAll(".demo-switch-btn").forEach(function(b) {
618    b.addEventListener("click", function() {
619      switchMode(this.getAttribute("data-mode"));
620    });
621  });
622});

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.