From ea1b8606ebcb8a4db76246abf96c02d3123ef2b9 Mon Sep 17 00:00:00 2001 From: rowkav09 Date: Sun, 11 Oct 2026 11:00:32 +0100 Subject: [PATCH 01/10] feat: redesign card styles and settings --- README.md | 8 +- docs/README.md | 1 + docs/assets/cards/compact.svg | 2 +- docs/assets/cards/editorial.svg | 13 +++ docs/assets/cards/episode-playing.svg | 6 +- docs/assets/cards/idle.svg | 6 +- docs/assets/cards/movie-playing.svg | 6 +- docs/assets/cards/music-light.svg | 6 +- docs/assets/cards/music-playing.svg | 6 +- docs/assets/cards/paused.svg | 6 +- docs/assets/cards/privacy-redacted.svg | 6 +- docs/assets/cards/studio.svg | 13 +++ docs/assets/cards/terminal.svg | 13 +++ docs/card-examples.md | 56 +++++++----- docs/customization.md | 80 +++++++++-------- docs/settings.md | 39 ++++++++ docs/wiki/Add-a-README-card.md | 6 +- docs/wiki/Home.md | 3 +- scripts/render-card-examples.js | 1 + src/app-config.js | 4 +- src/app-settings.js | 22 ++--- src/card-presets.js | 11 +++ src/card.js | 18 ++-- src/http-handler.js | 3 +- src/settings-design.js | 75 ++++++++++++++++ src/settings-page-handler.js | 89 ++++++++++++++----- src/settings.js | 9 +- src/setup-config.js | 5 +- test/card-styles.test.js | 89 +++++++++++++++++++ test/card-theme.test.js | 2 +- test/card-visual.test.js | 4 + test/fixtures/cards/centred-reordered.svg | 6 +- test/fixtures/cards/compact.svg | 2 +- test/fixtures/cards/default-no-artwork.svg | 6 +- test/fixtures/cards/default.svg | 6 +- test/fixtures/cards/editorial-narrow.svg | 13 +++ test/fixtures/cards/editorial.svg | 13 +++ test/fixtures/cards/end-aligned-tight.svg | 6 +- test/fixtures/cards/film-compact.svg | 2 +- test/fixtures/cards/film.svg | 6 +- test/fixtures/cards/idle.svg | 6 +- test/fixtures/cards/narrow-max-artwork.svg | 6 +- test/fixtures/cards/paper-artwork-right.svg | 6 +- test/fixtures/cards/progress-full-width.svg | 6 +- test/fixtures/cards/progress-under-text.svg | 6 +- test/fixtures/cards/rtl-auto.svg | 6 +- test/fixtures/cards/studio-narrow.svg | 13 +++ test/fixtures/cards/studio.svg | 13 +++ test/fixtures/cards/terminal-narrow.svg | 13 +++ test/fixtures/cards/terminal.svg | 13 +++ .../cards/tv-episode-paper-narrow.svg | 6 +- .../cards/tv-episode-series-hidden.svg | 6 +- test/fixtures/cards/tv-episode.svg | 6 +- test/settings-page-handler.test.js | 4 +- test/settings.test.js | 2 +- 55 files changed, 596 insertions(+), 184 deletions(-) create mode 100644 docs/assets/cards/editorial.svg create mode 100644 docs/assets/cards/studio.svg create mode 100644 docs/assets/cards/terminal.svg create mode 100644 docs/settings.md create mode 100644 src/card-presets.js create mode 100644 src/settings-design.js create mode 100644 test/card-styles.test.js create mode 100644 test/fixtures/cards/editorial-narrow.svg create mode 100644 test/fixtures/cards/editorial.svg create mode 100644 test/fixtures/cards/studio-narrow.svg create mode 100644 test/fixtures/cards/studio.svg create mode 100644 test/fixtures/cards/terminal-narrow.svg create mode 100644 test/fixtures/cards/terminal.svg diff --git a/README.md b/README.md index 21b74c8e..6a1ff436 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,11 @@ Your media server knows what you are playing. nowplaying turns that into a card | :---: | :---: | :---: | | ![Example card showing Holocene by Bon Iver](docs/assets/cards/music-playing.svg) | ![Example card showing Spirited Away](docs/assets/cards/movie-playing.svg) | ![Example card showing Lost, S04E05](docs/assets/cards/episode-playing.svg) | -*Example cards, not live playback. [More states and styles](docs/card-examples.md).* +*Example cards, not live playback. [Browse all six styles](docs/card-examples.md).* + +| Studio | Editorial | Terminal | +| :---: | :---: | :---: | +| ![Studio card](docs/assets/cards/studio.svg) | ![Editorial card](docs/assets/cards/editorial.svg) | ![Terminal card](docs/assets/cards/terminal.svg) | > **Development builds:** The [`dev` prerelease](https://github.com/rowkavdev/nowplaying/releases/tag/dev) is a moving test build, not an accepted stable release. It can break. Downloads are not code-signed; verify the checksum before running one. Windows has an installer and portable ZIP. **Linux x86_64 is experimental:** RPM, DEB, Arch and portable tarball downloads are available for desktop testing. [Linux installation and requirements](docs/wiki/Install-on-Linux.md). macOS desktop support is still experimental. @@ -51,7 +55,7 @@ Provider sign-in: [Plex](docs/wiki/Connect-Plex.md) · [Jellyfin](docs/wiki/Conn ## Settings, updates and problems -Open **Settings** from the tray icon to manage servers, Discord, hosted cards, privacy and the card's appearance. The local status page shows connections and logs. [Customization](docs/customization.md) · [Updates and release channels](docs/wiki/Updates-and-release-channels.md) · [Troubleshooting](docs/wiki/Troubleshooting.md). +Open **Settings** from the tray icon to manage servers, Discord, hosted cards, privacy and the card's appearance. Settings keeps configuration beside live logs, with shortcuts for servers, appearance, Discord, privacy, sharing and startup. Choose a card style from the visual gallery, preview changes, and save each section separately. [Settings guide](docs/settings.md) · [Customization](docs/customization.md) · [Updates and release channels](docs/wiki/Updates-and-release-channels.md) · [Troubleshooting](docs/wiki/Troubleshooting.md). If something fails on a dev build, [open an issue](https://github.com/rowkavdev/nowplaying/issues/new) with the build number, operating system and desktop environment, package format, what you tried and what happened. Leave out server addresses and tokens. diff --git a/docs/README.md b/docs/README.md index 8f4d1efe..d7d99f37 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,6 +10,7 @@ Two audiences, two homes: | Guide | Covers | Status | | --- | --- | --- | | [Provider configuration](providers.md) | Plex, Jellyfin, Navidrome and Emby adapter options and normalized output | Current | +| [Settings](settings.md) | Finding controls, previews, section saves and logs | Current | | [Customization](customization.md) | Templates, privacy controls, card themes and Discord settings | Current | | [Card examples](card-examples.md) | Renderer-generated gallery of every card state and theme | Current | | [Hosted card deployment](hosted-card.md) | Running the card endpoint: routes, headers, caching, health checks | Current | diff --git a/docs/assets/cards/compact.svg b/docs/assets/cards/compact.svg index f5d0ea31..6758767b 100644 --- a/docs/assets/cards/compact.svg +++ b/docs/assets/cards/compact.svg @@ -7,7 +7,7 @@ - Holocene + Holocene \ No newline at end of file diff --git a/docs/assets/cards/editorial.svg b/docs/assets/cards/editorial.svg new file mode 100644 index 00000000..feb8c2e0 --- /dev/null +++ b/docs/assets/cards/editorial.svg @@ -0,0 +1,13 @@ + + + NOW PLAYING: Blue MondayNew Order + + + + + NOW PLAYING + 1:23 / 7:28 + Blue Monday + New Order + + \ No newline at end of file diff --git a/docs/assets/cards/episode-playing.svg b/docs/assets/cards/episode-playing.svg index 17869d22..6d4c1a1a 100644 --- a/docs/assets/cards/episode-playing.svg +++ b/docs/assets/cards/episode-playing.svg @@ -5,9 +5,9 @@ - Now playing + Now playing 16:20 / 43:00 - Lost - S04E05 · The Constant + Lost + S04E05 · The Constant \ No newline at end of file diff --git a/docs/assets/cards/idle.svg b/docs/assets/cards/idle.svg index db141784..f2daae25 100644 --- a/docs/assets/cards/idle.svg +++ b/docs/assets/cards/idle.svg @@ -5,9 +5,9 @@ - Not playing + Not playing - Nothing playing - Music + Nothing playing + Music \ No newline at end of file diff --git a/docs/assets/cards/movie-playing.svg b/docs/assets/cards/movie-playing.svg index fcee7fdd..e7c0ab60 100644 --- a/docs/assets/cards/movie-playing.svg +++ b/docs/assets/cards/movie-playing.svg @@ -5,9 +5,9 @@ - Now playing + Now playing 1:00:00 / 2:05:00 - Spirited Away (2001) - Movie + Spirited Away (2001) + Movie \ No newline at end of file diff --git a/docs/assets/cards/music-light.svg b/docs/assets/cards/music-light.svg index 34d3fc92..e7c960a7 100644 --- a/docs/assets/cards/music-light.svg +++ b/docs/assets/cards/music-light.svg @@ -5,9 +5,9 @@ - Now playing + Now playing 2:34 / 5:36 - Holocene - Bon Iver + Holocene + Bon Iver \ No newline at end of file diff --git a/docs/assets/cards/music-playing.svg b/docs/assets/cards/music-playing.svg index 4a2fabbb..e3708810 100644 --- a/docs/assets/cards/music-playing.svg +++ b/docs/assets/cards/music-playing.svg @@ -5,9 +5,9 @@ - Now playing + Now playing 2:34 / 5:36 - Holocene - Bon Iver + Holocene + Bon Iver \ No newline at end of file diff --git a/docs/assets/cards/paused.svg b/docs/assets/cards/paused.svg index cd4eceb3..871d60e5 100644 --- a/docs/assets/cards/paused.svg +++ b/docs/assets/cards/paused.svg @@ -5,9 +5,9 @@ - Paused + Paused 2:34 / 5:36 - Holocene - Bon Iver + Holocene + Bon Iver \ No newline at end of file diff --git a/docs/assets/cards/privacy-redacted.svg b/docs/assets/cards/privacy-redacted.svg index 7f17aa44..13c9dbe9 100644 --- a/docs/assets/cards/privacy-redacted.svg +++ b/docs/assets/cards/privacy-redacted.svg @@ -5,9 +5,9 @@ - Now playing + Now playing 2:34 / 5:36 - Private media - Music + Private media + Music \ No newline at end of file diff --git a/docs/assets/cards/studio.svg b/docs/assets/cards/studio.svg new file mode 100644 index 00000000..e8cdd1ac --- /dev/null +++ b/docs/assets/cards/studio.svg @@ -0,0 +1,13 @@ + + + Now playing: Blue MondayNew Order + + + + + Now playing + 1:23 / 7:28 + Blue Monday + New Order + + \ No newline at end of file diff --git a/docs/assets/cards/terminal.svg b/docs/assets/cards/terminal.svg new file mode 100644 index 00000000..a26244f9 --- /dev/null +++ b/docs/assets/cards/terminal.svg @@ -0,0 +1,13 @@ + + + NOW PLAYING: Blue MondayNew Order + + + + + NOW PLAYING + 1:23 / 7:28 + Blue Monday + New Order + + \ No newline at end of file diff --git a/docs/card-examples.md b/docs/card-examples.md index 51e49484..34c7d47b 100644 --- a/docs/card-examples.md +++ b/docs/card-examples.md @@ -1,41 +1,55 @@ -# Card examples +# Card styles -Every image below is generated from the shipped renderer by `node scripts/render-card-examples.js`, so what you see is exactly what the current build draws. A test fails if the renderer changes and these are not regenerated. Live cards also embed sanitized artwork from your media server on the left. +Choose **Settings → Appearance → Card Settings**. These examples come directly from the shipped SVG renderer, without cover art, just like hosted README cards. Local cards can also show your server's sanitized cover or poster. -## Music +## Classic -![Card showing Now playing with the track Holocene by Bon Iver and a progress bar, on a dark background](assets/cards/music-playing.svg) +A familiar dark card with blue progress. -## Music, paper theme +![Classic card showing Holocene by Bon Iver](assets/cards/music-playing.svg) -![The same music card in the light paper theme](assets/cards/music-light.svg) +## Paper -## TV episode +White background, clear dark text. -![Card showing the series Lost as the title with the episode code S04E05 and episode title The Constant below](assets/cards/episode-playing.svg) +![Paper card showing Holocene by Bon Iver](assets/cards/music-light.svg) -## Movie +## Studio -![Card showing the film Spirited Away (2001) with a progress bar](assets/cards/movie-playing.svg) +Smaller type and cover, rounded corners and a warm accent. -## Paused +![Studio card showing Blue Monday by New Order](assets/cards/studio.svg) -![Card in the paused state with a partially filled progress bar](assets/cards/paused.svg) +## Editorial -## Nothing playing +Serif title, warm paper and square edges. Local artwork sits on the right. -![Card in the idle state showing Nothing playing](assets/cards/idle.svg) +![Editorial card showing Blue Monday by New Order](assets/cards/editorial.svg) -## Compact theme +## Terminal -![The music card in the compact theme, text only with no progress bar](assets/cards/compact.svg) +Monospace type with a muted green palette. -## Privacy: titles hidden +![Terminal card showing Blue Monday by New Order](assets/cards/terminal.svg) -![Card showing Private media instead of a title, with progress only](assets/cards/privacy-redacted.svg) +## Compact -The exact set shown depends on your privacy settings - with a media type hidden, anyone looking at the card sees the idle state instead. +Just the title, without artwork or progress by default. -## Discord Rich Presence +![Compact card showing Holocene](assets/cards/compact.svg) -The same presence feeds Discord: music shows as **Listening**, films and episodes as **Watching**, and TV episodes use the series name with the episode code (for example "Lost" and "S04E05 · The Constant"). Word for word Discord examples live in the [wiki](https://github.com/rowkavdev/nowplaying/wiki/Enable-Discord-Rich-Presence). +## Other playback states + +| State | Example | +| --- | --- | +| TV episode | ![Lost, S04E05, The Constant](assets/cards/episode-playing.svg) | +| Film | ![Spirited Away, 2001](assets/cards/movie-playing.svg) | +| Paused | ![Paused music card](assets/cards/paused.svg) | +| Nothing playing | ![Idle card](assets/cards/idle.svg) | +| Titles hidden | ![Private media with progress](assets/cards/privacy-redacted.svg) | + +Hiding a whole media type shows the idle state. Hiding progress also removes the timer. Privacy applies before any appearance choices. + +See [customization](customization.md) for layout controls and theme names. After changing a saved style, copy the new hosted README snippet to update an existing embed. + +For contributors: `node scripts/render-card-examples.js` regenerates these images. The example test checks that they match the renderer; visual fixtures also cover narrow cards, artwork, RTL and reordered fields. diff --git a/docs/customization.md b/docs/customization.md index 834ead27..ab77149b 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -1,62 +1,64 @@ -# Customization reference +# Customize NowPlaying -Customization is provider-neutral and applied after polling. The same normalized presence can feed cards and Discord without changing provider behavior. +Open **Settings** from the tray icon. Each section saves separately; changing the preview does not save a setting. -## Shipped settings +## Card appearance -`createSettings()` validates unknown keys and deep-merges immutable defaults for: +Choose **Appearance** in the settings navigation to open **Card Settings**. Pick a style, adjust the width, and press **Save**. The local card changes immediately. If you already embedded a hosted card, copy the updated README snippet from **Sharing** and replace the old one: appearance is part of that image's URL. -- locale; -- privacy mode and field removal; -- shared title/detail/state templates and separators; -- card theme, width and field visibility; -- Discord enablement, templates, timestamps and idle behavior. +| Style | Look | Theme value | +| --- | --- | --- | +| Classic | Dark background, sans-serif type, blue progress | `midnight-blue` | +| Paper | White background and dark type | `paper` | +| Studio | Smaller rounded cover, warm accent, slim progress | `studio` | +| Editorial | Warm paper, serif title, square border, cover on the right | `editorial` | +| Terminal | Monospace type, muted green, square edges | `terminal` | +| Compact | A single title without artwork or progress | `compact` | -Templates support documented named presence fields. Missing values remove their associated empty segments rather than leaving doubled separators. Output escaping remains destination-specific. +[See the card gallery](card-examples.md). Width can be 280–800 pixels. Hosted cards use these styles without artwork. The preview uses your current playback when available; when nothing is playing, it uses sample media and a placeholder cover. -## Cards +**Fine-tune typography, artwork and layout** opens the detailed controls: -`renderCard(presence, options)` supports: +- padding and corner radius; +- named font or a custom stack of up to five plain font names; +- status text, border, background and progress bar appearance; +- artwork size, shape and side; +- line order, alignment and text direction. -- `theme`: `midnight-blue`, `paper` or `compact`; -- `width`: integer from 280 to 800; -- `colors`: validated six-digit overrides for the selected palette; -- `show`: booleans for artwork, media type, progress, state and subtitle; -- `artworkDataUri`: validated, sanitized PNG/JPEG/WebP data. +Classic, Paper and Editorial choose artwork size by media type; Studio and Terminal use a smaller cover. The size sliders override those defaults. At narrow widths the renderer can reduce the displayed cover to leave room for text. The preview explains when this happens; your saved preference is kept. -`card.artworkTint` (config.json only, default `true`): the card background takes a tint from the cover's dominant colour, kept in a safe lightness range so text stays readable on white or black covers. Set it to `false` in `config.json` to turn it off; settings saves keep your choice. +Selecting a style resets these appearance controls to that style's defaults. It keeps the current width and reading direction. **Back to defaults** restores Classic at 440 pixels. Both actions are drafts until you press **Save**. -The hosted endpoint exposes a deliberately smaller public allowlist: `theme`, `width`, comma-separated `show` (with `notime` to hide the clock) and the layout and look options listed in the hosted card guide. Secrets, arbitrary colors and templates never enter public query strings. +Fonts must be installed on the device viewing the SVG. A custom name does not download or embed a font. The local card can take a subtle tint from its cover; set `card.artworkTint` to `false` in `config.json` to disable that. Settings saves preserve your tint choice. -Built-in palettes use color combinations selected to remain distinguishable for deuteranopia. Accessible title/description text is always emitted. +## Renderer and card links -## Discord - -Discord settings (Settings > Discord, saved to `config.json`): - -- elapsed, remaining, both or no timestamps (`discord.timestamps`); -- idle behaviour (`discord.idleBehavior`); -- the status text next to your name (`discord.name`) and which line Discord shows there (`discord.statusDisplayType`: `name`, `state` or `details`); -- album art lookup (`discord.artworkLookup`, off by default). +`renderCard(presence, options)` accepts `theme`, `width`, validated six-digit `colors`, field visibility in `show`, `layout`, sanitized raster artwork in `artworkDataUri`, and a six-digit `tint`. Explicit layout and visibility options override the selected style. -Wording templates for the other lines, buttons, asset keys and the update interval can't be changed yet. Values for them in `config.json` are ignored, and a settings save removes them. +```js +import { renderCard } from "nowplaying/card"; -TV episodes with a known series show the series and episode code, for example "Lost" and "S04E05 · The Constant". +const svg = renderCard(presence, { + theme: "editorial", + width: 480, + layout: { radius: 4, textAlign: "start" }, +}); +``` -Discord Rich Presence runs locally and communicates with the user's Discord IPC socket. Analytics, when explicitly enabled, sends only one anonymous installation ping. +`show` accepts booleans for `artwork`, `mediaType`, `progress`, `state`, `subtitle` and `time`. A privacy filter must run before rendering; showing a field cannot recover data removed by that filter. -## Status text +The local `/card.svg` URL accepts `theme`, `width` and comma-separated `show`. Hosted links also accept the public layout options documented in [hosted card deployment](hosted-card.md). Add `notime` to `show` to hide the clock. Credentials and custom text templates do not belong in a card URL. -For music, Discord shows ` - ` by default, for example "Holocene - Bon Iver" from Plex. Films and episodes keep Discord's own app name unless you set a status text. Change it in Settings > Discord > Status text, or set `discord.name` in `config.json`. Leave it empty for the default. Saved custom templates keep their wording, including `{artist} on {service}` if you prefer it. +## Discord -Write fields in braces: `{artist}`, `{title}`, `{subtitle}`, `{album}`, `{year}`, `{series}`, `{season}`, `{episode}`, `{episodeCode}`, `{service}` (Spotify, Navidrome, Plex and so on), `{provider}`, `{mediaType}`, `{state}`, `{stateLabel}`, `{position}`, `{duration}` and `{progressPercent}`. Other text is kept as typed. Write `{{` or `}}` for a literal brace. A field that is not in this list is rejected when you save. The text can be 128 characters at most. With hidden titles on, `{artist}` is blank. +In **Discord Settings**, choose whether to publish presence, which timer to use, what happens when playback stops, and the status text shown beside your name. Spotify appears on cards only; it never feeds Discord. -## Privacy +For music the default status text is `{title} - {artist}`. Films and episodes use the app name unless you provide a custom status text. Leave the field empty to use those defaults. -Privacy is applied before formatting. Removed data cannot be restored by card or Discord visibility settings. +Available fields: `{artist}`, `{title}`, `{subtitle}`, `{album}`, `{year}`, `{series}`, `{season}`, `{episode}`, `{episodeCode}`, `{service}`, `{provider}`, `{mediaType}`, `{state}`, `{stateLabel}`, `{position}`, `{duration}` and `{progressPercent}`. Write `{{` or `}}` for a literal brace. Unknown fields are rejected on save; the maximum is 128 characters. -Available controls include private/friends/public/custom modes, title redaction/replacement, artwork removal, progress removal and media-kind suppression. Public deployments should start with conservative policy and widen fields intentionally. +**Show my server's cover** uploads a cover to a temporary public host so Discord can display it. The fallback MusicBrainz lookup sends a track title and artist. Read [artwork behavior](artwork.md#discord-cover-upload) before enabling either on an existing install. Other Discord line templates, buttons and the update interval are not editable in Settings. -## Further layout work +## Privacy -The next customization slices will remain backward-compatible and small. Candidate controls include padding/corner radius, typography scale, artwork placement, progress dimensions/position, field order and RTL-aware alignment. These are not yet accepted renderer options; unsupported keys continue to fail validation rather than silently doing nothing. +**Privacy Settings** can hide titles, artwork, progress or whole media types across the local card, hosted card and Discord. A hidden media type appears as nothing playing. Privacy is applied before formatting or artwork lookup. [Privacy and safe configuration](wiki/Privacy-and-safe-configuration.md) explains these controls and where credentials are stored. diff --git a/docs/settings.md b/docs/settings.md new file mode 100644 index 00000000..4c7fca52 --- /dev/null +++ b/docs/settings.md @@ -0,0 +1,39 @@ +# Settings + +Open **Settings** from the tray icon, or visit `http://127.0.0.1:47832/settings` on the PC running NowPlaying. If you changed the local port, use that port instead. The page is local to your computer. + +Configuration is on the left and recent app logs are on the right, following DRPP's desktop layout. On a small screen the logs sit below configuration. Use the shortcuts at the top to open and jump to a section. Accordion headings work with the keyboard too. + +## Find a setting + +| Section | Use it for | +| --- | --- | +| Media servers | Find, connect and remove Plex, Jellyfin, Emby or Navidrome servers | +| Connected services | Connect Spotify or sign in with GitHub for a shared hosted card | +| Card Settings / Appearance | Pick a card style, preview it, change width and fine-tune layout | +| Discord Settings | Enable presence, choose timers and status text, manage cover art | +| Privacy Settings | Hide titles, artwork, progress or media types across outputs | +| YouTube Settings | Copy or replace a browser-extension pairing code | +| Startup Settings | Start the app when you sign in | +| Hosted Card Settings / Sharing | Enable uploads and copy a public card link or README snippet | +| Hosted Card Devices | Manage PCs connected to your GitHub-linked card | + +Sections that need an unavailable feature are hidden. For example, device management appears when the hosted account supplies that capability. First-run setup focuses on connecting a server; the full page appears once configuration is ready. + +## Preview and save + +Each section has its own **Save** button and feedback. A card preview updates before saving. Selecting a style resets that draft's look controls while keeping its width and reading direction. Detailed layout controls are under **Fine-tune typography, artwork and layout**. [Card styles and controls](customization.md). + +A successful save applies to the running app. After changing the appearance of a hosted card, copy the new Markdown snippet into your README. Existing snippets keep their original URL options. GitHub may cache the card image; open the card link directly to compare. + +A failed save leaves the stored configuration unchanged. Reloading abandons unsaved drafts. Sign-in, server removal, pairing-code changes and connection buttons perform their named action directly; they do not wait for a section's Save button. + +## Logs + +The log panel polls recent app events. **Search logs** accepts regular expressions; an invalid expression shows an error so you can correct it. **Auto Scroll** follows new entries; turn it off to inspect older messages. **Wrap Text** keeps long lines within the panel. These display preferences stay in this browser and do not change your app configuration. + +Logs and Settings can include local server details. Check them before sharing a screenshot. [Troubleshooting](wiki/Troubleshooting.md) covers connection failures and useful diagnostic details. + +## About and updates + +**Info** opens the README, license and open-source attribution shipped with the app. The header shows the installed version when available. Use [updates and release channels](wiki/Updates-and-release-channels.md) for update behavior; Settings does not provide a manual update button. diff --git a/docs/wiki/Add-a-README-card.md b/docs/wiki/Add-a-README-card.md index 652e42cd..66b8ed30 100644 --- a/docs/wiki/Add-a-README-card.md +++ b/docs/wiki/Add-a-README-card.md @@ -30,9 +30,9 @@ By default each PC gets its own card link with a random ID. If you have more tha ## What the card shows -- Music shows as "Listening", films and TV episodes as "Watching". -- Paused, idle and offline each have their own look. -- Themes, width and which fields to show are options on the card link; secrets never go in the URL. +- Playing cards say "Now playing"; paused cards say "Paused". Music shows the track and artist, and TV episodes show the series and episode code. +- Paused media retains its title. Idle or expired updates show nothing playing. +- Choose one of six styles in **Settings > Appearance**, adjust the width, and press **Save**. Copy the updated README snippet from **Sharing** after an appearance change; the style is part of its URL. [Style gallery](https://github.com/rowkavdev/nowplaying/blob/main/docs/card-examples.md). ## Privacy diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md index 16f15002..8e650e50 100644 --- a/docs/wiki/Home.md +++ b/docs/wiki/Home.md @@ -5,7 +5,7 @@ nowplaying shows what is playing on your own media server (Plex, Jellyfin, Navid - a **card** image you can embed in a GitHub README or any web page - **Discord Rich Presence**, the "Listening to / Watching" status on your Discord profile -Everything runs on your own machine or server. Your server address, sign-in details and media titles never appear in the card or in Discord unless you choose to show them. +Everything runs on your own machine or server. Your server address and sign-in details stay private. Titles are shown by default; Privacy Settings can hide them before they reach the card or Discord. ## Choose your path @@ -18,6 +18,7 @@ Everything runs on your own machine or server. Your server address, sign-in deta ## After setup +- [Settings guide](https://github.com/rowkavdev/nowplaying/blob/main/docs/settings.md): appearance, section saves and live logs - [Privacy and safe configuration](Privacy-and-safe-configuration): hide titles, artwork or progress, and keep credentials safe - [Updates and release channels](Updates-and-release-channels): stable releases versus development builds - [Troubleshooting](Troubleshooting): fixes organised by what you see going wrong diff --git a/scripts/render-card-examples.js b/scripts/render-card-examples.js index c49e7eac..fef1def1 100644 --- a/scripts/render-card-examples.js +++ b/scripts/render-card-examples.js @@ -7,6 +7,7 @@ import { join } from "node:path"; import { renderCard } from "../src/card.js"; export const EXAMPLES = [ + ...["studio", "editorial", "terminal"].map((theme) => ({ file: `${theme}.svg`, title: `${theme} style`, options: { theme }, presence: { kind: "track", state: "playing", title: "Blue Monday", subtitle: "New Order", positionMs: 83_000, durationMs: 448_000 } })), { file: "music-playing.svg", title: "Music, playing (default midnight-blue theme)", diff --git a/src/app-config.js b/src/app-config.js index 976213d3..57254e61 100644 --- a/src/app-config.js +++ b/src/app-config.js @@ -1,7 +1,7 @@ import { randomBytes } from "node:crypto"; import { readFile } from "node:fs/promises"; import { cardSettingsView, createAppSettingsStore, discordSettingsView, hostedSettingsView, privacySettingsView } from "./app-settings.js"; -import { renderCard } from "./card.js"; +import { cardPresets, renderCard } from "./card.js"; import { createCardHandler } from "./http-handler.js"; import { createCardPipeline } from "./card-pipeline.js"; import { createHttpServer } from "./http-server.js"; @@ -405,7 +405,7 @@ export async function startAppFromConfig({ configFile, credentialStore, host = " // A plain grey square stands in for artwork so placement and size // show in the preview; real art is fetched for /card.svg (#449). const svg = renderCard(presence, { ...cardRenderOptions(card), artworkDataUri: PREVIEW_ARTWORK }); - const automaticWidth = presence.kind === "track" ? 100 : 68; + const automaticWidth = cardPresets[card.theme ?? "midnight-blue"].layout.artworkWidth ?? (presence.kind === "track" ? 100 : 68); return svg.replace(" Object.freeze({ label, description, layout: Object.freeze(layout), show: Object.freeze(show) }); +export const cardPresets = Object.freeze({ + "midnight-blue": preset("Classic", "Dark background, blue accent."), + paper: preset("Paper", "A crisp white card."), + compact: preset("Compact", "A single line, without a cover.", {}, { artwork: false, progress: false, state: false, subtitle: false, time: false }), + studio: preset("Studio", "Smaller cover, warm accent.", { padding: 20, radius: 12, titleSize: 18, subtitleSize: 13, artworkWidth: 80, artworkHeight: 80, artShape: "rounded", statusStyle: "dot", progressStyle: "rounded", progressHeight: 2 }), + editorial: preset("Editorial", "Warm paper and serif type.", { padding: 24, radius: 0, fontFamily: "serif", titleSize: 24, subtitleSize: 14, statusStyle: "caps", progressHeight: 2, artworkPosition: "right" }), + terminal: preset("Terminal", "Monospace. Square edges.", { padding: 20, radius: 0, fontFamily: "mono", titleSize: 18, subtitleSize: 12, statusStyle: "caps", progressHeight: 2, artworkWidth: 80, artworkHeight: 80 }), +}); diff --git a/src/card.js b/src/card.js index 4767199b..b53e440d 100644 --- a/src/card.js +++ b/src/card.js @@ -1,5 +1,11 @@ +import { cardPresets } from "./card-presets.js"; +export { cardPresets } from "./card-presets.js"; + export const cardThemes = Object.freeze({ "midnight-blue": Object.freeze({ background: "#0d1117", border: "#30363d", primary: "#f0f6fc", secondary: "#9aa7b7", accent: "#58a6ff", track: "#30363d" }), + studio: Object.freeze({ background: "#181a1d", border: "#3a3d42", primary: "#f3f3f1", secondary: "#b1b3b7", accent: "#d6c8a9", track: "#393b40" }), + editorial: Object.freeze({ background: "#f8f5ee", border: "#c8c1b4", primary: "#25241f", secondary: "#625d52", accent: "#77583c", track: "#ddd6c8" }), + terminal: Object.freeze({ background: "#141817", border: "#4c5952", primary: "#e0e8df", secondary: "#a8b8aa", accent: "#b8d3ae", track: "#36443b" }), paper: Object.freeze({ background: "#ffffff", border: "#c7d2df", primary: "#172033", secondary: "#526173", accent: "#0969da", track: "#d8e0e8" }), compact: Object.freeze({ background: "#0d1117", border: "#30363d", primary: "#f0f6fc", secondary: "#9aa7b7", accent: "#58a6ff", track: "#30363d" }), }); @@ -10,7 +16,7 @@ const DATA_IMAGE_PATTERN = /^data:image\/(?:png|jpeg|webp);base64,[A-Za-z0-9+/]+ const DEFAULT_FIELD_ORDER = Object.freeze(["state", "title", "subtitle"]); const TEXT_ANCHORS = Object.freeze({ start: "start", middle: "middle", end: "end" }); const SHOW_DEFAULTS = Object.freeze({ artwork: true, mediaType: true, progress: true, state: true, subtitle: true, time: true }); -const COMPACT_SHOW = Object.freeze({ artwork: false, progress: false, state: false, subtitle: false, time: false }); + export function resolveCardTheme(theme = "midnight-blue", colors = {}) { @@ -30,6 +36,8 @@ export function renderCard(presence, { width = 440, show = {}, theme = "midnight if (!Number.isInteger(width) || width < 280 || width > 800) throw new RangeError("width must be an integer from 280 to 800"); if (show === null || typeof show !== "object" || Array.isArray(show)) throw new TypeError("show must be an object"); if (layout === null || typeof layout !== "object" || Array.isArray(layout)) throw new TypeError("layout must be an object"); + if (!Object.hasOwn(cardPresets, theme)) throw new TypeError(`Unknown card theme: ${theme}`); + layout = { ...cardPresets[theme].layout, ...layout }; const allowedLayout = new Set(["padding", "radius", "titleSize", "subtitleSize", "progressHeight", "artworkPosition", "artworkWidth", "artworkHeight", "fieldOrder", "textAlign", "progressPosition", "progressWidth", "direction", "fontFamily", "statusStyle", "artShape", "progressStyle", "border", "background", "fontStack"]); for (const key of Object.keys(layout)) if (!allowedLayout.has(key)) throw new TypeError(`Unknown card layout setting: ${key}`); const padding = bounded(layout.padding, 24, 12, 48, "layout.padding"); @@ -50,7 +58,7 @@ export function renderCard(presence, { width = 440, show = {}, theme = "midnight const borderStyle = oneOf(layout.border, "thin", ["none", "thin"], "layout.border"); const directionSetting = layout.direction ?? "ltr"; if (!new Set(["ltr", "rtl", "auto"]).has(directionSetting)) throw new TypeError("layout.direction: expected ltr, rtl or auto"); - const presetShow = theme === "compact" ? COMPACT_SHOW : {}; + const presetShow = cardPresets[theme].show; const visibility = { ...SHOW_DEFAULTS, ...presetShow, ...show }; for (const [key, value] of Object.entries(visibility)) { if (!Object.hasOwn(SHOW_DEFAULTS, key)) throw new TypeError(`Unknown card visibility setting: ${key}`); @@ -162,10 +170,10 @@ export function renderCard(presence, { width = 440, show = {}, theme = "midnight ${hasArtwork ? `` : ""} ${dot ? `` : ""} - ${visibility.state ? `${status}` : ""} + ${visibility.state ? `${status}` : ""} ${showTime ? `${timeLabel}` : ""} - ${escapeXml(truncate(title, Math.max(4, Math.floor(contentWidth / (titleSize * 0.64)))))} - ${hasSubtitle ? `${escapeXml(truncate(subtitle, Math.max(4, Math.floor(contentWidth / (subtitleSize * 0.55)))))}` : ""} + ${escapeXml(truncate(title, Math.max(4, Math.floor(contentWidth / (titleSize * 0.64)))))} + ${hasSubtitle ? `${escapeXml(truncate(subtitle, Math.max(4, Math.floor(contentWidth / (subtitleSize * 0.55)))))}` : ""} ${visibility.progress ? `` : ""} `; } diff --git a/src/http-handler.js b/src/http-handler.js index 158e794b..96eb206d 100644 --- a/src/http-handler.js +++ b/src/http-handler.js @@ -1,6 +1,7 @@ +import { cardPresets } from "./card-presets.js"; import { createHash } from "node:crypto"; -const THEMES = new Set(["midnight-blue", "paper", "compact"]); +const THEMES = new Set(Object.keys(cardPresets)); const VISIBILITY = new Set(["artwork", "mediaType", "progress", "state", "subtitle"]); export function parseCardQuery(searchParams) { diff --git a/src/settings-design.js b/src/settings-design.js new file mode 100644 index 00000000..8b8d02c3 --- /dev/null +++ b/src/settings-design.js @@ -0,0 +1,75 @@ +// Presentation layer for the existing Settings forms. Native details, labels +// and submit buttons keep keyboard behavior and section saves predictable. +export const SETTINGS_DESIGN_CSS = ` +html.drpp-root{height:100%;overflow:clip} +body.drpp-shell{height:100dvh;display:flex;flex-direction:column;overflow:clip;background:#191a1c;color:#e4e5e7;font:14px/1.55 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif} +.drpp-shell .drpp-header{flex:none;min-height:72px;padding:16px 28px;background:#202124;gap:16px} +.drpp-shell h1{font-size:19px;font-weight:650;letter-spacing:-.5px} +.drpp-heading{gap:14px}.drpp-heading #drpp-version{font-size:12px;color:#adb0b8} +.drpp-actions{gap:8px}.drpp-actions a,.drpp-shell .drpp-actions button{border:0;background:transparent;color:#c3c6cc;padding:7px 10px;font-size:13px} +.drpp-actions a:hover,.drpp-shell .drpp-actions button:hover{background:#303237;color:#fff} +.drpp-columns{height:auto;flex:1;min-height:0} +.drpp-config{width:64%;background:#202124}.drpp-logs{width:36%;background:#191a1c} +.drpp-shell .drpp-panel-heading{padding:16px 24px;gap:12px;min-height:67px;border-color:#383a3f} +.drpp-shell .drpp-panel-heading h2{font-size:15px;font-weight:600} +.drpp-config>.drpp-panel-heading>span:not(.drpp-divider){color:#adb0b8;font-size:12px} +.drpp-shell .drpp-panel-heading label{font-size:12px;color:#c3c6cc} +.drpp-divider{background:#3d3f44;height:20px} +.settings-nav{display:flex;gap:6px;padding:12px 24px;border-bottom:1px solid #383a3f;flex:none;overflow-x:auto} +.drpp-shell .settings-nav a{color:#b4b7bf;font-size:13px;padding:5px 9px;white-space:nowrap;text-decoration:none;border-radius:4px} +.drpp-shell .settings-nav a:hover,.drpp-shell .settings-nav a[aria-current=true]{background:#33363c;color:#f3f4f6} +.drpp-config-scroll{padding:24px;scroll-behavior:smooth;scroll-padding-top:16px} +.drpp-config .drpp-accordion{background:#24262a;border:1px solid #42454b;border-radius:6px;margin:0 0 12px;overflow:clip} +.drpp-config .drpp-accordion>summary{padding:17px 20px;font-size:14px;font-weight:600;gap:16px} +.drpp-config .drpp-accordion>summary:hover{background:#2b2d32} +.drpp-config .drpp-accordion[open]>summary{border-color:#42454b} +.section-copy{display:flex;flex-direction:column;gap:2px}.section-copy small{font-size:12px;font-weight:400;color:#adb0b8} +.drpp-config .drpp-accordion section{padding:20px} +.drpp-shell .row{gap:10px 16px;margin:0 0 18px;align-items:center} +.drpp-shell .row label[for]{min-width:155px;max-width:100%;color:#d4d6dc;font-size:13px} +.drpp-config .hint,.drpp-config legend,.drpp-config dt,.drpp-config output,.drpp-config .unit{color:#b4b7bf;font-size:12px;line-height:1.7} +.drpp-config .hint{margin-bottom:18px;max-width:70ch} +.drpp-shell input,.drpp-shell select,.drpp-shell button{max-width:100%} +.drpp-shell button,.drpp-shell select,.drpp-shell input[type=number],.drpp-shell input[type=search],.drpp-shell input[type=text],.drpp-shell input[type=url],.drpp-shell input[type=password],.drpp-shell input:not([type]){border-radius:4px;background:#202124;color:#e4e5e7;padding:8px 10px;font-size:13px} +.drpp-shell button{border-color:#696d77;font-weight:500;min-height:34px} +.drpp-shell button:not(:disabled):hover{background:#35383f} +.drpp-shell button[type=submit]{background:#1971c2;border-color:#398bd7;color:#fff;padding:8px 18px} +.drpp-shell button[type=submit]:not(:disabled):hover{background:#1864ab} +.drpp-shell input:not([type=checkbox]):not([type=range]),.drpp-shell select{border-color:#858990} +.drpp-shell input[type=checkbox]{accent-color:#78aef1;width:16px;height:16px;vertical-align:-3px;margin:0 7px 0 0;flex:none} +.drpp-shell input[type=range]{width:min(180px,100%);accent-color:#8ab4f8} +.drpp-shell select{min-width:150px}.drpp-shell input[type=url],.drpp-shell input[type=text],.drpp-shell input[type=password],.drpp-shell input:not([type]){width:300px} +.drpp-shell input[type=search]{width:100%}.drpp-shell input::placeholder{color:#a8a9ae} +.drpp-shell :focus-visible{outline:2px solid #8ab4f8;outline-offset:3px} +.drpp-config a{color:#8fc4ff}.drpp-shell .ok{color:#8fc4ff}.drpp-shell .bad{color:#ffbb82}.drpp-shell .warn{color:#e0d070} +.drpp-config .drpp-setup{width:100%;background:#2d2922;border-color:#8e795a;border-radius:6px;margin:0 0 20px} +.drpp-setup a{width:fit-content;min-width:120px} +.drpp-shell #scan-help{font-size:12px;color:#b4b7bf;line-height:1.7;margin:0 0 16px} +.drpp-shell #discovered-list li,.drpp-shell #servers-list li{padding:12px 0;gap:12px;border-color:#474a51} +.drpp-shell #manual-add{margin-bottom:0}.drpp-shell #manual-add>summary{font-size:13px;color:#8fc4ff} +.drpp-shell #manual-add[open]>summary{margin-bottom:16px} +.drpp-shell #services-section h3{font-size:14px;font-weight:600}.drpp-shell #services-section>p{font-size:13px;color:#b4b7bf} +.drpp-shell #services-section>div{padding-top:12px;border-color:#474a51} +.drpp-shell #card-form .preview{margin:0 0 22px;padding:24px 16px;display:flex;flex-direction:column;align-items:center;background:#191a1c;border:1px solid #474a51;border-radius:4px;min-height:180px} +.drpp-shell .preview-label{align-self:flex-start;text-transform:uppercase;letter-spacing:.09em;font-size:10px;color:#adb0b8;margin:0 0 20px} +.drpp-shell .preview img{max-width:100%;height:auto}.drpp-shell .preview .hint{margin:12px 0 0} +.card-style-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin:0 0 20px} +.drpp-shell .card-style{padding:10px;text-align:left;display:flex;flex-direction:column;gap:8px;background:#202124;border:1px solid #696d77;min-width:0;border-radius:4px} +.drpp-shell .card-style[aria-pressed=true]{border:2px solid #8ab4f8;padding:9px;background:#283442} +.card-style img{display:block;width:100%;height:auto;aspect-ratio:280/140;object-fit:contain} +.card-style strong{font-size:12px;font-weight:600}.card-style small{color:#b4b7bf;font-size:11px;line-height:1.5;font-weight:400} +.drpp-shell .card-advanced{margin:20px 0;border-top:1px solid #474a51;padding-top:16px} +.drpp-shell .card-advanced>summary{color:#d4d6dc;font-size:13px;font-weight:500;margin-bottom:16px} +.drpp-shell .card-advanced[open]>summary{margin-bottom:22px} +.drpp-logs .drpp-panel-heading{padding:16px;align-content:flex-start;gap:10px} +.drpp-logs .drpp-panel-heading input{flex-basis:100%;order:4}.drpp-logs .drpp-panel-heading small{margin-left:auto} +.drpp-logs .drpp-divider{display:none}.drpp-logs .drpp-panel-heading label{font-size:11px} +.drpp-log-lines{font:12px/1.8 ui-monospace,SFMono-Regular,Consolas,monospace;padding:12px 0} +.drpp-log-line{border-left-width:2px;padding:2px 14px;background:transparent}.drpp-log-line .source{color:#b5bcc8} +#drpp-info{background:#24262a;border-color:#696d77;border-radius:6px} +@media(min-width:1500px){.drpp-config-scroll{padding:28px 40px}.drpp-config{width:62%}.drpp-logs{width:38%}} +@media(prefers-reduced-motion:reduce){.drpp-config-scroll{scroll-behavior:auto}} +@media(max-width:1100px){.drpp-config{width:66%}.drpp-logs{width:34%}.card-style-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.drpp-shell .drpp-panel-heading{padding:16px}.settings-nav{padding:12px 16px}.drpp-config-scroll{padding:16px}} +@media(max-width:900px){html.drpp-root{height:auto;overflow:auto}body.drpp-shell{height:auto;display:block;overflow:visible}.drpp-columns{height:auto;display:block}.drpp-config,.drpp-logs{width:100%}.drpp-config-scroll{max-height:none;overflow:visible}.drpp-shell .drpp-header{padding:16px}.drpp-logs{border-top:1px solid #474a51}.drpp-log-lines{min-height:220px;max-height:360px}.settings-nav{flex-wrap:wrap}.drpp-config>.drpp-panel-heading{flex-wrap:wrap}.drpp-header .drpp-divider{display:none}} +@media(max-width:520px){.drpp-config .drpp-accordion>summary,.drpp-config .drpp-accordion section{padding:16px}.drpp-shell .row label[for]{min-width:0;flex-basis:100%}.drpp-shell .row input:not([type=checkbox]):not([type=range]),.drpp-shell .row select{width:100%}.drpp-shell .drpp-heading{flex-wrap:wrap}.card-style-grid{gap:8px}.drpp-shell #card-form .preview{padding:20px 10px}} +`; diff --git a/src/settings-page-handler.js b/src/settings-page-handler.js index e7095920..cd2bb002 100644 --- a/src/settings-page-handler.js +++ b/src/settings-page-handler.js @@ -1,3 +1,6 @@ +import { cardPresets } from "./card-presets.js"; +import { renderCard } from "./card.js"; +import { SETTINGS_DESIGN_CSS } from "./settings-design.js"; // Local settings page (#253): Discord section (#272), including what // Discord shows when nothing is playing, and card appearance (#94). Served on loopback by // the running app next to the status page. Saves go to /api/settings as JSON; @@ -41,19 +44,20 @@ const buildPage = (startupCopy) => ` NowPlaying settings Skip to configuration
-

NowPlaying

Version: checking...
+

NowPlaying

Version: checking...

Configuration

Save each section below
+
-
Media servers${serverPanel()}
-
Connected services${servicePanel()}
-
Discord Settings +
Media serversConnect Plex, Jellyfin, Emby or Navidrome.${serverPanel()}
+
Connected servicesSpotify and your card hosting account.${servicePanel()}
+
Discord SettingsChoose what appears on your profile.

Discord

@@ -95,7 +99,7 @@ const buildPage = (startupCopy) => `
-
Privacy Settings +
Privacy SettingsControl what other people can see.
-
Card Settings +
Card SettingsChoose a style and make it your own.
-
YouTube Settings +
YouTube SettingsPair your browser extension.
-
Startup Settings +
Startup SettingsLaunch NowPlaying when you sign in.
-
Hosted Card Settings +
Hosted Card SettingsPut your card in a GitHub README.

Hosted card

Puts your card on nowplaying-hosted.vercel.app so a GitHub README can show it without opening your server to the internet. It sends only what your card shows: playing or paused, the title, artist and progress if the card shows them. Never your server address, user name, sign-in, artwork or Discord details. What leaves your PC

@@ -232,7 +238,7 @@ const buildPage = (startupCopy) => `
-
Hosted Card Devices +
Hosted Card DevicesManage the PCs updating your public card.