site/static/js/status.js (3433B)
1 // The bar pinned to the bottom of the page is ommp's own status bar. It reads
2 // the page the way the player reads a track: how far in you are, and what is
3 // playing right now. Nothing here is required to read the page — if this file
4 // never loads, the bar simply sits at 0:00.
5 (function () {
6 "use strict";
7
8 var fill = document.querySelector(".sb-fill");
9 var track = document.querySelector(".sb-track");
10 var elapsed = document.querySelector(".sb-elapsed");
11 if (!fill || !track || !elapsed) return;
12
13 var TOTAL = 237; // 3:57, the length of the track in the screenshot
14 var sections = Array.prototype.slice.call(document.querySelectorAll("[data-section]"));
15
16 function clock(seconds) {
17 var s = Math.round(seconds);
18 return Math.floor(s / 60) + ":" + String(s % 60).padStart(2, "0");
19 }
20
21 function update() {
22 var scrollable = document.documentElement.scrollHeight - window.innerHeight;
23 var progress = scrollable > 0 ? Math.min(1, Math.max(0, window.scrollY / scrollable)) : 0;
24
25 fill.style.width = (progress * 100).toFixed(2) + "%";
26 elapsed.textContent = clock(progress * TOTAL);
27
28 // Whichever labelled section has most recently crossed the middle of the
29 // viewport is the one you are reading.
30 var middle = window.innerHeight / 2;
31 var current = "ommp";
32 for (var i = 0; i < sections.length; i++) {
33 if (sections[i].getBoundingClientRect().top <= middle) {
34 current = sections[i].getAttribute("data-section");
35 }
36 }
37 if (track.textContent !== current) track.textContent = current;
38 }
39
40 var queued = false;
41 function onScroll() {
42 if (queued) return;
43 queued = true;
44 requestAnimationFrame(function () { queued = false; update(); });
45 }
46
47 addEventListener("scroll", onScroll, { passive: true });
48 addEventListener("resize", onScroll);
49 update();
50
51 // Copy a command. The button is its own element beside the box, so the box
52 // never resizes — the icon swaps and that is the whole feedback.
53 document.querySelectorAll(".cmd-copy").forEach(function (btn) {
54 var cmd = btn.parentNode.querySelector(".cmd");
55 if (!cmd) return;
56 btn.addEventListener("click", function () {
57 if (!navigator.clipboard) return;
58 // textContent, so the "$ " prompt drawn by CSS is left behind.
59 navigator.clipboard.writeText(cmd.textContent.trim()).then(function () {
60 btn.classList.add("is-copied");
61 setTimeout(function () { btn.classList.remove("is-copied"); }, 1600);
62 });
63 });
64 });
65
66 // The setup you skip crosses itself out as you reach it. The markup ships
67 // already struck, so this first un-strikes it and then puts the lines back
68 // one at a time; if the script never runs, the finished state is what shows.
69 var setup = document.querySelector(".setup-skipped");
70 if (setup && "IntersectionObserver" in window) {
71 var lines = Array.prototype.slice.call(setup.children);
72 setup.parentNode.classList.add("setup-armed");
73
74 var still = matchMedia("(prefers-reduced-motion: reduce)").matches;
75 var strike = new IntersectionObserver(function (entries) {
76 entries.forEach(function (e) {
77 if (!e.isIntersecting) return;
78 strike.unobserve(e.target);
79 lines.forEach(function (li, i) {
80 setTimeout(function () { li.classList.add("is-struck"); }, still ? 0 : i * 260);
81 });
82 });
83 }, { threshold: 0.6 });
84
85 strike.observe(setup);
86 }
87 })();