mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 14:16:16 -08:00
docs(website): illustrate feature guides with app screenshots
This commit is contained in:
1 parent
887ac64d18
commit
807b5ea259
24 files changed
+268
-3
No files matched your search
@@ -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,
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
@@ -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',
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
Reference in new issue
Block a user