From 64aee54fb6b83bd6b6c2e61e516994a2fae071c8 Mon Sep 17 00:00:00 2001 From: jtrzupek Date: Wed, 1 Jul 2026 10:45:50 +0200 Subject: [PATCH] =?UTF-8?q?feat(mobile):=20quick-play=20=E2=96=B6=20on=20s?= =?UTF-8?q?cene=20thumbnails?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tap the ▶ overlay on a tile to play the best source without opening SceneDetail first (user request: fewer steps). It routes to SceneDetail with an autoplay flag that fires the top source's existing resolve on mount — reusing all the phone-side/backend resolve, quality-picker and fallback handling in place rather than duplicating it. With a default quality set (Settings → Playback) this is one tap to video; otherwise the quality chooser still appears. ▶ is hidden in duplicate-select mode; tap elsewhere on the tile = details. Co-Authored-By: Claude Opus 4.8 (1M context) --- mobile/src/changelog.ts | 7 ++++++ mobile/src/components/SceneTile.tsx | 28 ++++++++++++++++++++++++ mobile/src/navigation.tsx | 3 ++- mobile/src/screens/SceneDetailScreen.tsx | 21 ++++++++++++++++-- 4 files changed, 56 insertions(+), 3 deletions(-) diff --git a/mobile/src/changelog.ts b/mobile/src/changelog.ts index 6ca2bae..026eb95 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-01b', + date: 'July 2026', + items: [ + 'Quick play: tap the ▶ on any thumbnail to start the best source right away — with a default quality set (Settings → Playback) it is one tap to video.', + ], + }, { id: '2026-07-01', date: 'July 2026', diff --git a/mobile/src/components/SceneTile.tsx b/mobile/src/components/SceneTile.tsx index f0668ea..5d4f380 100644 --- a/mobile/src/components/SceneTile.tsx +++ b/mobile/src/components/SceneTile.tsx @@ -142,6 +142,18 @@ function SceneTileBase({ scene, secondLine = 'studio', seenSince, onLongPress }: {durLabel} ) : null} + {/* Quick-play (user-request): tap ▶ = od razu graj najlepsze źródło (bez wchodzenia + na SceneDetail). Autoplay resolvuje top-source na mount; z ustawioną domyślną + jakością to 1 tap → wideo. Tap poza ▶ = szczegóły. Ukryte w trybie wyboru dup. */} + {!isSelecting ? ( + navigation.navigate('SceneDetail', { id: scene.id, autoplay: true })} + > + + + ) : null} {scene.title} @@ -226,6 +238,22 @@ const styles = StyleSheet.create({ paddingVertical: 2, borderRadius: 4, }, + playBtn: { + position: 'absolute', + left: '50%', + top: '50%', + marginLeft: -18, + marginTop: -18, + width: 36, + height: 36, + borderRadius: 18, + backgroundColor: 'rgba(0,0,0,0.5)', + borderWidth: 1, + borderColor: 'rgba(255,255,255,0.35)', + alignItems: 'center', + justifyContent: 'center', + }, + playBtnIcon: { color: '#fff', fontSize: 15, marginLeft: 2 }, durText: { color: theme.fg, fontSize: 11, diff --git a/mobile/src/navigation.tsx b/mobile/src/navigation.tsx index 0c4fef9..acea8e0 100644 --- a/mobile/src/navigation.tsx +++ b/mobile/src/navigation.tsx @@ -37,7 +37,8 @@ export type RootStackParamList = { // do title bara (np. 'hqporner.com'). SiteScenes: { origin: string; name: string }; MovieDetail: { id: string }; - SceneDetail: { id: string }; + // autoplay: wejście z ▶ na kafelku (quick-play) — SceneDetail sam odpala najlepsze źródło. + SceneDetail: { id: string; autoplay?: boolean }; Performers: undefined; // `seenSince` (ISO timestamp): jeśli ten performer jest favorited, FavoritesScreen // przekazuje `last_seen_at` SPRZED markFavoriteSeen. Sceny z `created_at > seenSince` diff --git a/mobile/src/screens/SceneDetailScreen.tsx b/mobile/src/screens/SceneDetailScreen.tsx index f99b311..dbbecea 100644 --- a/mobile/src/screens/SceneDetailScreen.tsx +++ b/mobile/src/screens/SceneDetailScreen.tsx @@ -36,7 +36,7 @@ export function SceneDetailScreen() { const queryClient = useQueryClient(); const route = useRoute>(); const nav = useNavigation>(); - const { id } = route.params; + const { id, autoplay } = route.params; const { data, isLoading, error } = useQuery({ queryKey: ['scene', id], @@ -362,12 +362,14 @@ export function SceneDetailScreen() { {data.playback_sources.length > 0 && (
- {data.playback_sources.map((p) => ( + {data.playback_sources.map((p, i) => ( ))} long-press: open in browser / mark as broken @@ -472,10 +474,12 @@ function PlaybackButton({ sceneId, sceneDurationSec, source, + autoPlay = false, }: { sceneId: string; sceneDurationSec: number | null; source: PlaybackSource; + autoPlay?: boolean; }) { const client = useClient(); const queryClient = useQueryClient(); @@ -743,6 +747,19 @@ function PlaybackButton({ } }; + // Quick-play z kafelka (▶ → SceneDetail autoplay): odpal to źródło raz na mount. + // onPress ma pełną logikę resolve (phone-side/backend + quality picker + fallbacki), + // więc reużywamy ją w kontekście SceneDetail zamiast duplikować gdziekolwiek indziej. + const didAutoPlayRef = React.useRef(false); + React.useEffect(() => { + if (autoPlay && !didAutoPlayRef.current) { + didAutoPlayRef.current = true; + void onPress(); + } + // onPress celowo poza deps — strzał raz na mount, nie przy każdym re-renderze. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [autoPlay]); + const onSelectQuality = async (link: StreamLink) => { setQualityLinks(null); try {