feat(mobile): quick-play ▶ on scene thumbnails
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) <noreply@anthropic.com>
This commit is contained in:
parent
129c71cf95
commit
17d82d465a
4 changed files with 56 additions and 3 deletions
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -142,6 +142,18 @@ function SceneTileBase({ scene, secondLine = 'studio', seenSince, onLongPress }:
|
|||
<Text style={styles.durText}>{durLabel}</Text>
|
||||
</View>
|
||||
) : 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 ? (
|
||||
<Pressable
|
||||
style={styles.playBtn}
|
||||
hitSlop={6}
|
||||
onPress={() => navigation.navigate('SceneDetail', { id: scene.id, autoplay: true })}
|
||||
>
|
||||
<Text style={styles.playBtnIcon}>▶</Text>
|
||||
</Pressable>
|
||||
) : null}
|
||||
</View>
|
||||
<Text style={[styles.title, dim && styles.titleDim]} numberOfLines={1}>
|
||||
{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,
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export function SceneDetailScreen() {
|
|||
const queryClient = useQueryClient();
|
||||
const route = useRoute<RouteProp<RootStackParamList, 'SceneDetail'>>();
|
||||
const nav = useNavigation<NativeStackNavigationProp<RootStackParamList, 'SceneDetail'>>();
|
||||
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 && (
|
||||
<Section title="Watch">
|
||||
{data.playback_sources.map((p) => (
|
||||
{data.playback_sources.map((p, i) => (
|
||||
<PlaybackButton
|
||||
key={p.id}
|
||||
sceneId={data.id}
|
||||
sceneDurationSec={data.duration_sec ?? null}
|
||||
source={p}
|
||||
// Quick-play: pierwsze źródło (backend-ranked native-first) odpala się samo.
|
||||
autoPlay={!!autoplay && i === 0}
|
||||
/>
|
||||
))}
|
||||
<Text style={styles.tagHint}>long-press: open in browser / mark as broken</Text>
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue