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 {