From c1ba5366072915fd95dcc09cd5e53f407370a542 Mon Sep 17 00:00:00 2001 From: goon-foss Date: Tue, 28 Jul 2026 11:56:59 +0200 Subject: [PATCH] feat(player): zmiana jakosci w trakcie odtwarzania User-request 89845707 ('quality customisation button in player'). Do tej pory jakosc dalo sie wybrac TYLKO przed startem (PlaybackQualityModal w SceneDetail); zeby ja zmienic, trzeba bylo wyjsc z playera i puscic scene od nowa. SceneDetail przekazuje teraz cala liste jakosci w params.qualityLinks (label+url+ headers), player pokazuje ikone kolka gdy jest >1 wariant. Przelaczenie robi replace() i przywraca pozycje sprzed podmiany (replace resetuje czas, wiec zapisujemy currentTime i ustawiamy go po zaladowaniu), czyli user nie traci miejsca w filmie. Lista jako overlay, nie Alert - Androidowy AlertDialog renderuje max 3 przyciski, a jakosci bywa wiecej. --- mobile/src/changelog.ts | 7 +++ mobile/src/navigation.tsx | 4 ++ mobile/src/screens/PlayerScreen.tsx | 60 ++++++++++++++++++++++++ mobile/src/screens/SceneDetailScreen.tsx | 15 +++++- 4 files changed, 84 insertions(+), 2 deletions(-) diff --git a/mobile/src/changelog.ts b/mobile/src/changelog.ts index 376665e..f0cdfbe 100644 --- a/mobile/src/changelog.ts +++ b/mobile/src/changelog.ts @@ -16,6 +16,13 @@ export type ChangelogEntry = { }; export const CHANGELOG: ChangelogEntry[] = [ + { + id: '2026-07-28b', + date: 'July 2026', + items: [ + 'Change video quality while watching: tap the gear icon in the player. It keeps your position, so you do not lose your place.', + ], + }, { id: '2026-07-28', date: 'July 2026', diff --git a/mobile/src/navigation.tsx b/mobile/src/navigation.tsx index 8be729e..dfe32f9 100644 --- a/mobile/src/navigation.tsx +++ b/mobile/src/navigation.tsx @@ -82,6 +82,10 @@ export type RootStackParamList = { refererHost?: string; title?: string; mode?: 'video' | 'webview'; + // Lista jakości tego samego wideo (z resolve). Pozwala przełączyć jakość BEZ + // wychodzenia z playera (user-request 89845707) — player robi replace() i wraca + // na zapamiętaną pozycję. Undefined = brak wyboru (jedno źródło). + qualityLinks?: { label: string; url: string; headers?: Record }[]; // Headers do bezpośredniego fetchu CDN URL'a (Referer + User-Agent). Mobile // próbuje direct PIERWSZY — bandwidth idzie device→CDN, nie przez VPS proxy. // Backend zwraca te headers w StreamLink.headers przy resolve. diff --git a/mobile/src/screens/PlayerScreen.tsx b/mobile/src/screens/PlayerScreen.tsx index b8c73bb..d578fac 100644 --- a/mobile/src/screens/PlayerScreen.tsx +++ b/mobile/src/screens/PlayerScreen.tsx @@ -30,6 +30,8 @@ import { theme } from '../theme'; import type { StreamLink } from '../types'; interface RouteParams { + /** Lista jakości tego samego wideo → przełącznik w playerze (user-request 89845707). */ + qualityLinks?: { label: string; url: string; headers?: Record }[]; url: string; sceneId: string; // 'tube:' źródła — do telemetrii odtwarzania (ranking źródeł). Opcjonalne; @@ -215,6 +217,35 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) { } }, [url]); + // Zmiana jakości BEZ wychodzenia z playera (user-request 89845707). Podmieniamy + // źródło i wracamy na tę samą pozycję — replace() resetuje czas, więc zapisujemy go + // przed podmianą i przywracamy po załadowaniu. + const qualityLinks = params.qualityLinks; + const [qualityOpen, setQualityOpen] = React.useState(false); + const [activeQuality, setActiveQuality] = React.useState(null); + const switchQuality = React.useCallback( + (q: { label: string; url: string; headers?: Record }) => { + setQualityOpen(false); + let at = 0; + try { at = player.currentTime || 0; } catch { at = 0; } + try { + player.replace(q.headers ? { uri: q.url, headers: q.headers } : q.url); + setActiveQuality(q.label); + setTimeout(() => { + try { + if (at > 1) player.currentTime = at; + player.play(); + } catch { + // player zdążył się rozmontować — nic nie robimy + } + }, 600); + } catch { + // ignore + } + }, + [player], + ); + const statusEvent = useEvent(player, 'statusChange', { status: player.status }); const status = statusEvent?.status ?? player.status; const playerError = statusEvent?.error; @@ -916,6 +947,11 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) { ) : ( )} + {qualityLinks && qualityLinks.length > 1 ? ( + setQualityOpen(true)} hitSlop={16} style={styles.iconBtn}> + + + ) : null} 🔗 @@ -958,6 +994,30 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) { + {qualityOpen && qualityLinks ? ( + // quality-picker-overlay: lista jakości nad wideo. Bez Modala — Androidowy + // AlertDialog renderuje max 3 przyciski, a jakości bywa więcej. + + Quality + {qualityLinks.map((q: { label: string; url: string; headers?: Record }) => { + const on = (activeQuality ?? '') === q.label || (!activeQuality && q.url === url); + return ( + switchQuality(q)} + style={{ paddingVertical: 10, paddingHorizontal: 22 }} + > + + {on ? '✓ ' : ''}{q.label} + + + ); + })} + setQualityOpen(false)} style={{ paddingVertical: 10 }}> + Cancel + + + ) : null} {status === 'loading' && ( diff --git a/mobile/src/screens/SceneDetailScreen.tsx b/mobile/src/screens/SceneDetailScreen.tsx index c48c980..2f07dc4 100644 --- a/mobile/src/screens/SceneDetailScreen.tsx +++ b/mobile/src/screens/SceneDetailScreen.tsx @@ -597,6 +597,7 @@ function PlaybackButton({ const openAsVideo = async ( link: StreamLink, fallbackEmbedUrl?: string, + allLinks?: StreamLink[], ) => { let refererHost: string | undefined; try { @@ -638,6 +639,16 @@ function PlaybackButton({ headers: isDirect && link.headers ? link.headers : undefined, fallbackProxyUrl: isDirect ? link.stream_url || undefined : undefined, fallbackEmbedUrl, + // Pozostałe jakości → przełącznik w playerze (bez powrotu do listy źródeł). + qualityLinks: (allLinks && allLinks.length > 1) + ? allLinks + .map((l) => ({ + label: l.quality || 'auto', + url: l.direct_url || l.stream_url || '', + headers: l.headers || undefined, + })) + .filter((l) => !!l.url) + : undefined, }); }; @@ -761,7 +772,7 @@ function PlaybackButton({ const autoPick = pickAuto(directLinks); if (autoPick) { markStarted(); - await openAsVideo(autoPick, fallbackEmbedUrl); + await openAsVideo(autoPick, fallbackEmbedUrl, directLinks); return; } setQualityLinks(directLinks); @@ -824,7 +835,7 @@ function PlaybackButton({ // direct link dał 403. `qualityLinks` zawiera tylko stream_url-e, więc // szukamy embed'a w `links` na resolve response wcześniej; tu już go nie // mamy, ale to kompromis (rzadki przypadek multi-quality + dead direct). - await openAsVideo(link); + await openAsVideo(link, undefined, qualityLinks || undefined); } else if (link.embed_url) { let refererHost: string | undefined; try {