// Bottom-sheet wybór jakości po resolve. Sortuje malejąco po pikselach (4K > 1080 > 720 > ...). // Single quality → caller pomija modal i odpala od razu. import React from 'react'; import { Modal, Pressable, ScrollView, StyleSheet, Text, View, } from 'react-native'; import { theme } from '../theme'; import type { DefaultQuality } from '../storage'; import type { StreamLink } from '../types'; export function qualityToInt(q: string | null | undefined): number { if (!q) return 0; const s = q.toLowerCase(); if (s.includes('4k') || s.includes('uhd')) return 2160; if (s.includes('2k') || s.includes('qhd')) return 1440; const m = s.match(/\d+/); if (m) return parseInt(m[0], 10); if (s.includes('fhd')) return 1080; if (s.includes('hd')) return 720; if (s.includes('sd')) return 480; return 0; } export function sortByQualityDesc(links: StreamLink[]): StreamLink[] { return [...links].sort((a, b) => qualityToInt(b.quality) - qualityToInt(a.quality)); } /** * Wybierz link pasujący do domyślnej jakości usera; `null` → pokaż picker (auto, brak * dopasowania, albo same nieznane jakości). Reguła: największa wysokość <= target; * gdy nic <= target (wszystkie wyższe) → najbliższa. 'lowest' → najmniejsza dodatnia. * Linki o nieznanej wysokości (qualityToInt=0) pomijamy gdy jest jakikolwiek znany, żeby * nieopisany link nie udawał wybranej jakości. NIE dotyczy movie parts (tam preserveOrder). */ export function pickByDefaultQuality( links: StreamLink[], pref: DefaultQuality, ): StreamLink | null { if (pref === 'auto' || links.length === 0) return null; const known = links .map((l) => ({ l, px: qualityToInt(l.quality) })) .filter((x) => x.px > 0); if (known.length === 0) return null; if (pref === 'lowest') { return known.reduce((a, b) => (b.px < a.px ? b : a)).l; } const target = pref; const atOrBelow = known.filter((x) => x.px <= target); if (atOrBelow.length > 0) { return atOrBelow.reduce((a, b) => (b.px > a.px ? b : a)).l; // największa <= target } return known.reduce((a, b) => Math.abs(b.px - target) < Math.abs(a.px - target) ? b : a, ).l; // wszystkie wyższe → najbliższa } export function PlaybackQualityModal({ visible, links, onSelect, onCancel, title = 'Select quality', /** * preserveOrder=true → nie sortuj (zachowaj kolejność z `links`). Używane dla * paradisehill multipart picker gdzie chcemy Part 1..N rosnąco, nie po "quality" * (qualityToInt("Part 35")=35 sortowałby części od końca). */ preserveOrder = false, }: { visible: boolean; links: StreamLink[]; onSelect: (link: StreamLink) => void; onCancel: () => void; title?: string; preserveOrder?: boolean; }) { const sorted = React.useMemo( () => (preserveOrder ? links : sortByQualityDesc(links)), [links, preserveOrder], ); return ( e.stopPropagation()}> {title} {/* ScrollView: paradisehill movie może mieć 35+ parts, bez scrolla sheet przekraczał ekran i część była nieosiągalna. (Android Alert.alert capował do 3 buttonów, bug-report 2ebd0690 2026-06-07 "35 parts, 3 w popup".) */} {sorted.map((link, i) => { const px = qualityToInt(link.quality); return ( onSelect(link)} > {link.quality || 'unknown'} {px >= 144 ? `${px}p` : ''} {link.type ? ` · ${shortType(link.type)}` : ''} ); })} Cancel ); } function shortType(t: string): string { const low = t.toLowerCase(); if (low.includes('mp4') || low === 'direct') return 'mp4'; if (low.includes('mpegurl') || low.includes('m3u8')) return 'hls'; if (low.includes('dash')) return 'dash'; if (low.includes('webm')) return 'webm'; return t; } const styles = StyleSheet.create({ backdrop: { flex: 1, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'flex-end', }, sheet: { backgroundColor: theme.bg, borderTopLeftRadius: 16, borderTopRightRadius: 16, paddingHorizontal: 16, paddingTop: 14, paddingBottom: 24, borderTopColor: theme.border, borderTopWidth: 1, }, list: { maxHeight: 380 }, listContent: { paddingBottom: 2 }, title: { color: theme.muted, fontSize: 12, textTransform: 'uppercase', letterSpacing: 0.5, marginBottom: 10, }, row: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', backgroundColor: theme.card, borderColor: theme.border, borderWidth: 1, borderRadius: 8, padding: 14, marginBottom: 8, }, rowQuality: { color: theme.fg, fontSize: 16, fontWeight: '600' }, rowMeta: { color: theme.muted, fontSize: 13 }, cancel: { marginTop: 6, padding: 12, alignItems: 'center', }, cancelText: { color: theme.muted, fontSize: 14 }, });