prime8dev

prime8dev
git clone https://git.prime8.dev/prime8dev.git
Log | Files | Refs | README | LICENSE

layout.js (4589B)


      1 import {
      2   getWakaTimeData,
      3   startWakaTimeRefreshTimer,
      4   subscribeWakaTimeRefresh,
      5 } from "./wakatime.js";
      6 import { revealLines } from "./reveal.js";
      7 
      8 const BOOT_CHARS = "▓▒░█▄▀■□▪▫●○◆◇";
      9 const SCRAMBLE_DURATION = 400;
     10 const SCRAMBLE_INTERVAL = 30;
     11 const HOLD_DURATION = 450;
     12 const FADE_DURATION = 300;
     13 
     14 let bootPlayed = false;
     15 let unsubscribeWakaFooter = null;
     16 
     17 export async function initLayout() {
     18   const wakaEnabled = Boolean(document.getElementById("wakatime-link"));
     19   const initialWakaDataPromise = wakaEnabled ? getWakaTimeData() : null;
     20 
     21   await playBootAnimation();
     22 
     23   initSSHCopy();
     24 
     25   if (!wakaEnabled) return;
     26 
     27   await loadWakaFooter(await initialWakaDataPromise);
     28   if (unsubscribeWakaFooter) {
     29     unsubscribeWakaFooter();
     30   }
     31   unsubscribeWakaFooter = subscribeWakaTimeRefresh((data) => {
     32     void loadWakaFooter(data);
     33   });
     34   startWakaTimeRefreshTimer();
     35 }
     36 
     37 function sleep(ms) {
     38   return new Promise((resolve) => setTimeout(resolve, ms));
     39 }
     40 
     41 function scrambleText(length) {
     42   return Array.from(
     43     { length },
     44     () => BOOT_CHARS[Math.floor(Math.random() * BOOT_CHARS.length)],
     45   ).join("");
     46 }
     47 
     48 async function scrambleReveal(element, target, duration) {
     49   const steps = Math.floor(duration / SCRAMBLE_INTERVAL);
     50   const length = target.length;
     51 
     52   for (let i = 0; i <= steps; i++) {
     53     const progress = i / steps;
     54     const revealedCount = Math.floor(progress * length);
     55     const revealed = target.slice(0, revealedCount);
     56     const scrambled = scrambleText(length - revealedCount);
     57     element.textContent = revealed + scrambled;
     58     await sleep(SCRAMBLE_INTERVAL);
     59   }
     60 
     61   element.textContent = target;
     62 }
     63 
     64 function createBootOverlay(title, subtitle) {
     65   const overlay = document.createElement("div");
     66   overlay.id = "boot-overlay";
     67   overlay.innerHTML =
     68     '<div class="boot-terminal"><div class="boot-text"><span class="boot-title"></span><span class="boot-subtitle"></span></div></div>';
     69 
     70   const bootTitle = overlay.querySelector(".boot-title");
     71   const bootSubtitle = overlay.querySelector(".boot-subtitle");
     72   if (bootTitle) bootTitle.textContent = scrambleText(title.length);
     73   if (bootSubtitle) bootSubtitle.textContent = scrambleText(subtitle.length);
     74 
     75   document.body.appendChild(overlay);
     76   return overlay;
     77 }
     78 
     79 async function playBootAnimation() {
     80   const app = document.getElementById("app");
     81   if (!app) return;
     82 
     83   if (bootPlayed || app.classList.contains("ready")) {
     84     bootPlayed = true;
     85     app.classList.add("ready");
     86     return;
     87   }
     88 
     89   const title = app.dataset.bootTitle || "prime8.dev";
     90   const subtitle = app.dataset.bootSubtitle || "www.prime8.dev";
     91 
     92   const overlay = createBootOverlay(title, subtitle);
     93   const bootTitle = overlay.querySelector(".boot-title");
     94   const bootSubtitle = overlay.querySelector(".boot-subtitle");
     95 
     96   await sleep(100);
     97   if (bootTitle) await scrambleReveal(bootTitle, title, SCRAMBLE_DURATION);
     98   if (bootSubtitle)
     99     await scrambleReveal(bootSubtitle, subtitle, SCRAMBLE_DURATION);
    100 
    101   await sleep(HOLD_DURATION);
    102   overlay.classList.add("fade-out");
    103   await sleep(FADE_DURATION);
    104 
    105   overlay.remove();
    106   // same line-by-line reveal as a tab switch, armed before #app shows
    107   revealLines(document.getElementById("content"));
    108   app.classList.add("ready");
    109   bootPlayed = true;
    110 }
    111 
    112 async function loadWakaFooter(data = null) {
    113   const link = document.getElementById("wakatime-link");
    114   if (!link) return;
    115 
    116   try {
    117     const stats = data || (await getWakaTimeData());
    118     const text = stats?.AllTime?.TotalText;
    119     if (text) {
    120       link.textContent = text;
    121       link.style.display = "";
    122       return;
    123     }
    124     // nothing new: keep whatever is already shown, hide only if empty
    125     if (!link.textContent.trim()) link.style.display = "none";
    126   } catch {
    127     if (!link.textContent.trim()) link.style.display = "none";
    128   }
    129 }
    130 
    131 function initSSHCopy() {
    132   const button = document.querySelector("#statusbar [data-ssh-copy]");
    133   const label = button?.querySelector("[data-ssh-command]");
    134   if (!button || !label || button.dataset.bound === "true") return;
    135 
    136   button.dataset.bound = "true";
    137   const command = label.textContent.trim();
    138   button.addEventListener("click", async () => {
    139     const copied = await copyText(command);
    140     label.textContent = copied ? "copied" : "copy failed";
    141     button.classList.toggle("copied", copied);
    142     setTimeout(() => {
    143       label.textContent = command;
    144       button.classList.remove("copied");
    145     }, 1200);
    146   });
    147 }
    148 
    149 async function copyText(text) {
    150   try {
    151     await navigator.clipboard.writeText(text);
    152     return true;
    153   } catch {
    154     return false;
    155   }
    156 }