goon/mobile/src/screens/PlaybackQualityModal.tsx
goon-foss a4486980a4 style: myslniki em/en z komentarzy i docstringow (calosc repo)
1745 wystapien w 291 plikach (backend, scripts, mobile). Dominowal jeden ksztalt,
' — ' laczace zdania, wiec poszedl przecinek; myslnik otwierajacy mysl zdjety;
zakresy 0–5 i A–Z na dywiz.

Podzial komentarz/kod robil skaner stanow, nie regex, zeby nie ruszyc myslnika
stojacego w stringu albo w wyrazeniu regularnym. Potwierdzone maszynowo: po wycieciu
komentarzy z wersji HEAD i z dysku obie sa identyczne we wszystkich 291 plikach,
czyli poza komentarzami nie zmienilo sie nic. 233 pliki .py parsuja sie bez bledu,
tsc ma te same 11 bledow w tych samych 3 plikach co przed zmiana.

Dwie rzeczy wymagaly poprawki po automacie: wyrownane listy definicji, gdzie myslnik
byl separatorem kolumny (37 miejsc, kolumna przywrocona), i trzy przeniesienia mysli
do nowej linii, gdzie zostawal wiszacy przecinek na poczatku (poprawione recznie).

Bundla nie republikuje: Hermes i tak nie niesie komentarzy, wiec dla uzytkownika
to zero zmiany.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 11:51:57 +02:00

182 lines
5.7 KiB
TypeScript

// 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 (
<Modal
visible={visible}
transparent
animationType="fade"
onRequestClose={onCancel}
>
<Pressable style={styles.backdrop} onPress={onCancel}>
<Pressable style={styles.sheet} onPress={(e) => e.stopPropagation()}>
<Text style={styles.title}>{title}</Text>
{/* 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".) */}
<ScrollView style={styles.list} contentContainerStyle={styles.listContent}>
{sorted.map((link, i) => {
const px = qualityToInt(link.quality);
return (
<Pressable
key={`${link.stream_url || link.embed_url}-${i}`}
style={styles.row}
onPress={() => onSelect(link)}
>
<Text style={styles.rowQuality}>
{link.quality || 'unknown'}
</Text>
<Text style={styles.rowMeta}>
{px >= 144 ? `${px}p` : ''}
{link.type ? ` · ${shortType(link.type)}` : ''}
</Text>
</Pressable>
);
})}
</ScrollView>
<Pressable style={styles.cancel} onPress={onCancel}>
<Text style={styles.cancelText}>Cancel</Text>
</Pressable>
</Pressable>
</Pressable>
</Modal>
);
}
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 },
});