diff options
Diffstat (limited to 'index.html')
| -rwxr-xr-x | index.html | 1317 |
1 files changed, 8 insertions, 1309 deletions
@@ -7,278 +7,7 @@ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Audiowide&family=Caveat:wght@400;500;600;700&family=Michroma&family=Plus+Jakarta+Sans:wght@200;300;400;500;600;700;800&family=Space+Grotesk:wght@400;500;700&family=IBM+Plex+Mono:wght@400;500&display=swap" /> <link rel="stylesheet" href="aero.css" /> -<style> - /* ============= LOCKED PALETTE ============= */ - :root { - --sky: url("img/wallpaper.png") center / cover no-repeat, linear-gradient(180deg, oklch(78% 0.10 215) 0%, oklch(88% 0.14 145) 100%); - --sun: radial-gradient(circle, oklch(99% 0.02 215) 0%, oklch(94% 0.05 215 / 0) 60%); - --icon-blue: linear-gradient(135deg, oklch(92% 0.06 215), oklch(72% 0.13 220) 60%, oklch(48% 0.13 230)); - --icon-orange: linear-gradient(135deg, oklch(94% 0.10 145), oklch(78% 0.18 145) 55%, oklch(52% 0.16 150)); - --icon-green: linear-gradient(135deg, oklch(94% 0.12 130), oklch(76% 0.18 140) 55%, oklch(50% 0.16 150)); - --icon-pink: linear-gradient(135deg, oklch(88% 0.08 195), oklch(68% 0.13 200) 60%, oklch(45% 0.13 210)); - --icon-silver: linear-gradient(135deg, oklch(98% 0.005 220), oklch(85% 0.015 220) 60%, oklch(62% 0.03 225)); - --title-bar: linear-gradient(to bottom, oklch(94% 0.05 195), oklch(78% 0.10 200) 50%, oklch(60% 0.12 215)); - --start-btn: linear-gradient(to bottom, oklch(94% 0.10 145) 0%, oklch(75% 0.18 145) 48%, oklch(50% 0.16 150) 52%, oklch(68% 0.18 145) 100%); - --start-border: oklch(40% 0.14 150); - } - .desk { position: fixed; inset: 0; overflow: hidden; background: var(--sky); background-size: cover; background-position: center; } - .icons { position: absolute; left: 24px; top: 24px; display: grid; grid-template-columns: 1fr; gap: 18px; z-index: 10; } - .icon { display: flex; flex-direction: column; align-items: center; gap: 4px; width: 80px; cursor: pointer; text-align: center; } - .icon .glyph { - width: 56px; height: 56px; border-radius: 14px; - background: linear-gradient(135deg, oklch(88% 0.10 220), oklch(70% 0.14 230) 60%, oklch(50% 0.13 240)); - box-shadow: inset 0 1px 0 rgba(255,255,255,0.9), inset 0 -3px 6px rgba(40,80,140,0.4), 0 4px 14px rgba(40,80,140,0.35); - display: flex; align-items: center; justify-content: center; - font-size: 26px; color: white; text-shadow: 0 1px 2px rgba(0,0,0,0.4); - position: relative; - } - .icon .glyph::before { content: ""; position: absolute; left: 4px; right: 4px; top: 3px; height: 40%; border-radius: 12px; background: linear-gradient(to bottom, rgba(255,255,255,0.75), transparent); } - .icon .glyph.orange { background: linear-gradient(135deg, oklch(92% 0.08 70), oklch(75% 0.16 55) 60%, oklch(55% 0.15 35)); } - .icon .glyph.green { background: linear-gradient(135deg, oklch(90% 0.10 145), oklch(75% 0.15 145) 60%, oklch(50% 0.13 155)); } - .icon .glyph.pink { background: linear-gradient(135deg, oklch(90% 0.10 350), oklch(75% 0.16 350) 60%, oklch(55% 0.16 340)); } - .icon .glyph.silver { background: linear-gradient(135deg, oklch(95% 0.01 240), oklch(78% 0.03 240) 60%, oklch(55% 0.04 240)); } - .icon .label { font-size: 12px; color: white; text-shadow: 0 1px 3px rgba(0,0,0,0.6); font-weight: 500; } - .icon:hover .glyph { transform: translateY(-2px) scale(1.04); transition: transform 200ms; } - - .win { position: absolute; min-width: 320px; z-index: 50; } - /* When a window has been resized, it switches to a flex column so the - body becomes a scroll region — keeps content from getting clipped */ - .win.resized { - display: flex; flex-direction: column; - overflow: hidden; - border-radius: 18px; - } - .win.resized .body { - flex: 1; min-height: 0; - overflow-y: auto; - border-radius: 0 0 18px 18px; - } - /* The browser window is already a flex column internally; let it fill */ - .win.resized > .browser { flex: 1; min-height: 0; } - /* RESIZE HANDLES */ - .rs { position: absolute; z-index: 5; } - .rs-e { right: -3px; top: 14px; bottom: 18px; width: 8px; cursor: ew-resize; } - .rs-s { left: 14px; right: 18px; bottom: -3px; height: 8px; cursor: ns-resize; } - .rs-se { - right: 0; bottom: 0; width: 22px; height: 22px; - cursor: nwse-resize; - background-image: - linear-gradient(135deg, transparent 0 38%, rgba(40,80,140,0.85) 38% 48%, transparent 48% 60%, rgba(40,80,140,0.75) 60% 70%, transparent 70%, rgba(40,80,140,0.6) 82% 92%, transparent 92%); - border-radius: 0 0 18px 0; - opacity: 0.65; - transition: opacity 150ms, filter 150ms; - } - .rs-se:hover { opacity: 1; filter: drop-shadow(0 0 4px rgba(120,170,220,0.8)); } - body.rs-cursor-e, body.rs-cursor-e * { cursor: ew-resize !important; } - body.rs-cursor-s, body.rs-cursor-s * { cursor: ns-resize !important; } - body.rs-cursor-se, body.rs-cursor-se * { cursor: nwse-resize !important; } - .win .titlebar { - height: 32px; padding: 0 12px; display: flex; align-items: center; gap: 8px; - border-radius: 18px 18px 0 0; - background: var(--title-bar); - color: var(--title-fg, white); font-size: 13px; font-weight: 600; text-shadow: 0 1px 2px var(--title-shadow, rgba(0,0,0,0.3)); - cursor: grab; user-select: none; - border-bottom: 1px solid rgba(0,0,0,0.15); - } - .win .titlebar .dots { display: flex; gap: 6px; margin-right: 8px; } - .win .titlebar .dot { width: 13px; height: 13px; border-radius: 50%; border: 1px solid rgba(0,0,0,0.35); cursor: pointer; box-shadow: inset 0 1px 0 rgba(255,255,255,0.7); } - .win .titlebar .dot.r { background: radial-gradient(circle at 35% 30%, oklch(85% 0.18 30), oklch(55% 0.18 30)); } - .win .titlebar .dot.y { background: radial-gradient(circle at 35% 30%, oklch(95% 0.15 95), oklch(70% 0.18 80)); } - .win .titlebar .dot.g { background: radial-gradient(circle at 35% 30%, oklch(90% 0.18 145), oklch(60% 0.18 150)); } - .win .body { padding: 16px; font-size: 13px; line-height: 1.55; color: oklch(22% 0.04 240); border-radius: 0 0 18px 18px; } - - .taskbar { - position: absolute; left: 50%; bottom: 16px; transform: translateX(-50%); - height: 56px; padding: 0 12px; display: flex; align-items: center; gap: 10px; - border-radius: 28px; - background: linear-gradient(to bottom, rgba(255,255,255,0.55), rgba(180,210,240,0.45)); - backdrop-filter: blur(20px) saturate(180%); - border: 1px solid rgba(255,255,255,0.85); - box-shadow: inset 0 1px 0 rgba(255,255,255,0.95), 0 12px 36px rgba(40,80,140,0.3); - z-index: 200; - } - .taskbar .start { - height: 40px; padding: 0 18px 0 14px; display: inline-flex; align-items: center; gap: 8px; - border-radius: 20px; - background: var(--start-btn); - color: white; text-shadow: 0 1px 2px rgba(0,0,0,0.4); font-weight: 700; font-size: 13px; - border: 1px solid var(--start-border); cursor: pointer; - box-shadow: inset 0 1px 0 rgba(255,255,255,0.85), 0 3px 10px rgba(180,90,40,0.3); - } - .taskbar .sep { width: 1px; height: 32px; background: linear-gradient(to bottom, transparent, rgba(60,100,160,0.4), transparent); } - .tray { display: inline-flex; align-items: center; gap: 8px; padding: 0 12px; font-size: 12px; color: oklch(25% 0.05 240); } - .clock { font-family: "Segoe UI", Tahoma; font-weight: 600; } - - /* SERVER ROW */ - .srv-row { - display: flex; align-items: center; gap: 10px; padding: 7px 0; - border-bottom: 1px dotted oklch(72% 0.05 220); font-size: 12px; - } - .srv-row:last-child { border-bottom: 0; } - .srv-led { - width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; - box-shadow: inset 0 1px 0 rgba(255,255,255,0.7); - animation: led-pulse 2.2s ease-in-out infinite; - } - .srv-led.ok { background: radial-gradient(circle at 30% 30%, oklch(96% 0.18 145), oklch(60% 0.18 150)); box-shadow: inset 0 1px 0 rgba(255,255,255,0.7), 0 0 8px oklch(70% 0.18 145 / 0.7); } - .srv-led.warn { background: radial-gradient(circle at 30% 30%, oklch(95% 0.16 85), oklch(70% 0.18 75)); box-shadow: inset 0 1px 0 rgba(255,255,255,0.7), 0 0 8px oklch(70% 0.18 80 / 0.6); } - @keyframes led-pulse { 50% { opacity: 0.55; } } - .srv-host { font-family: 'Courier New', monospace; flex: 1; color: oklch(25% 0.05 230); } - .srv-meta { font-size: 10px; opacity: 0.7; } - - /* FILM DIARY ROW */ - .film-row { - display: flex; gap: 10px; padding: 8px 0; - border-bottom: 1px dotted oklch(72% 0.05 220); - } - .film-row:last-child { border-bottom: 0; } - .film-poster { - width: 38px; height: 56px; border-radius: 4px; flex-shrink: 0; - box-shadow: inset 0 1px 0 rgba(255,255,255,0.4), 0 2px 6px rgba(40,80,140,0.2); - } - .film-meta { flex: 1; min-width: 0; font-size: 12px; line-height: 1.45; } - .film-title { font-weight: 700; color: oklch(25% 0.06 230); font-size: 13px; } - .film-year { font-weight: 400; opacity: 0.6; font-size: 11px; margin-left: 3px; } - .film-rating { font-size: 11px; margin: 1px 0 2px; } - .film-rating .star { color: oklch(80% 0.04 230); } - .film-rating .star.on { color: oklch(72% 0.16 60); text-shadow: 0 0 4px oklch(80% 0.18 70 / 0.5); } - .film-rating .star.half { background: linear-gradient(90deg, oklch(72% 0.16 60) 50%, oklch(80% 0.04 230) 50%); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; } - .film-note { font-size: 11px; opacity: 0.75; font-style: italic; } - - /* ============= FAUX BROWSER ============= */ - .browser { display: flex; flex-direction: column; height: 100%; } - .browser .titlebar { border-bottom: 1px solid rgba(0,0,0,0.18); } - .browser-toolbar { - display: flex; align-items: center; gap: 8px; - padding: 8px 12px; - background: linear-gradient(to bottom, oklch(96% 0.005 220), oklch(86% 0.015 225) 50%, oklch(78% 0.025 230)); - border-bottom: 1px solid oklch(60% 0.04 230); - box-shadow: inset 0 -1px 0 rgba(255,255,255,0.5); - } - .nav-btn { - width: 28px; height: 24px; border-radius: 12px; - display: inline-flex; align-items: center; justify-content: center; - background: linear-gradient(to bottom, oklch(99% 0.005 220), oklch(88% 0.015 225) 50%, oklch(74% 0.03 230)); - border: 1px solid oklch(55% 0.05 230); - color: oklch(35% 0.05 235); font-size: 13px; line-height: 1; - cursor: pointer; - box-shadow: inset 0 1px 0 rgba(255,255,255,0.95), 0 1px 2px rgba(40,80,140,0.25); - transition: transform 100ms, box-shadow 100ms; - } - .nav-btn:hover { transform: translateY(-0.5px); } - .nav-btn:active { transform: translateY(0.5px); box-shadow: inset 0 1px 3px rgba(40,80,140,0.4); } - .nav-btn[disabled] { opacity: 0.35; cursor: default; transform: none !important; } - .nav-btn.refresh.spinning svg { animation: spin 700ms linear; } - @keyframes spin { to { transform: rotate(360deg); } } - .url-bar { - flex: 1; height: 26px; display: flex; align-items: center; gap: 6px; - padding: 0 10px; - background: linear-gradient(to bottom, oklch(99% 0.003 220), oklch(94% 0.008 220)); - border: 1px solid oklch(60% 0.04 230); - border-radius: 13px; - box-shadow: inset 0 1px 2px rgba(40,80,140,0.18); - font: 12px "IBM Plex Mono", "Courier New", monospace; - color: oklch(28% 0.05 235); - overflow: hidden; min-width: 0; - } - .url-bar .lock { font-size: 10px; color: oklch(58% 0.16 145); flex-shrink: 0; } - .url-bar .url-scheme { opacity: 0.55; } - .url-bar .url-host { color: oklch(30% 0.08 240); font-weight: 600; } - .url-bar .url-path { opacity: 0.85; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } - .url-bar .caret { width: 1px; height: 14px; background: oklch(40% 0.08 240); animation: blink 1s steps(2) infinite; } - @keyframes blink { 50% { opacity: 0; } } - .browser-bookmarks { - display: flex; align-items: center; gap: 2px; - padding: 4px 10px; - background: linear-gradient(to bottom, oklch(94% 0.01 220), oklch(86% 0.02 225)); - border-bottom: 1px solid oklch(62% 0.04 230); - font-size: 11px; - overflow-x: auto; scrollbar-width: none; - } - .browser-bookmarks::-webkit-scrollbar { display: none; } - .bm { - display: inline-flex; align-items: center; gap: 5px; - padding: 3px 8px; border-radius: 8px; - color: oklch(28% 0.06 235); white-space: nowrap; cursor: pointer; - border: 1px solid transparent; - transition: background 120ms, border-color 120ms; - } - .bm:hover { background: rgba(255,255,255,0.6); border-color: rgba(255,255,255,0.95); } - .bm.active { background: linear-gradient(to bottom, rgba(255,255,255,0.7), rgba(200,230,255,0.4)); border-color: rgba(120,170,220,0.5); } - .bm .favicon { width: 12px; height: 12px; border-radius: 3px; box-shadow: inset 0 1px 0 rgba(255,255,255,0.6); } - .bm.home .favicon { background: linear-gradient(135deg, oklch(85% 0.10 220), oklch(55% 0.14 235)); } - .browser-page { - flex: 1; min-height: 0; overflow-y: auto; - background: oklch(98% 0.004 220); - color: oklch(20% 0.03 240); - font: 14px "Georgia", "Times New Roman", serif; - line-height: 1.65; - padding: 0; - position: relative; - } - .browser-page::-webkit-scrollbar { width: 12px; } - .browser-page::-webkit-scrollbar-track { background: linear-gradient(to right, oklch(92% 0.01 220), oklch(96% 0.005 220)); } - .browser-page::-webkit-scrollbar-thumb { background: linear-gradient(to right, oklch(82% 0.03 225), oklch(70% 0.05 230)); border-radius: 6px; border: 2px solid oklch(94% 0.008 220); } - .browser-status { - height: 22px; padding: 0 12px; - display: flex; align-items: center; justify-content: space-between; - background: linear-gradient(to bottom, oklch(90% 0.01 220), oklch(80% 0.02 225)); - border-top: 1px solid oklch(60% 0.04 230); - font: 11px "Segoe UI", Tahoma, sans-serif; - color: oklch(35% 0.05 235); - border-radius: 0 0 18px 18px; - } - .browser-status .progress { - flex: 1; max-width: 80px; height: 8px; margin: 0 8px; - background: oklch(82% 0.02 225); border-radius: 4px; - border: 1px solid oklch(65% 0.04 230); - overflow: hidden; box-shadow: inset 0 1px 2px rgba(40,80,140,0.15); - } - .browser-status .progress-bar { - height: 100%; width: 100%; - background: linear-gradient(to bottom, oklch(82% 0.13 220), oklch(58% 0.14 235)); - box-shadow: inset 0 1px 0 rgba(255,255,255,0.65); - } - /* index page */ - .idx-hero { - padding: 32px 40px 24px; - background: - radial-gradient(60% 80% at 100% 0%, oklch(88% 0.10 215 / 0.5) 0%, transparent 60%), - linear-gradient(to bottom, oklch(94% 0.03 220), oklch(98% 0.004 220)); - border-bottom: 1px solid oklch(88% 0.02 220); - } - .idx-eyebrow { font: 500 10px "IBM Plex Mono", monospace; letter-spacing: 2px; text-transform: uppercase; color: oklch(45% 0.10 230); margin-bottom: 8px; } - .idx-title { font: 700 38px/1.05 "Georgia", "Times New Roman", serif; color: oklch(22% 0.06 235); letter-spacing: -1px; margin: 0 0 10px; } - .idx-title em { font-style: italic; font-weight: 400; color: oklch(45% 0.12 220); } - .idx-sub { font: italic 14px/1.5 "Georgia", serif; color: oklch(40% 0.04 235); max-width: 440px; margin: 0; } - .idx-list { padding: 16px 40px 40px; } - .idx-art { display: grid; grid-template-columns: 60px 1fr auto; gap: 18px; padding: 18px 0; border-bottom: 1px solid oklch(90% 0.015 220); cursor: pointer; transition: padding 120ms; } - .idx-art:last-child { border-bottom: 0; } - .idx-art:hover { padding-left: 6px; } - .idx-art:hover .idx-art-title { color: oklch(40% 0.16 230); text-decoration: underline; text-decoration-color: oklch(70% 0.13 220 / 0.5); text-underline-offset: 3px; } - .idx-art-num { font: 600 28px/1 "IBM Plex Mono", monospace; color: oklch(80% 0.04 225); padding-top: 4px; } - .idx-art-title { font: 700 18px/1.25 "Georgia", serif; color: oklch(22% 0.06 235); margin: 0 0 4px; letter-spacing: -0.2px; } - .idx-art-excerpt { font: 13px/1.55 "Georgia", serif; color: oklch(40% 0.04 235); margin: 4px 0 0; } - .idx-art-meta { font: 11px "IBM Plex Mono", monospace; color: oklch(55% 0.06 230); text-align: right; padding-top: 4px; line-height: 1.6; white-space: nowrap; } - .idx-art-tag { display: inline-block; padding: 1px 6px; border-radius: 3px; background: oklch(92% 0.04 220); color: oklch(40% 0.10 230); font-size: 10px; text-transform: lowercase; letter-spacing: 0.5px; } - /* article page */ - .art-page { padding: 36px 56px 60px; max-width: 620px; margin: 0 auto; } - .art-back { display: inline-flex; align-items: center; gap: 6px; font: 500 11px "IBM Plex Mono", monospace; color: oklch(45% 0.10 230); text-decoration: none; margin-bottom: 24px; cursor: pointer; letter-spacing: 1px; text-transform: uppercase; } - .art-back:hover { color: oklch(35% 0.16 230); } - .art-eyebrow { font: 500 10px "IBM Plex Mono", monospace; letter-spacing: 2px; text-transform: uppercase; color: oklch(55% 0.10 220); margin-bottom: 12px; } - .art-title { font: 700 32px/1.15 "Georgia", "Times New Roman", serif; color: oklch(20% 0.06 235); letter-spacing: -0.6px; margin: 0 0 14px; } - .art-byline { font: italic 13px/1.5 "Georgia", serif; color: oklch(48% 0.04 235); margin: 0 0 32px; padding-bottom: 16px; border-bottom: 1px solid oklch(88% 0.015 220); } - .art-body { font-size: 15px; line-height: 1.75; color: oklch(22% 0.03 240); } - .art-body p { margin: 0 0 18px; text-wrap: pretty; } - .art-body p:first-of-type::first-letter { font-size: 56px; font-weight: 700; float: left; line-height: 0.9; padding: 4px 8px 0 0; color: oklch(40% 0.14 220); font-family: "Georgia", serif; } - .art-body em { color: oklch(35% 0.10 230); } - .art-body a, .art-body .ilink { color: oklch(40% 0.16 230); text-decoration: underline; text-decoration-color: oklch(70% 0.13 220 / 0.4); text-underline-offset: 2px; cursor: pointer; } - .art-body h2 { font: 700 18px "Georgia", serif; color: oklch(22% 0.06 235); margin: 32px 0 12px; letter-spacing: -0.2px; } - .art-body blockquote { margin: 24px 0; padding: 0 0 0 18px; border-left: 3px solid oklch(78% 0.10 220); font-style: italic; color: oklch(38% 0.06 230); } - .art-body code { font: 13px "IBM Plex Mono", monospace; background: oklch(94% 0.01 220); padding: 1px 5px; border-radius: 3px; color: oklch(35% 0.10 230); } - .art-foot { margin-top: 40px; padding-top: 20px; border-top: 1px solid oklch(88% 0.015 220); font: 12px "IBM Plex Mono", monospace; color: oklch(55% 0.06 230); display: flex; justify-content: space-between; align-items: center; } - .art-foot .more { color: oklch(40% 0.16 230); cursor: pointer; } -</style> +<link rel="stylesheet" href="index.css" /> </head> <body> <script>(function(){var t=localStorage.getItem('tyler.theme')||'aero';document.body.setAttribute('data-theme',t);})();</script> @@ -358,11 +87,11 @@ <div class="titlebar"><div class="dots"><div class="dot r no-drag" onclick="this.closest('.win').style.display='none'"></div><div class="dot y"></div><div class="dot g"></div></div>last.fm — trollshotlol</div> <div class="body" id="np-host"> <div id="np-card"></div> - <div style="margin-top: 14px; font-size: 11px; opacity: 0.75; text-transform: uppercase; letter-spacing: 1px;">recent</div> - <div id="np-recent" style="margin-top: 6px; display: flex; flex-direction: column; gap: 6px; font-size: 12px;"> - <div style="display:flex;justify-content:space-between;"><span>Stella by Starlight · Bill Evans</span><span style="opacity:0.6">2m</span></div> - <div style="display:flex;justify-content:space-between;"><span>Body and Soul · Coleman Hawkins</span><span style="opacity:0.6">9m</span></div> - <div style="display:flex;justify-content:space-between;"><span>Flamingo · Kero Kero Bonito</span><span style="opacity:0.6">38m</span></div> + <div class="np-recent-label">recent</div> + <div id="np-recent"> + <div class="np-row"><span>Stella by Starlight · Bill Evans</span><span class="np-ago">2m</span></div> + <div class="np-row"><span>Body and Soul · Coleman Hawkins</span><span class="np-ago">9m</span></div> + <div class="np-row"><span>Flamingo · Kero Kero Bonito</span><span class="np-ago">38m</span></div> </div> </div> </div> @@ -614,1037 +343,7 @@ </div> <script src="aero.js"></script> -<script> - Aero.initTheme(); - const desk = document.getElementById('desk'); - Aero.makeClouds(document.getElementById('clouds')); - Aero.spawnBubbles(desk, 24); - Aero.sparkleCursor(); - Aero.mountThemeSwitcher(); - - document.querySelectorAll('.win').forEach(w => { - Aero.makeDraggable(w, w.querySelector('.titlebar')); - Aero.makeResizable(w, { minW: 300, minH: 220 }); - }); - - // Sticky note — draggable, lightly random tilt so each load feels handmade - const sticky = document.getElementById('sticky'); - if (sticky) { - const rot = (-4 + Math.random() * 3).toFixed(2); - sticky.style.transform = `rotate(${rot}deg)`; - Aero.makeDraggable(sticky, sticky); - } - - // Webring — silly old-internet behaviour - const wrPrev = document.getElementById('wr-prev'); - const wrNext = document.getElementById('wr-next'); - const wrRand = document.getElementById('wr-rand'); - const wrCount = document.getElementById('wr-count'); - const ringSites = [ - 'mireia.computer', 'jaylim.fm', 'trinh.computer', 'arielab.dev', - 'linhwrites.net', 'marco.audio', 'smallgreenpix.org', 'amalia.zone', - 'thursday.cafe', 'soft.garden', 'foglamp.club', 'kanji.coffee' - ]; - let ringIdx = 42; - function ringHop(dir) { - ringIdx = Math.max(1, Math.min(184, ringIdx + dir)); - wrCount.textContent = `site ${ringIdx} / 184`; - const site = ringSites[Math.floor(Math.random() * ringSites.length)]; - wrCount.title = `would surf to ${site}`; - wrCount.animate( - [{ opacity: 0.3 }, { opacity: 0.75 }], - { duration: 280, easing: 'ease-out' } - ); - } - if (wrPrev) wrPrev.addEventListener('click', e => { e.preventDefault(); ringHop(-1); }); - if (wrNext) wrNext.addEventListener('click', e => { e.preventDefault(); ringHop(1); }); - if (wrRand) wrRand.addEventListener('click', e => { - e.preventDefault(); - ringIdx = 1 + Math.floor(Math.random() * 184); - wrCount.textContent = `site ${ringIdx} / 184`; - wrCount.animate( - [{ transform: 'scale(0.9)', opacity: 0.3 }, { transform: 'scale(1)', opacity: 0.75 }], - { duration: 320, easing: 'ease-out' } - ); - }); - - document.querySelectorAll('.icon').forEach(ic => { - ic.addEventListener('dblclick', () => { - const key = ic.dataset.open; - const w = document.getElementById('w-' + key); - if (w) { w.style.display = ''; w.style.zIndex = (++window.__zTop || (window.__zTop = 100)); } - }); - ic.addEventListener('click', () => { - const key = ic.dataset.open; - const w = document.getElementById('w-' + key); - if (w) { w.style.display = ''; w.style.zIndex = (++window.__zTop || (window.__zTop = 100)); } - }); - }); - - // podcast RSS - let podLoaded = false; - async function loadPodcast() { - if (podLoaded) return; - podLoaded = true; - try { - const { art, episodes } = await Aero.fetchReelMouthFeed(6); - if (art) { - const img = document.getElementById('pod-art'); - const ph = document.getElementById('pod-art-placeholder'); - img.src = art; - img.style.display = ''; - if (ph) ph.style.display = 'none'; - } - const container = document.getElementById('pod-episodes'); - if (episodes.length) { - container.innerHTML = episodes.map(e => - `<div style="display:flex;justify-content:space-between;gap:8px;"> - <a href="${e.url}" target="_blank" rel="noopener" style="color:inherit;text-decoration:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${e.title}">${e.title.toLowerCase()}</a> - <span style="opacity:0.6;flex-shrink:0;">${e.duration}</span> - </div>` - ).join(''); - } else { - container.innerHTML = '<div style="opacity:0.5;font-style:italic;">no episodes found</div>'; - } - } catch (err) { - document.getElementById('pod-episodes').innerHTML = '<div style="opacity:0.5;font-style:italic;">couldn\'t load feed</div>'; - } - } - document.querySelectorAll('.icon[data-open="podcast"]').forEach(ic => { - ic.addEventListener('click', loadPodcast); - ic.addEventListener('dblclick', loadPodcast); - }); - - // music toggle + volume slider (theme-aware) - const MUSIC = { - aero: { src: '/mus/bazaar-theme.mp3', label: '♪ a-dog — bazaar theme' }, - chrome: { src: '/mus/CoolMan - WhoIsUsingThisComputer_.mp3', label: '♪ coolman — who is using this computer?' }, - }; - document.getElementById('mt').innerHTML = Aero.musicToggleHTML(); - const mtDiv = document.getElementById('mt'); - const mtBtn = mtDiv.querySelector('.mt-btn'); - const mtLabel = mtDiv.querySelector('.mt-label'); - const bgm = document.getElementById('bgm'); - bgm.volume = 0.2; - - const vol = document.createElement('input'); - vol.type = 'range'; vol.min = '0'; vol.max = '1'; vol.step = '0.01'; vol.value = '0.2'; - vol.className = 'no-drag aero-vol'; - vol.title = 'volume'; - vol.style.cssText = 'width:70px;margin-left:6px;cursor:pointer;accent-color:oklch(55% 0.13 230);vertical-align:middle;'; - mtDiv.querySelector('.music-toggle').appendChild(vol); - vol.addEventListener('input', () => { bgm.volume = Number(vol.value); }); - - let musicOn = false; - - function applyMusicTheme() { - const track = MUSIC[Aero.getTheme()] || MUSIC.aero; - if (bgm.getAttribute('src') !== track.src) { - const pos = bgm.currentTime; - bgm.src = track.src; - if (musicOn) { bgm.load(); bgm.currentTime = 0; bgm.play(); } - } - if (musicOn) mtLabel.textContent = (MUSIC[Aero.getTheme()] || MUSIC.aero).label; - } - - mtBtn.addEventListener('click', () => { - musicOn = !musicOn; - if (musicOn) { - const track = MUSIC[Aero.getTheme()] || MUSIC.aero; - bgm.src = track.src; - bgm.load(); - bgm.play(); - mtBtn.textContent = '❚❚'; - mtLabel.textContent = track.label; - mtBtn.style.background = 'radial-gradient(circle at 30% 25%,white,oklch(75% 0.14 55) 60%,oklch(55% 0.15 35))'; - } else { - bgm.pause(); - mtBtn.textContent = '▶'; - mtLabel.textContent = 'music off'; - mtBtn.style.background = 'radial-gradient(circle at 30% 25%,white,oklch(82% 0.12 220) 60%,oklch(50% 0.13 240))'; - } - }); - - // swap track live when theme changes while music is playing - window.addEventListener('themechange', applyMusicTheme); - - // autoplay on load; fall back to first click if browser blocks it - (function() { - const track = MUSIC[Aero.getTheme()] || MUSIC.aero; - bgm.src = track.src; - bgm.load(); - bgm.play().then(() => { - musicOn = true; - mtBtn.textContent = '❚❚'; - mtLabel.textContent = track.label; - mtBtn.style.background = 'radial-gradient(circle at 30% 25%,white,oklch(75% 0.14 55) 60%,oklch(55% 0.15 35))'; - }).catch(() => { - document.addEventListener('click', () => { - const t = MUSIC[Aero.getTheme()] || MUSIC.aero; - bgm.src = t.src; - bgm.load(); - bgm.play().then(() => { - musicOn = true; - mtBtn.textContent = '❚❚'; - mtLabel.textContent = t.label; - mtBtn.style.background = 'radial-gradient(circle at 30% 25%,white,oklch(75% 0.14 55) 60%,oklch(55% 0.15 35))'; - }); - }, { once: true }); - }); - })(); - - // counter - document.getElementById('cc').innerHTML = Aero.counterHTML(0, 'visitors'); - Aero.fetchVisitorCount() - .then(n => { - document.getElementById('cc').innerHTML = Aero.counterHTML(n, 'visitors'); - }) - .catch(() => {}); - - // now playing - document.getElementById('np-card').innerHTML = Aero.nowPlayingHTML(false); - Aero.animateEq(document.getElementById('np-card')); - - // fetch last.fm - Aero.fetchLastFm() - .then(tracks => { - if (tracks && tracks.length > 0) { - document.getElementById('np-card').innerHTML = Aero.nowPlayingHTML(false, tracks[0]); - Aero.animateEq(document.getElementById('np-card')); - - const recentDiv = document.getElementById('np-recent'); - if (tracks.length > 1) { - recentDiv.innerHTML = tracks.slice(1, 4).map(t => { - const ago = t.when ? Math.floor((Date.now() - t.when) / 60000) : 0; - const timeStr = ago < 60 ? ago + 'm' : Math.floor(ago / 60) + 'h'; - return `<div style="display:flex;justify-content:space-between;"><span>${t.artist ? t.artist + ' — ' : ''}${t.name}</span><span style="opacity:0.6">${timeStr}</span></div>`; - }).join(''); - } - } - }) - .catch(() => {}); - - // clock - function tick() { - const d = new Date(); - document.getElementById('clock').textContent = - d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }); - } - tick(); setInterval(tick, 30000); - - // guestbook send (fake but cute) - const gbBtn = document.getElementById('gb-send'); - if (gbBtn) { - gbBtn.addEventListener('click', async () => { - const name = document.getElementById('gb-name').value.trim(); - const email = document.getElementById('gb-email').value.trim(); - const message = document.getElementById('gb-msg').value.trim(); - const status = document.getElementById('gb-status'); - if (!name || !email || !message) { status.textContent = '— fill in all three fields —'; return; } - status.textContent = 'sending…'; - gbBtn.disabled = true; - try { - const res = await fetch('https://tylerhoang.xyz/api/contact', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name, email, message }), - }); - const data = await res.json().catch(() => ({})); - if (res.ok) { - status.textContent = '✓ sent! talk soon'; - document.getElementById('gb-name').value = ''; - document.getElementById('gb-email').value = ''; - document.getElementById('gb-msg').value = ''; - } else { - status.textContent = data.error || '✗ something went wrong'; - } - } catch { - status.textContent = '✗ could not reach server'; - } finally { - gbBtn.disabled = false; - setTimeout(() => { status.textContent = '— takes ~2 days for a reply —'; }, 5000); - } - }); - } - - /* ============= FAUX BROWSER ============= - Articles live in the ARTICLES array below. - Each `body` is plain HTML — use <p>, <h2>, <blockquote>, <em>, <code>, - and <span class="ilink" data-go="/articles/slug"> for in-page links. - ============================================== */ - const ARTICLES = [ - { - slug: 'personal-library', - title: 'Personal <em>Library</em>', - date: '2022-01-01', - tag: 'books', - excerpt: 'Cookbooks, piano methods, and fake books I actually own and use.', - body: ` - <p>I'm not going to pretend that I'm an avid reader, but I do own a few books that I reference regularly. Here's what's on the shelf.</p> - <h2>Cooking</h2> - <p><em>Mastering the Art of French Cooking</em> by Julia Child is one of the best cookbooks ever made. Dense, encyclopedic, filled with techniques and detailed illustrations. Not just a recipe book — a guide on how to think about cooking. I've referenced it countless times when I don't know what to make for dinner, and every time the dish turns out great.</p> - <p><em>The Food Lab</em> by J. Kenji López-Alt is the modernized version. Cooking at a scientific level, every technique explained with a <em>why</em>. Say what you want about Kenji, but this book is the real deal. I'd call it a modernized Julia Child — it really is more of an encyclopedia than a cookbook. You don't need to read all of it. Just look at what interests you.</p> - <p><em>Mastering the Art of Chinese Cooking</em> by Eileen Yin-Fei Lo covers hundreds of recipes with cultural context and history. My knowledge of Chinese cooking is limited but a lot of the material in there looks delicious. <em>Complete Chinese Cookbook</em> by Ken Hom came highly recommended by basically everyone trying to get into Chinese cooking.</p> - <p><em>Tokyo Cult Recipes</em> by Maori Murota — lots of pictures, lots of text, lots of recipes. My friend had a copy and recommended I check it out.</p> - <h2>Piano</h2> - <p><em>Jazz Piano Method</em> by Mark Davis has everything you need as a total beginner to start your journey as a jazz pianist. It's not a long book, but don't rush through it — the exercises are dense and it might take a whole year to really absorb everything in it. Develop the muscle memory. Don't get frustrated.</p> - <p><em>Jazz Piano — Methods and Songbook for Professional Playing</em> by Kent Hewitt was my first jazz piano book. Not quite as polished as the Mark Davis book, but the content is amazing. Hewitt puts soul into it, explains things simply, and makes learning fun. Once you start getting better though, you'll need to rely more on just listening and playing.</p> - <p><em>The Real Book, 6th Edition</em> — industry standard for jazz lead sheets. Many inaccuracies, but if you could only have one fake book this is the one. Use it as a guide to learn melodies, then try to memorize the changes and embellish the melody without looking.</p> - <p><em>The Disney Fake Book</em> is great fun. Watch out for alternate keys — <em>Someday My Prince Will Come</em> is written in F instead of B♭ for some reason.</p> - `, - }, - { - slug: 'music-list', - title: 'Music <em>Collection</em>', - date: '2022-01-01', - tag: 'music', - excerpt: 'Everything in my library. ~500 albums from jazz to hyperpop to city pop.', - body: ` - <p>Generated from my music directory via <code>tree</code>. No commentary, just the list.</p> - <pre style="font-size:11px;line-height:1.6;color:inherit;background:rgba(0,0,0,0.04);padding:16px;border-radius:8px;overflow-x:auto;white-space:pre-wrap">Music -├── 100 gecs -│ ├── 1000 gecs -│ └── mememe -├── 100 gecs feat. Charli XCX, Rico Nasty, Kero Kero Bonito -│ └── ringtone (remix) -├── 2814 -│ └── 新しい日の誕生 -├── 3776 -│ └── 歳時記 -├── 385 -│ └── 脳みそあらおう -├── Aimer -│ ├── BEST SELECTION "blanc" -│ ├── BEST SELECTION "noir" -│ ├── Penny Rain -│ └── Sun Dance -├── Aiobahn feat. nayuta -│ └── 過ぎゆく日と君へ -├── All That Jazz -│ ├── EVER JAZZ -│ ├── Ghibli Jazz -│ └── Ghibli Jazz 2 -├── Art Blakey & The Jazz Messengers -│ └── The Big Beat -├── Astra King -│ └── Silver -├── BABYMETAL -│ ├── BABYMETAL -│ ├── METAL GALAXY -│ └── METAL RESISTANCE -├── BENEE -│ └── Hey U X -├── Bill Conti -│ └── Rocky: Original Motion Picture Score -├── Bill Evans -│ ├── Everybody Digs Bill Evans -│ ├── Some Other Time: The Lost Session From the Black Forest -│ └── You Must Believe in Spring -├── Bill Evans & Jim Hall -│ ├── Intermodulation -│ └── Undercurrent -├── Bill Evans Trio -│ ├── Explorations -│ ├── Portrait in Jazz -│ ├── Sunday at the Village Vanguard -│ └── Waltz for Debby -├── Billie Eilish -│ ├── Happier Than Ever -│ └── WHEN WE ALL FALL ASLEEP, WHERE DO WE GO? -├── Billie Holiday -│ └── Billie's Best -├── Björk -│ ├── Homogenic -│ └── Vespertine -├── Björk & Tríó Guðmundar Ingólfssonar -│ └── Gling-Gló -├── Blonde Redhead -│ └── 23 -├── Boredoms -│ ├── Super æ -│ └── VISION CREATION NEWSUN -├── C418 -│ ├── Minecraft, Volume Alpha -│ └── Minecraft, Volume Beta -├── Casiopea -│ ├── Casiopea -│ └── Mint Jams -├── Charles Mingus -│ └── Mingus Ah Um -├── Choro Club -│ └── ヨコハマ買い出し紀行 -Quiet Country Cafe- Original Soundtrack -├── Compilations -│ ├── EVANGELION FINALLY -│ ├── SUPER EUROBEAT presents INITIAL D ~D SELECTION~ -│ ├── Super Eurobeat Presents Initial D Fifth Stage D Selection -│ ├── Super Eurobeat Presents Initial D Second Stage Non-Stop Selection -│ ├── The Original Jazz Masters Series, Volume 1 -│ └── mikgazer vol.1 -├── DAOKO -│ ├── DAOKO -│ ├── THANK YOU BLUE -│ └── anima -├── Daft Punk -│ ├── Discovery -│ ├── Homework -│ ├── Human After All -│ └── Random Access Memories -├── Death Grips -│ ├── Exmilitary -│ ├── No Love Deep Web -│ └── The Money Store -├── EGOIST -│ └── GREATEST HITS 2011-2017 "ALTER EGO" -├── Eric Dolphy -│ └── Out to Lunch! -├── Fiona Apple -│ └── Fetch the Bolt Cutters -├── Fleetwood Mac -│ └── Then Play On -├── Friday Night Plans -│ └── Plastic Love -├── Godspeed You! Black Emperor -│ └── Lift Yr. Skinny Fists Like Antennas to Heaven! -├── Gus Dapperton -│ ├── Moodna, Once with Grace -│ └── You Think You're a Comic! -├── Gábor Szabó -│ └── Dreams -├── Hello Sleepwalkers -│ └── Masked Monkey Awakening -├── Hyper Potions, Synthion & MYLK -│ └── Maboroshi -├── Japanese Breakfast -│ └── Jubilee -├── John Coltrane -│ ├── A Love Supreme -│ ├── Giant Steps -│ └── My Favorite Things -├── John Coltrane Quartet -│ └── Ballads -├── Justin Hurwitz, Benj Pasek & Justin Paul -│ └── La La Land: Original Motion Picture Soundtrack -├── KANA-BOON -│ ├── DOPPEL -│ └── TIME -├── KOTO -│ ├── ばいばいてぃーんずららばい -│ └── プラトニック プラネット (Platonic Planet) -├── Kanye West -│ ├── 808s & Heartbreak -│ └── My Beautiful Dark Twisted Fantasy -├── Kazumi Tateishi Trio -│ ├── GHIBLI meets JAZZ ~Beautiful Songs~ -│ └── Smile ~Beautiful Song in Jazz~ -├── Keith Jarrett -│ └── The Köln Concert -├── Ken Ishii -│ └── Jelly Tones -├── Kendrick Lamar -│ ├── DAMN. -│ └── To Pimp a Butterfly -├── Kero Kero Bonito -│ ├── Bonito Generation -│ ├── Civilisation I -│ ├── Flamingo -│ ├── Heartbeat -│ ├── Intro Bonito -│ ├── TOTEP -│ ├── The Princess and the Clock -│ └── Time 'n' Place -├── Kim Petras -│ └── Slut Pop -├── King Crimson -│ └── In the Court of the Crimson King -├── Lamp -│ └── 恋人へ -├── Lena Raine -│ └── Minecraft Nether Update (Original Game Soundtrack) -├── LiSA -│ ├── LiSA BEST -Day- -│ ├── LiSA BEST -Way- -│ └── 紅蓮華 -├── Lil Mariko -│ └── Lil Mariko -├── Lil Pump -│ └── Lil Pump -├── Linked Horizon -│ └── 真実への進撃 -├── M.O.O.N. -│ └── Moon - EP -├── MASS OF THE FERMENTING DREGS -│ ├── MASS OF THE FERMENTING DREGS -│ ├── No New World -│ └── ゼロコンマ、色とりどりの世界 -├── MONDO GROSSO -│ └── 何度でも新しく生まれる -├── MYTH & ROID -│ ├── PANTA RHEI -│ ├── TIT FOR TAT -│ ├── VORACITY -│ ├── eYe's -│ └── shadowgraph -├── Magdalena Bay -│ └── Mercurial World -├── Masayoshi Minoshima -│ └── Bad Apple!! -├── Matt Uelmen -│ └── Diablo II Soundtrack -├── Melt-Banana -│ ├── Cell-Scape -│ └── Fetch -├── Miles Davis -│ └── Kind of Blue -├── Mingus Big Band -│ └── Nostalgia in Times Square -├── Moe Shop -│ ├── Moe Moe -│ └── Moshi Moshi -├── My Bloody Valentine -│ └── Loveless -├── NUMBER GIRL -│ ├── NUM-HEAVYMETALLIC -│ └── SCHOOL GIRL DISTORTIONAL ADDICT -├── Nacio Herb Brown -│ └── Singin' in the Rain -├── Neutral Milk Hotel -│ └── In the Aeroplane Over the Sea -├── Night Tempo -│ └── Fantasy -├── Nirvana -│ └── Nevermind -├── Ogre You Asshole -│ └── OGRE YOU ASSHOLE -├── Olivia Rodrigo -│ └── SOUR -├── Otoboke Beaver -│ └── Itekoma Hits -├── Otomo Yoshihide's New Jazz Ensemble -│ └── Dreams -├── POLYSICS -│ └── Hey! Bob! My Friend! -├── PSYQUI feat. Such -│ └── ヒステリックナイトガール -├── Panchiko -│ └── D_E_A_T_H_M_E_T_A_L -├── Party In Backyard & PewDiePie -│ └── Bitch Lasagna -├── Paul Hardcastle / Pigbag -│ └── Papa's Got A Brand New Pigbag -├── Perfume -│ ├── COSMIC EXPLORER -│ ├── Cling Cling -│ ├── Future Pop -│ ├── GAME -│ ├── JPN -│ ├── LEVEL3 -│ ├── ⊿ -│ └── ナナナナナイロ -├── RADWIMPS -│ ├── Weathering With You -│ └── Your Name (Original Motion Picture Soundtrack) -├── Red Velvet -│ └── Perfect Velvet -├── Reol -│ ├── Sigma -│ ├── エンドレスEP -│ ├── 事実上 -│ ├── 極彩色 -│ └── 金字塔 -├── Rina Sawayama -│ └── SAWAYAMA -├── SAINT PEPSI -│ └── Hit Vibes -├── SOIL&"PIMP"SESSIONS -│ ├── 6 -│ └── Pimp of The Year -├── SOPHIE -│ ├── Faceshopping -│ └── OIL OF EVERY PEARL'S UN-INSIDES -├── SUGAR BABE -│ └── SONGS -├── SUPERCAR -│ └── HIGHVISION -├── Sarah Vaughan -│ └── Sarah Vaughan -├── Shibayan Records -│ ├── Adrastea -│ ├── TOHO BOSSA NOVA 2 -│ ├── TOHO BOSSA NOVA 5 -│ ├── TOHO BOSSA NOVA 7 -│ ├── TOHO BOSSA NOVA 8 -│ └── TOHO BOSSA NOVA 9 -├── Shinichi Osawa -│ └── The One -├── Snail's House -│ ├── Ordinary Songs -│ ├── Ordinary Songs 2 -│ └── Snö -├── Sonny Clark -│ └── Leapin' and Lopin' -├── Stan Getz / João Gilberto featuring Antônio Carlos Jobim -│ └── Getz/Gilberto -├── THE ORAL CIGARETTES -│ └── FIXION -├── TK -│ └── unravel -├── TOHO JAZZ MESSENGERS -│ ├── girls apartment -│ └── girls apartment 2 -├── Taylor Swift -│ └── Red (Taylor's version) -├── Tessa Violet -│ └── Bad Ideas -├── The Beatles -│ └── Abbey Road -├── The Dave Brubeck Quartet -│ └── Time Out -├── The Strokes -│ └── Is This It -├── The Velvet Underground -│ └── The Velvet Underground & Nico -├── Tomggg -│ └── Butter Sugar Cream -├── Tzusing -│ └── 東方不敗 -├── WhaleDontSleep -│ └── ねむるまち (feat. yama) -├── YUC'e -│ └── macaron moon -├── YUKIKA -│ └── 서울여자 -├── Yaeji -│ └── EP2 -├── Yellow Magic Orchestra -│ ├── Naughty Boys -│ ├── Solid State Survivor -│ └── Yellow Magic Orchestra -├── Yeule -│ └── Serotonin II -├── Yunomi -│ └── ゆのもきゅ -├── Yunomi feat. TORIENA -│ └── 大江戸コントローラー EP -├── Yunomi feat. nicamoq -│ └── ゆのみっくにお茶して EP -├── Yunomi feat. ローラーガール -│ └── ジェリーフィッシュ -├── Yunomi feat. 桃箱 -│ └── ミラクルシュガーランド -├── Zazen Boys -│ └── ZAZEN BOYS -├── Zedd -│ └── Clarity -├── Zedd & Alessia Cara -│ └── Stay -├── bbno$ -│ └── my oh my -├── masara -│ └── Love10 -├── mus.hiba -│ └── White Girl -├── pidalso -│ └── The Ocean Waves OST Piano Cover Collection -├── toe -│ └── the book about my idle plot on a vague anxiety -├── tricot -│ ├── A N D -│ ├── T H E -│ └── 真っ黒 -├── かめりあ -│ └── heart of android -├── きのこ帝国 -│ └── eureka -├── きゃりーぱみゅぱみゅ -│ ├── Nanda Collection -│ └── キャンディーレーサー -├── ずっと真夜中でいいのに。 -│ ├── 今は今で誓いは笑みで -│ └── 潜潜話 -├── はっぴいえんど -│ └── 風街ろまん -├── ぼくたちのいるところ。 -│ └── ごみ -├── ギガP -│ └── No title− -├── シートベルツ -│ └── COWBOY BEBOP -├── パスピエ -│ └── 演出家出演 -├── ピンクネオン東京 -│ └── Single Collection Vol.2 -├── フィッシュマンズ -│ ├── Long Season -│ └── 空中キャンプ -├── フレデリック -│ ├── oddloop -│ ├── フレデリズム -│ └── フレデリズム2 -├── フレネシ -│ └── キュプラ -├── ポルカドットスティングレイ -│ ├── JET -│ └── 新世紀 -├── ミカヅキBIGWAVE -│ ├── WAVESウェーブス - EP -│ └── 星空ROMANTIC -├── ミドリ -│ ├── shinsekai -│ ├── あらためまして、はじめまして、ミドリです。 -│ ├── セカンド -│ ├── ファースト -│ └── 清水 -├── ヨルシカ -│ ├── だから僕は音楽を辞めた -│ ├── エルマ -│ ├── 夏草が邪魔をする -│ ├── 盗作 -│ ├── 花に亡霊 -│ └── 負け犬にアンコールはいらない -├── ラブリーサマーちゃん -│ └── LSC -├── 三月のパンタシア -│ └── ガールズブルー・ハッピーサッド -├── 佐井好子 -│ └── 萬花鏡 -├── 八神純子 -│ └── FULL MOON -├── 凛として時雨 -│ └── Inspiration is DEAD -├── 原神玲 -│ └── Lust -├── 喜多嶋修 -│ └── Benzaiten -├── 坂本龍一 -│ └── Merry Christmas Mr. Lawrence -├── 大森靖子 -│ ├── 大森靖子 -│ └── 洗脳 -├── 大比良瑞希 -│ └── TRUE ROMANCE -├── 大貫妙子 -│ ├── Mignonne -│ ├── SUNSHOWER -│ └── copine -├── 宇多田ヒカル -│ └── ULTRA BLUE -├── 山下達郎 -│ └── FOR YOU -├── 山崎ハコ -│ └── 飛・び・ま・す -├── 戸川純 -│ ├── 好き好き大好き -│ └── 玉姫様 -├── 新井正人 -│ └── MASAHITO ARAI +1 -├── 春ねむり -│ ├── LOVETHEISM -│ ├── アトム・ハート・マザー -│ └── 春と修羅 -├── 杏里 -│ ├── Heaven Beach -│ └── TIMELY!! -├── 村岡実 -│ └── Bamboo -├── 東京ブラススタイル -│ └── ブラスタジブリ -├── 松下誠 -│ └── FIRST LIGHT -├── 椎名林檎 -│ ├── 加爾基 精液 栗ノ花 -│ └── 無罪モラトリアム -├── 椎名林檎×SOIL&"PIMP"SESSIONS -│ └── カリソメ乙女(DEATH JAZZ ver.) -├── 水曜日のカンパネラ -│ ├── SUPERMAN -│ └── ガラパゴス -├── 永田茂 -│ └── 海がきこえる -├── 沢井美空 -│ └── カラフル。 -├── 津々井まり -│ └── 愛すれど心さびしく -├── 清水靖晃 -│ └── 案山子 -├── 相対性理論 -│ └── ハイファイ新書 -├── 矢野顕子 -│ ├── JAPANESE GIRL -│ └── ただいま。 -├── 神聖かまってちゃん -│ └── つまんね -├── 福居良 -│ ├── Live at Vidro '77 -│ ├── Mellow Dream -│ ├── My Favorite Tune -│ ├── Ryo Fukui In New York -│ └── Scenery -├── 竹内まりや -│ ├── Love Songs -│ └── Variety -├── 米津玄師 -│ └── Flamingo / TEENAGE RIOT -├── 細野晴臣、鈴木茂、山下達郎 -│ └── Pacific -├── 菅野よう子 -│ └── 坂道のアポロン KIDS ON THE SLOPE ORIGINAL SOUNDTRACK -├── 菊池桃子 -│ └── ADVENTURE -├── 銀杏BOYZ -│ └── 君と僕の第三次世界大戦的恋愛革命 -├── 間宮貴子 -│ └── Love Trip -├── 青葉市子 -│ ├── 0 -│ ├── うたびこ -│ └── アダンの風 -├── 飯島真理 -│ └── Rosé -├── 高橋洋子 / Claire -│ └── 残酷な天使のテーゼ / FLY ME TO THE MOON -├── 박혜진 -│ ├── Before I Die -│ └── IF U WANT IT -├── 이달의 소녀 오드아이써클 -│ └── Max & Match -└── 장윤주 - └── Dream - -507 directories</pre> - `, - }, - { - slug: 'software-and-hardware', - title: 'Software & <em>Hardware</em>', - date: '2022-01-01', - tag: 'linux', - excerpt: 'What I run day-to-day. CachyOS, hyprland, vim, and a lot of opinions.', - body: ` - <p>A lot of people like to know what hardware and software I run on a day-to-day basis, so here it is.</p> - <h2>Hardware</h2> - <p><strong>PC:</strong> AMD Ryzen 5 5600x · AMD Radeon 7900XT · 2×16GB G.Skill Trident 3600MHz · Thermalright Phantom Spirit 120 SE · XPG Core Reactor 850W 80+ Gold.</p> - <p><strong>Laptop:</strong> Lenovo ThinkPad T480s.</p> - <p><strong>Mouse:</strong> Logitech G305 wireless. Just works. Great battery life, latency is not noticeable. Fits my hands well. One issue: after years of use, the scroll wheel gets shotty.</p> - <p><strong>Audio:</strong> Dedicated laptop running Daphile (Linux/Squeezebox) for bit-perfect playback → Topping E30 DAC → Topping L30 amp → Onkyo receiver → Edifier P17 speakers or SHP9500 headphones. IEMs: BLON BL03 and Moondrop Aria (2021) — the Arias are mostly flat/neutral with a slight mid-bass hump, super fun to walk around with. Headphones: Beyerdynamic DT 770 Pro 250Ω on the desktop, Phillips SHP9500 on the hifi rig. Mic: Samson Q2U dynamic XLR/USB into a Scarlett Focusrite 2i2.</p> - <h2>Software</h2> - <p><strong>OS:</strong> <em>CachyOS</em> — I used to run Artix, basically Arch without systemd. I don't inherently have anything against systemd, but OpenRC has a fast startup time and I got used to it quickly. I was running Artix for a couple of years and just wanted to change it up a bit.</p> - <p><strong>Terminal:</strong> <code>st</code> (simple terminal) by suckless, although foot is also a good one if you're on wayland</p> - <p><strong>Browser:</strong> Brave. Mozilla keeps destroying Firefox with every update, so I've decided to boycott Firefox and any Mozilla product. Install uBlock Origin and Decentraleyes at minimum, though Brave comes with decent ad/tracking blocking by default.</p> - <p><strong>Text editor:</strong> <code>vim</code>. I used emacs for about a month — didn't hate it, but it didn't fit my workflow. I was already too used to vim.</p> - <p><strong>Window manager:</strong> hyprland. Before that <a href="http://github.com/tyhoang/dwm">dwm</a>. Before that, herbstluftwm. bspwm and i3 are also great — they all basically do the same thing anyway.</p> - <h2>Utilities</h2> - <p><strong>File manager:</strong> vifm — dual-pane, vim bindings, super customizable.</p> - <p><strong>Mail:</strong> Thunderbird.</p> - <p><strong>XMPP:</strong> Gajim on Linux, Conversations on Android, both with OMEMO.</p> - <p><strong>Voice:</strong> Mumble (and Discord when I can't get friends to switch).</p> - <p><strong>Music:</strong> mpd + ncmpcpp + beet for library organization.</p> - <p><strong>Video:</strong> mpv — lightweight, massively scriptable.</p> - <p><strong>RSS:</strong> newsboat. Most sites have an RSS feed; take advantage of them.</p> - <p><strong>Torrents:</strong> rtorrent via Flood UI.</p> - <p><strong>Images:</strong> sxiv</p> - <p><strong>PDF:</strong> zathura.</p> - <p><strong>Image editing:</strong> GIMP, imagemagick for quick tasks.</p> - <p><strong>Documents:</strong> LibreOffice for spreadsheets/slides, LaTeX for anything that needs to look good.</p> - <blockquote>All of the software I use is free and open source. Software that respects my freedom to use, share, and maintain privacy. Proprietary services like Discord, Google Chrome, and Amazon exist to extort your personal data. Switch to libre software.</blockquote> - `, - }, - ]; - - const ARTICLE_INDEX = Object.fromEntries(ARTICLES.map(a => [a.slug, a])); - const HOST = 'fun.tylerhoang.xyz'; - const browserHistory = { stack: [], idx: -1 }; - - function fmtDate(iso) { - const d = new Date(iso); - return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }).toLowerCase(); - } - - function renderIndex() { - const items = ARTICLES.map((a, i) => ` - <div class="idx-art" data-go="/articles/${a.slug}"> - <div class="idx-art-num">${String(i + 1).padStart(2, '0')}</div> - <div> - <div class="idx-art-title">${a.title}</div> - <div class="idx-art-excerpt">${a.excerpt}</div> - </div> - <div class="idx-art-meta"> - <div>${fmtDate(a.date)}</div> - <div><span class="idx-art-tag">${a.tag}</span></div> - </div> - </div> - `).join(''); - return ` - <div class="idx-hero"> - <div class="idx-eyebrow">/notes — Tyler Hoang</div> - <h1 class="idx-title">Things I've been <em>thinking about</em>.</h1> - <p class="idx-sub">Half-finished essays from a banker who'd rather be playing piano. New ones land when they land — usually monthly, never on a schedule.</p> - </div> - <div class="idx-list">${items}</div> - `; - } - - function renderArticle(slug) { - const a = ARTICLE_INDEX[slug]; - if (!a) return `<div class="art-page"><h1 class="art-title">404 — not found</h1><p>That page isn't here.</p><span class="art-back" data-go="/articles">← back to notes</span></div>`; - const i = ARTICLES.findIndex(x => x.slug === slug); - const next = ARTICLES[(i + 1) % ARTICLES.length]; - return ` - <article class="art-page"> - <span class="art-back" data-go="/articles">← all notes</span> - <div class="art-eyebrow">${a.tag} · ${fmtDate(a.date)}</div> - <h1 class="art-title">${a.title}</h1> - <div class="art-byline">by Tyler Hoang · ~${Math.max(2, Math.round(a.body.replace(/<[^>]+>/g,'').split(/\s+/).length / 200))} min read</div> - <div class="art-body">${a.body}</div> - <div class="art-foot"> - <span>↑ thanks for reading</span> - <span class="more" data-go="/articles/${next.slug}">next: ${next.title.replace(/<[^>]+>/g,'')} →</span> - </div> - </article> - `; - } - - function pathToContent(path) { - if (path === '/articles' || path === '/articles/') return renderIndex(); - const m = path.match(/^\/articles\/([a-z0-9-]+)$/); - if (m) return renderArticle(m[1]); - return `<div class="art-page"><h1 class="art-title">404</h1><p>No page at <code>${path}</code>.</p><span class="art-back" data-go="/articles">← back</span></div>`; - } - - function updateChrome(path) { - document.getElementById('br-host').textContent = HOST; - document.getElementById('br-path').textContent = path; - const isArt = /^\/articles\/[a-z0-9-]+$/.test(path); - const slug = isArt ? path.split('/').pop() : null; - const titleTxt = isArt && ARTICLE_INDEX[slug] - ? `${ARTICLE_INDEX[slug].title.replace(/<[^>]+>/g,'')} — fun.tylerhoang.xyz` - : 'Notes — fun.tylerhoang.xyz'; - document.getElementById('br-title').textContent = titleTxt; - document.querySelectorAll('.browser-bookmarks .bm').forEach(bm => { - bm.classList.toggle('active', bm.dataset.go === path); - }); - document.getElementById('br-back').disabled = browserHistory.idx <= 0; - document.getElementById('br-fwd').disabled = browserHistory.idx >= browserHistory.stack.length - 1; - } - - function loadPath(path, pushHist = true) { - const page = document.getElementById('br-page'); - const status = document.getElementById('br-status'); - const progress = document.getElementById('br-progress'); - status.textContent = `Contacting fun.tylerhoang.xyz…`; - progress.style.display = 'block'; - progress.querySelector('.progress-bar').style.width = '20%'; - setTimeout(() => { - progress.querySelector('.progress-bar').style.width = '70%'; - status.textContent = `Reading ${path}…`; - }, 80); - setTimeout(() => { - page.innerHTML = pathToContent(path); - page.scrollTop = 0; - if (pushHist) { - browserHistory.stack = browserHistory.stack.slice(0, browserHistory.idx + 1); - browserHistory.stack.push(path); - browserHistory.idx = browserHistory.stack.length - 1; - } - updateChrome(path); - progress.querySelector('.progress-bar').style.width = '100%'; - setTimeout(() => { - progress.style.display = 'none'; - progress.querySelector('.progress-bar').style.width = '0%'; - status.textContent = 'Done'; - }, 120); - }, 220); - } - - // delegated click handler — anything with data-go inside the browser navigates - document.getElementById('w-browser').addEventListener('click', (e) => { - const t = e.target.closest('[data-go]'); - if (t) { e.preventDefault(); loadPath(t.dataset.go); } - }); - document.getElementById('br-back').addEventListener('click', () => { - if (browserHistory.idx > 0) { browserHistory.idx--; loadPath(browserHistory.stack[browserHistory.idx], false); } - }); - document.getElementById('br-fwd').addEventListener('click', () => { - if (browserHistory.idx < browserHistory.stack.length - 1) { browserHistory.idx++; loadPath(browserHistory.stack[browserHistory.idx], false); } - }); - document.getElementById('br-home').addEventListener('click', () => loadPath('/articles')); - document.getElementById('br-go').addEventListener('click', () => { - loadPath(document.getElementById('br-path').textContent.trim() || '/articles'); - }); - document.getElementById('br-reload').addEventListener('click', (e) => { - const btn = e.currentTarget; - btn.classList.remove('spinning'); void btn.offsetWidth; btn.classList.add('spinning'); - loadPath(browserHistory.stack[browserHistory.idx] || '/articles', false); - }); - // initial page load - loadPath('/articles'); - - // fetch films - Aero.fetchFilms() - .then(data => { - try { - const films = Array.isArray(data) ? data : (data.films || data.data || []); - const filmsList = document.getElementById('films-list'); - - if (films.length > 0) { - const esc = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); - filmsList.innerHTML = films.slice(0, 4).map((film) => { - const rating = Math.max(0, Math.min(3, Number(film.stars ?? film.rating ?? 0))); - const stars = Array.from({ length: 3 }, (_, i) => - i < rating ? '<span class="star on">★</span>' : '<span class="star">★</span>' - ).join(''); - - let when = ''; - const raw = film.date_watched || film.watchedAt; - if (raw) { - const d = new Date(raw); - if (!isNaN(d)) { - const days = Math.floor((Date.now() - d) / 86400000); - if (days <= 0) when = 'today'; - else if (days === 1) when = '1d ago'; - else if (days < 7) when = days + 'd ago'; - else if (days < 30) when = Math.floor(days / 7) + 'w ago'; - else when = Math.floor(days / 30) + 'mo ago'; - } else { - when = String(raw); - } - } - - const poster = film.poster_url || film.posterUrl; - const posterStyle = poster - ? `background: url('${esc(poster).replace(/'/g, '%27')}') center / cover;` - : `background: linear-gradient(135deg, oklch(70% 0.13 220), oklch(40% 0.10 250));`; - - const sub = film.director ? esc(film.director) : (film.note ? esc(film.note) : ''); - - return ` - <div class="film-row"> - <div class="film-poster" style="${posterStyle}"></div> - <div class="film-meta"> - <div class="film-title">${esc(film.title)} <span class="film-year">${esc(film.year || '')}</span></div> - <div class="film-rating">${stars}<span style="opacity:0.6;margin-left:8px;">${when}</span></div> - ${sub ? `<div class="film-note">${sub}</div>` : ''} - </div> - </div> - `; - }).join(''); - } - - const statsDiv = document.getElementById('films-stats'); - if (data.total !== undefined || data.count !== undefined) { - const total = data.total || data.count; - let statsHtml = `<div><strong style="color: oklch(28% 0.08 230); font-size: 13px;">${total}</strong> watched</div>`; - if (data.thisYear !== undefined) { - statsHtml += `<div><strong style="color: oklch(28% 0.08 230); font-size: 13px;">${data.thisYear}</strong> this year</div>`; - } - statsDiv.innerHTML = statsHtml; - } - } catch (err) { - console.error('Films parse error:', err); - } - }) - .catch(err => { - console.error('Films fetch error:', err); - }); -</script> +<script src="articles.js"></script> +<script src="index.js"></script> </body> </html> |
