docs(website): illustrate feature guides with app screenshots

This commit is contained in:
4gray committed 2026-09-27 22:09:17 +02:00
1 parent 887ac64d18
commit 807b5ea259
24 files changed
+268 -3

No files matched your search

+7
View File
@@ -163,6 +163,13 @@ Evergreen how-to posts live in the blog collection next to release notes
channel ids that match no playlist `tvg-id` on purpose) into the isolated
database through the settings, then right-click a channel of the M3U
fixture and search the dialog.
The 0.23/0.24 feature articles use the separate `feature-guides` capture
group (`pnpm release:screenshots --group feature-guides --theme dark`).
Playback shots decode the generated local slate documented in
`tools/release/fixtures/README.md`; settings and library shots use the same
isolated demo profile. The multi-channel EPG image is the original image
attached to the v0.24.0 GitHub release, explicitly selected by the maintainer;
its provenance is recorded alongside the assets. It is not a mock capture.
`tools/testing/website-guides.test.mjs` (part of `pnpm nx test website`) checks
each guide for the FAQPage schema, a link to the download hub and the presence
@@ -0,0 +1,11 @@
# Screenshot provenance
- `multi-epg-v0-24.jpg`: original JPEG attached to the
[v0.24.0 release](https://github.com/4gray/iptvnator/releases/tag/v0.24.0),
reused at the maintainer's explicit request on 2026-09-27.
[Original attachment](https://github.com/user-attachments/assets/205dc53e-70cf-4c63-a49b-9b976fbeb5e5).
Downloaded without edits; 2648×1626.
- `screenshots/guide-*-dark.png`: actual Electron UI captured with isolated
fictional playlists by the `feature-guides` group in
`tools/release/screenshots.manifest.json`. Playback uses the original local
demo slate documented in `tools/release/fixtures/README.md`.
Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

@@ -3,6 +3,7 @@ title: Switch Channels and Episodes Without Leaving Fullscreen
description: Open IPTVnator's fullscreen side panel with C or the left edge of the video. Search live channels, choose a season, and switch episodes while keeping the player fullscreen.
pubDate: 2026-09-27
author: 4gray
heroImage: /iptvnator/blog/feature-guides/screenshots/guide-fullscreen-channels-dark.png
tags:
- guide
- playback
@@ -62,6 +63,10 @@ For M3U, **Page Up / Page Down** also let you step through channels without open
panel. Those keys have a different job inside the separate programme Guide, where they
move between days.
![Fullscreen video with the live-channel list open on the left](/iptvnator/blog/feature-guides/screenshots/guide-fullscreen-channels-dark.png)
*Press C in player fullscreen to open the channel list. The pictured channels and video are local demo fixtures.*
## Choose another episode
Start an Xtream or Stalker series episode and open the panel the same way. Instead of a
@@ -3,13 +3,14 @@ title: "Keep Your Library Tidy: Sources, Favorites and Watched Titles"
description: Review source availability, return from a favorite channel to its playlist, mark titles watched and simplify catalog views in IPTVnator without confusing hidden items with missing data.
pubDate: 2026-09-27
author: 4gray
heroImage: /iptvnator/blog/feature-guides/screenshots/guide-library-watched-dark.png
tags:
- guide
draft: true
faq:
- q: Does an unavailable source mean my channels were deleted?
a: No. Availability describes a connection check. A timeout, a temporary outage or an unverified account is different from deleting a saved source or losing its channels.
- q: Does source cleanup delete entries automatically?
- q: Does library watched state delete entries automatically?
a: No. The desktop cleanup dialog checks sources and lets you review the selection before deleting. Confirmed expired or disabled accounts can be preselected; uncertain results need review.
- q: What happens when I delete a source?
a: Its saved favorites, history and playback progress are removed with it. Downloaded files are kept. Export a playlist backup first if you want a restorable copy of supported source state.
@@ -83,6 +84,7 @@ Deleting a source also removes its saved favorites, history and playback progres
Downloaded files are kept. Hiding a category or collapsing a panel is a better fit when
your goal is simply to see less in the current view.
## Keep watch state in step with what you finished
For Xtream and Stalker movies, open the detail page and use **Mark as Watched** when you
@@ -94,6 +96,10 @@ Series already have controls for marking a season or the whole series watched. U
when you are catching the library up with your viewing, rather than opening every episode
just to seek to its end.
![Movie detail page with the watched toggle active beside Play and Favorites](/iptvnator/blog/feature-guides/screenshots/guide-library-watched-dark.png)
*The checked watched button marks this fictional demo movie as finished. Click it again to mark the movie as unwatched.*
## Fit more covers on screen
Under **Settings → General**, turn off **Show titles under covers** for a denser movie
@@ -2,7 +2,9 @@
title: Browse the TV Guide Without Leaving Your Channel
description: Keep watching while you compare channel schedules in IPTVnator's M3U programme guide. Open the grid, filter by group or favorites, and switch channels without closing it.
pubDate: 2026-09-27
updatedDate: 2026-09-27
author: 4gray
heroImage: /iptvnator/blog/feature-guides/multi-epg-v0-24.jpg
tags:
- guide
- epg
@@ -47,6 +49,10 @@ Opening Guide does not restart the current channel. The same player changes size
room for the schedules. Collapse the preview strip when you want more space for the grid,
then expand it when you want to see the picture again.
![Multi-channel EPG with the current channel playing above the programme grid](/iptvnator/blog/feature-guides/multi-epg-v0-24.jpg)
*The multi-channel Guide in v0.24. The highlighted grid button in the top toolbar opens this view. Screenshot from the v0.24.0 release.*
## Choose the channels you want to compare
The rows come from **your playlist**, rather than every channel present in an XMLTV file.
@@ -3,6 +3,7 @@ title: "Subtitles, Quality and Picture-in-Picture: Get More from the Player"
description: Use IPTVnator's unified player controls to choose stream quality, load subtitles, adjust their timing and keep video visible in picture-in-picture. Learn why some options depend on the stream or player.
pubDate: 2026-09-27
author: 4gray
heroImage: /iptvnator/blog/feature-guides/screenshots/guide-player-subtitles-dark.png
tags:
- guide
- playback
@@ -73,6 +74,10 @@ supports your file rather than simply changing its filename extension.
If the stream already includes subtitle tracks, you can choose one directly from the
same menu. **Off** hides subtitles without removing the movie or changing the audio track.
![Subtitle menu in the unified player controls with the Load subtitle file action](/iptvnator/blog/feature-guides/screenshots/guide-player-subtitles-dark.png)
*Open Subtitles from the player control bar to load a subtitle file. Playback here uses a local demonstration stream.*
## Adjust timing and readability
For a selected external subtitle in the web players, use **Show subtitles earlier** or
@@ -3,6 +3,7 @@ title: Back Up Your Playlists and Move to a New Computer
description: Export and restore an IPTVnator playlist backup, understand which favorites and playback state it carries, and check what needs to be moved separately when changing computers.
pubDate: 2026-09-27
author: 4gray
heroImage: /iptvnator/blog/feature-guides/screenshots/guide-backup-dark.png
tags:
- guide
- troubleshooting
@@ -45,6 +46,10 @@ Export the receiving computer's library too if it already has sources and favori
care about. Restoring a snapshot can replace saved state for a matching source; preserving
both backups gives you a record of each library before the import.
![Settings Backup with Import data and Export data actions](/iptvnator/blog/feature-guides/screenshots/guide-backup-dark.png)
*Settings → Backup contains both actions: Export data creates the backup; Import data restores it.*
## What travels in the file
| Source or data | Included | Separate or rebuilt later |
@@ -3,6 +3,7 @@ title: Stable or Nightly? How IPTVnator Updates Work
description: Choose an update channel in the desktop app, understand what happens when you switch back to Stable, and find the build details that make a nightly bug report useful.
pubDate: 2026-09-27
author: 4gray
heroImage: /iptvnator/blog/feature-guides/screenshots/guide-update-channel-dark.png
tags:
- guide
draft: true
@@ -41,6 +42,10 @@ Nightly can be useful when an issue you reported has been fixed but the next sta
has not arrived. Include the build you tested when replying to that issue: two people using
“nightly” may be running different code.
![Settings About with the Stable and Nightly update-channel choices](/iptvnator/blog/feature-guides/screenshots/guide-update-channel-dark.png)
*The update-channel selector lives in Settings → About. Choose a channel here, then save the change.*
## Switch to Nightly
1. Export a playlist backup from **Settings → Backup** and keep the file somewhere you can
@@ -3,6 +3,7 @@ title: What Stream Info Can Tell You About Playback Problems
description: Read resolution, frame rate, bitrate, buffer and dropped frames in IPTVnator's Stream info panel, then collect a useful playback report without guessing from a single number.
pubDate: 2026-09-27
author: 4gray
heroImage: /iptvnator/blog/feature-guides/screenshots/guide-stream-info-dark.png
tags:
- guide
- playback
@@ -45,6 +46,10 @@ The panel shows only values the current engine can report. A missing audio bitra
source frame rate is not a zero reading. Immediately after starting, the panel may say
there is no stream data yet.
![Stream info panel showing metadata for a playing local demonstration stream](/iptvnator/blog/feature-guides/screenshots/guide-stream-info-dark.png)
*The information button opens Stream info over the video. These values describe the local demonstration stream used for this screenshot.*
## Read the values in context
| Field | What it helps you understand |
+3 -1
View File
@@ -89,10 +89,12 @@ export function writeM3uFixture(dataDir: string): string {
['Culture', 'Atlas Culture', 'atlas-culture'],
['Culture', 'Night Music', 'night-music'],
];
const stream = `${XTREAM_MOCK_ORIGIN}/live/marketing/marketing/52000.m3u8`;
const lines = ['#EXTM3U'];
channels.forEach(([group, title, slug], index) => {
// Distinct URLs keep the player from highlighting every demo channel
// as the current one when a fullscreen list is captured.
const stream = `${XTREAM_MOCK_ORIGIN}/live/marketing/marketing/${52000 + index}.m3u8`;
lines.push(
`#EXTINF:-1 tvg-id="demo-${index + 1}" tvg-name="${title}" tvg-logo="${XTREAM_MOCK_ORIGIN}/assets/marketing/logo/${slug}.svg?size=256x256" group-title="${group}",${title}`,
stream
@@ -0,0 +1,94 @@
/** Real UI captures for the 0.23/0.24 feature guides, using local demo media. */
import { readFileSync } from 'node:fs';
import path from 'node:path';
import type { Page } from '@playwright/test';
import { type CaptureAction, openXtreamSection } from './capture-navigation-helpers';
import { openM3uGroups } from './capture-navigation-portal-actions';
import { openSettings } from './capture-navigation-setup-actions';
async function openSettingsSection(page: Page, section: string): Promise<void> {
await openSettings(page);
await page.locator(`[data-test-id="settings-section-${section}"]`).click();
await page.locator(`#${section}`).waitFor({ state: 'visible' });
}
async function openUpdateChannel(page: Page): Promise<void> {
await openSettingsSection(page, 'about');
await page.locator('[data-test-id="select-update-channel"]').click();
await page.locator('[data-test-id="update-channel-nightly"]').waitFor();
}
async function openBackup(page: Page): Promise<void> {
await openSettingsSection(page, 'backup');
}
async function openLibraryWatched(page: Page): Promise<void> {
await openXtreamSection(page, 'vod', 'Action & Mystery');
const watched = page.locator('[data-testid="vod-watched-toggle"]');
await watched.waitFor();
if ((await watched.getAttribute('aria-label')) === 'Mark as Watched') {
await watched.click();
}
await page.getByRole('button', { name: 'Mark as Unwatched', exact: true }).waitFor();
}
async function openDemoPlayback(page: Page): Promise<void> {
await openSettingsSection(page, 'playback');
const player = page.locator('[data-test-id="select-video-player"]');
if (!(await player.innerText()).includes('HTML5')) {
await player.click();
await page.locator('[data-test-id="html5"]').click();
await page.locator('[data-test-id="save-settings"]').click();
await page.locator('[data-test-id="save-settings"]').waitFor({ state: 'hidden' });
}
// A local generated H.264 slate, never a provider stream. Use MPEG-TS
// segments so the normal M3U/HLS player path decodes it without overrides.
const fixture = path.resolve('tools/release/fixtures/guide-demo.mpegts');
await page.route('**/live/marketing/marketing/*.m3u8', (route) =>
route.fulfill({
contentType: 'application/vnd.apple.mpegurl',
body: '#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:20\n#EXT-X-MEDIA-SEQUENCE:0\n#EXTINF:20.0,\nhttp://localhost:3211/demo/guide-demo.ts\n#EXT-X-ENDLIST\n',
})
);
await page.route('**/demo/guide-demo.ts', (route) =>
route.fulfill({ contentType: 'video/mp2t', body: readFileSync(fixture) })
);
await openM3uGroups(page);
await page.locator('[data-test-id="channel-item"]').first().click();
await page.waitForFunction(() => {
const video = document.querySelector('video');
return video && video.readyState >= 2 && video.videoWidth === 1280;
}, undefined, { timeout: 25_000 });
// Freeze decoded media, not the interface, for a repeatable illustration.
await page.locator('video').evaluate((video: HTMLVideoElement) => video.pause());
await page.locator('video').hover();
}
async function openFullscreenChannels(page: Page): Promise<void> {
await openDemoPlayback(page);
await page.getByRole('button', { name: 'Enter fullscreen', exact: true }).click();
await page.keyboard.press('c');
await page.locator('[data-test-id="fullscreen-channel-panel"][aria-hidden="false"]').waitFor();
await page.locator('[data-test-id="m3u-fullscreen-view-all"]').click();
}
async function openSubtitles(page: Page): Promise<void> {
await openDemoPlayback(page);
await page.getByRole('button', { name: 'Subtitles', exact: true }).click();
await page.locator('[data-test-id="player-controls-load-subtitle"]').waitFor();
}
async function openStreamInfo(page: Page): Promise<void> {
await openDemoPlayback(page);
await page.locator('[data-test-id="player-controls-stream-info-button"]').click();
await page.locator('[data-test-id="player-controls-stream-info-panel"]').waitFor();
}
export const FEATURE_ACTIONS: Readonly<Record<string, CaptureAction>> = {
'open-update-channel': openUpdateChannel,
'open-backup': openBackup,
'open-library-watched': openLibraryWatched,
'open-fullscreen-channels': openFullscreenChannels,
'open-player-subtitles': openSubtitles,
'open-stream-info': openStreamInfo,
};
+8
View File
@@ -25,6 +25,7 @@ import { DOWNLOAD_ACTIONS } from './capture-navigation-download-actions';
import { EPG_ACTIONS } from './capture-navigation-epg-actions';
import { PORTAL_ACTIONS } from './capture-navigation-portal-actions';
import { SETUP_ACTIONS } from './capture-navigation-setup-actions';
import { FEATURE_ACTIONS } from './capture-navigation-feature-actions';
export {
clickDialogOption,
@@ -41,6 +42,7 @@ const ACTIONS: Readonly<Record<string, CaptureAction>> = {
...PORTAL_ACTIONS,
...DOWNLOAD_ACTIONS,
...EPG_ACTIONS,
...FEATURE_ACTIONS,
};
/* ------------------------------------------------------------------ */
@@ -93,6 +95,12 @@ export async function runAction(
// dialog open or a dirty settings form. Clear whatever the previous step
// left behind before this one starts navigating: a dialog backdrop
// swallows every click, and unsaved settings raise a leave prompt.
await page.evaluate(async () => {
if (document.fullscreenElement) await document.exitFullscreen();
});
if (await page.locator('.cdk-overlay-backdrop-showing').count()) {
await page.keyboard.press('Escape');
}
await dismissDialogs(page);
await discardUnsavedSettings(page);
await run(page, param);
+27
View File
@@ -0,0 +1,27 @@
# Guide playback fixture
`guide-demo.mpegts` is an original, synthetic 20-second H.264 video slate at
1280×720 / 25 fps, with no audio or third-party footage. Feature-guide captures
serve it through a local HLS manifest, decode it with the real HTML5 player,
and pause the decoded frame for repeatability. No player state or statistics
are fabricated. Subtitle menus use the app's normal file-selection flow.
To regenerate on macOS with FFmpeg (Helvetica is a system font):
```sh
ffmpeg -f lavfi -i 'gradients=s=1280x720:r=25:c0=0x14243e:c1=0x507f9f:n=2:d=20:speed=0.01:seed=24' \
-vf "drawtext=fontfile=/System/Library/Fonts/Helvetica.ttc:text='IPTVnator':fontcolor=white:fontsize=52:x=(w-text_w)/2:y=(h-text_h)/2-30,drawtext=fontfile=/System/Library/Fonts/Helvetica.ttc:text='DEMO STREAM':fontcolor=white@0.6:fontsize=20:x=(w-text_w)/2:y=(h-text_h)/2+35" \
-c:v libx264 -preset fast -crf 28 -pix_fmt yuv420p -g 50 -an \
-f mpegts tools/release/fixtures/guide-demo.mpegts
```
Capture the six feature-guide screens with:
```sh
pnpm nx run electron-backend:build-e2e
pnpm release:screenshots --group feature-guides --theme dark
```
The EPG article uses the original GitHub release image selected by the
maintainer, not this fixture. Its source is documented in
`apps/website/public/blog/feature-guides/SOURCES.md`.
Binary file not shown.
+6
View File
@@ -40,6 +40,12 @@ export const HOST_RESOLVER_RULES =
/** Setup actions the driver implements. Manifest steps must match. */
export const KNOWN_ACTIONS = [
'open-update-channel',
'open-backup',
'open-library-watched',
'open-fullscreen-channels',
'open-player-subtitles',
'open-stream-info',
'open-dashboard',
'open-settings',
'open-xtream-vod',
+48
View File
@@ -203,6 +203,54 @@
"setup": [
"open-settings-tmdb"
]
},
{
"slug": "guide-update-channel",
"group": "feature-guides",
"description": "update channel",
"setup": [
"open-update-channel"
]
},
{
"slug": "guide-backup",
"group": "feature-guides",
"description": "backup",
"setup": [
"open-backup"
]
},
{
"slug": "guide-library-watched",
"group": "feature-guides",
"description": "library watched state",
"setup": [
"open-library-watched"
]
},
{
"slug": "guide-player-subtitles",
"group": "feature-guides",
"description": "player subtitles",
"setup": [
"open-player-subtitles"
]
},
{
"slug": "guide-stream-info",
"group": "feature-guides",
"description": "stream info",
"setup": [
"open-stream-info"
]
},
{
"slug": "guide-fullscreen-channels",
"group": "feature-guides",
"description": "fullscreen channels",
"setup": [
"open-fullscreen-channels"
]
}
]
}
+26 -1
View File
@@ -17,7 +17,7 @@ const SITE = 'https://4gray.github.io/iptvnator';
const GUIDES = [
{
slug: 'm3u-programme-guide',
screenshots: [],
screenshots: ['blog/feature-guides/multi-epg-v0-24.jpg'],
},
{
slug: 'xtream-codes-setup-guide',
@@ -80,6 +80,31 @@ const GUIDES = [
const readDist = (relativePath) => readFile(new URL(relativePath, distRoot), 'utf8');
const FEATURE_GUIDE_SHOTS = {
'stable-nightly-updates-guide': 'guide-update-channel',
'playlist-backup-restore-guide': 'guide-backup',
'library-organization-guide': 'guide-library-watched',
'player-controls-guide': 'guide-player-subtitles',
'stream-info-diagnostics-guide': 'guide-stream-info',
'fullscreen-channel-episode-guide': 'guide-fullscreen-channels',
};
if (process.env.PUBLIC_INCLUDE_DRAFTS === 'true') {
for (const [slug, shot] of Object.entries(FEATURE_GUIDE_SHOTS)) {
GUIDES.push({ slug, screenshots: [`blog/feature-guides/screenshots/${shot}-dark.png`] });
}
}
test('feature guide drafts reference screenshots that ship with the site', async () => {
for (const [slug, shot] of Object.entries(FEATURE_GUIDE_SHOTS)) {
const source = await readFile(new URL(`../../apps/website/src/content/blog/${slug}.mdx`, import.meta.url), 'utf8');
const screenshot = `blog/feature-guides/screenshots/${shot}-dark.png`;
assert.ok(source.includes(`](/iptvnator/${screenshot})`), `${slug}: inline screenshot`);
assert.ok(source.includes(`heroImage: /iptvnator/${screenshot}`), `${slug}: card image`);
await access(new URL(screenshot, distRoot));
}
});
const HUB_GROUPS = ['getting-started', 'live-tv-epg', 'playback', 'library', 'updates'];
const DRAFT_GUIDES = [
'm3u-programme-guide', 'stable-nightly-updates-guide', 'fullscreen-channel-episode-guide',