Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 41 additions & 4 deletions web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>nixamp - it really kicks the DMCA's ass...</title>
<meta name="description" content="nixamp — it really whips the terminal's ass. A Winamp-shaped player for your files and for the nixamp running on your other machine." />
<title>nixamp: open source live streaming from your own machine</title>
<meta name="description" content="An open source player that turns a folder on your machine into a live stream anyone can tune in to. Terminal, web and desktop on one engine. Listen free, publish with one command, charge with x402." />
<link rel="manifest" href="/manifest.webmanifest" />
<meta name="theme-color" content="#080c09" />
<link rel="icon" href="/icons/icon-192.png" type="image/png" />
Expand All @@ -13,8 +13,8 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="nixamp" />
<meta name="mobile-web-app-capable" content="yes" />
<meta property="og:title" content="nixamp - it really kicks the DMCA's ass..." />
<meta property="og:description" content="It really whips the terminal's ass." />
<meta property="og:title" content="nixamp: open source live streaming from your own machine" />
<meta property="og:description" content="Your library, live, from your own machine. Listen free, publish with one command, charge with x402." />
<meta property="og:type" content="website" />
</head>
<body>
Expand All @@ -31,6 +31,43 @@
<button id="install" type="button" class="ghost" hidden>Install</button>
</header>

<!-- The pitch, for somebody who has never seen this before. In the shell
so a crawler and a link preview read it; shown by app.ts only where
accounts live (nixamp.com), only signed out, and only until hidden.
On your own server you already know what this is. -->
<section class="panel welcome player-only" data-title="What this is" id="welcome" hidden>
<h1 class="welcome-title">Your library, live, from your own machine.</h1>
<p class="welcome-lead">
nixamp is an open source player that turns a folder on your computer into a
stream anyone can tune in to. A terminal player, a web app and a desktop app
on one engine. The streaming service you would run yourself: nothing uploaded,
no platform in the middle, and the command line is a first class citizen.
</p>
<div class="welcome-grid">
<div>
<h2>Listen</h2>
<p>See what is on the air right now and press play. No account needed. Every
live stream has a room code you can dial from any phone.</p>
</div>
<div>
<h2>Publish</h2>
<p><code>nixamp serve ~/Music</code> and you are live with a link, a room code
and a phone number. Your recordings, a lecture, a podcast, or a link you paste.</p>
</div>
<div>
<h2>Get paid</h2>
<p>Past five listeners a stream can charge a dollar a day, paid to you through
x402. Only the audio is gated, so the page stays open to everyone.</p>
</div>
</div>
<div class="picker welcome-actions">
<button id="welcome-create" type="button" class="button primary">Create a free account</button>
<button id="welcome-browse" type="button" class="button">See what is live</button>
<code class="welcome-install">curl -fsSL https://nixamp.com/install.sh | sh</code>
<button id="welcome-hide" type="button" class="ghost" title="Hide this, on this device">Hide this</button>
</div>
</section>

<section class="panel player-only" data-title="Now Playing">
<video id="video" playsinline hidden></video>
<!-- A pasted YouTube, Vimeo or SoundCloud link, played by that site's
Expand Down
49 changes: 48 additions & 1 deletion web/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ export function start(): void {
accountProviders: need<HTMLDivElement>("account-providers"),
accountPanel: need<HTMLElement>("account-panel"),
accountElsewhere: need<HTMLParagraphElement>("account-elsewhere"),
welcome: need<HTMLElement>("welcome"),
welcomeCreate: need<HTMLButtonElement>("welcome-create"),
welcomeBrowse: need<HTMLButtonElement>("welcome-browse"),
welcomeHide: need<HTMLButtonElement>("welcome-hide"),
accountSignOut: need<HTMLButtonElement>("account-signout"),
accountNote: need<HTMLParagraphElement>("account-note"),
adminPanel: need<HTMLElement>("admin-panel"),
Expand Down Expand Up @@ -3415,6 +3419,26 @@ export function start(): void {
let creating = false;
/** The signed-in account, so the directory knows whose stream is whose. */
let meId = "";
/** Whether this nixamp keeps accounts at all: nixamp.com does, a laptop does not. */
let keepsAccounts = false;

// --- the welcome ------------------------------------------------------
//
// The pitch, for somebody who has never seen this before. Only where
// accounts live, only signed out, and only until they hide it: on your own
// server you already know what this is, and once you have an account so do
// you. Remembered per device, because the server has nobody to remember it
// for.
const WELCOME_HIDDEN = "nixamp.welcome";
const showWelcome = (): void => {
let hiddenByThem = false;
try {
hiddenByThem = localStorage.getItem(WELCOME_HIDDEN) === "hidden";
} catch {
// A private window may refuse storage; then it shows every time.
}
dom.welcome.hidden = !keepsAccounts || meId !== "" || hiddenByThem;
};

const showAccount = (email: string | null): void => {
const signedIn = email !== null;
Expand Down Expand Up @@ -3443,6 +3467,7 @@ export function start(): void {
dom.accountSubmit.textContent = creating ? "Create account" : "Sign in";
dom.accountToggle.textContent = creating ? "I have one" : "Create one";
dom.accountPassword.autocomplete = creating ? "new-password" : "current-password";
showWelcome();
};

/**
Expand All @@ -3455,7 +3480,7 @@ export function start(): void {
*/
const showProviders = async (): Promise<void> => {
let offered: { id: string; name: string }[] = [];
let keepsAccounts = false;
keepsAccounts = false;
try {
const answer = await fetch("/api/v1/auth/providers");
if (answer.ok) {
Expand All @@ -3474,6 +3499,7 @@ export function start(): void {
// nixamp.com, so that is where the panel points instead.
dom.accountPanel.hidden = !keepsAccounts;
dom.accountElsewhere.hidden = keepsAccounts;
showWelcome();
for (const provider of offered) {
const link = document.createElement("a");
link.className = "button";
Expand Down Expand Up @@ -3526,6 +3552,27 @@ export function start(): void {
showAccount(null);
});

// The welcome's buttons lead into the page rather than away from it: the
// form below, already switched to creating, and the directory.
dom.welcomeCreate.addEventListener("click", () => {
creating = true;
showAccount(null);
dom.accountPanel.scrollIntoView({ behavior: "smooth", block: "center" });
dom.accountEmail.focus({ preventScroll: true });
});
dom.welcomeBrowse.addEventListener("click", () => {
if (dom.directory.hidden) dom.browse.click();
else dom.directory.scrollIntoView({ behavior: "smooth", block: "nearest" });
});
dom.welcomeHide.addEventListener("click", () => {
try {
localStorage.setItem(WELCOME_HIDDEN, "hidden");
} catch {
// Then it comes back next visit, which is the most it can do.
}
dom.welcome.hidden = true;
});

dom.accountForm.addEventListener("submit", (event) => {
event.preventDefault();
const email = dom.accountEmail.value.trim();
Expand Down
43 changes: 43 additions & 0 deletions web/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,49 @@ body {
gap: 10px;
}

/* ---- the welcome ------------------------------------------------------- */

/* The pitch. Bigger type than anything else on the page, once, because it is
the only panel a stranger reads before deciding whether to stay. */
.welcome-title {
margin: 4px 0 8px;
font-size: 22px;
line-height: 1.25;
color: var(--green);
font-weight: 600;
}
.welcome-lead { margin: 0 0 14px; max-width: 70ch; color: var(--fg); }
.welcome-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
margin-bottom: 14px;
}
.welcome-grid h2 {
margin: 0 0 4px;
font-size: 13px;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--green-dim);
}
.welcome-grid p { margin: 0; color: var(--muted); font-size: 13px; }
.welcome-grid code, .welcome-install { color: var(--accent); }
.welcome-actions { gap: 10px; }
.welcome-install {
font-size: 12px;
padding: 6px 10px;
border: 1px dashed var(--edge);
border-radius: 4px;
overflow-wrap: anywhere;
}
/* A stranger's first button is the same shape as every other, but green. */
.welcome .button.primary { color: var(--green); border-color: var(--green-dim); }

@media (max-width: 720px) {
.welcome-grid { grid-template-columns: 1fr; }
.welcome-title { font-size: 19px; }
}

/* ---- header ------------------------------------------------------------ */

.bar {
Expand Down
18 changes: 18 additions & 0 deletions web/test/web.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,24 @@ test("the shell links the manifest, the icons and the theme colour", () => {
});


test("the shell says what nixamp is, and asks a stranger to sign up", () => {
const html = readFileSync(join(webDir, "index.html"), "utf8");
// The title is what a search result and a link preview show. It has to say
// what the thing is, and it must not dare anybody to send a takedown.
assert.match(html, /<title>nixamp: open source live streaming from your own machine<\/title>/);
assert.doesNotMatch(html, /DMCA/);
// The pitch is in the shell itself, hidden until app.ts decides, so a
// crawler reads it without running anything.
assert.match(html, /id="welcome"[^>]*hidden/);
assert.match(html, /id="welcome-create"/);
assert.match(html, /id="welcome-browse"/);
assert.match(html, /id="welcome-hide"/);
assert.match(html, /curl -fsSL https:\/\/nixamp\.com\/install\.sh \| sh/);
// And app.ts only ever shows it where accounts live.
const app = readFileSync(join(webDir, "src/app.ts"), "utf8");
assert.match(app, /dom\.welcome\.hidden = !keepsAccounts \|\| meId !== ""/);
});

test("the service worker can receive a push and act on a click", () => {
const source = serviceWorkerSource(["/assets/app-abc123.js"], "build-9");

Expand Down