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 }