Some checks are pending
Backend tests / test (push) Waiting to run
PIN: kazdy etap (obecny, nowy, potwierdzenie) to ten sam PinEntry w tym samym
miejscu, wiec React zostawial wpisane cyfry. Potwierdzenie startowalo z wpisanym
juz PIN-em, a przy zmianie PIN-u nowe cyfry doklejaly sie do starych i zapisywal
sie sklejony PIN (zgloszenie a8d85930 "I forgot my pin"). key={stage}. Emulator:
na paczce z produkcji 1234 dwa razy = "PINs do not match", z poprawka zapisane.
Forgot PIN? przy zmianie i wylaczaniu PIN-u: odcisk albo blokada ekranu
telefonu (expo-local-authentication z device fallback); bez blokady komunikat.
Sprawdzone na emulatorze w obu wariantach.
Cast: Huawei z EMUI i uslugami Google (YAL-L21, Android 10) mial przy kazdym
otwarciu filmu krach "background can not be translucent" w MediaRouteButton
(GOON-82, 7 zdarzen). Przycisk Cast wylaczony na Huawei.
Edge: bez wibracji przy przewinieciu (zgloszenie 12db400a).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
99 lines
4.1 KiB
TypeScript
99 lines
4.1 KiB
TypeScript
/**
|
|
* Przycisk Chromecasta i wysyłka materiału na telewizor.
|
|
*
|
|
* Dlaczego to osobny komponent, a nie kilka linijek w `PlayerScreen`: hooki biblioteki
|
|
* (`useRemoteMediaClient` → `useCastSession`) **synchronicznie subskrybują natywne
|
|
* zdarzenia** w `useEffect`. Na urządzeniu bez Google Play Services natywnego modułu nie
|
|
* ma i taka subskrypcja wywraca cały ekran. Hooka nie da się wywołać warunkowo, więc
|
|
* jedynym czystym wyjściem jest zamontowanie tego komponentu dopiero wtedy, gdy wiemy,
|
|
* że Cast działa. To nie jest teoria: mamy użytkownika na LineageOS (GOON-3B) i sideload
|
|
* na zegarku (GOON-51), oba bez pełnego GMS.
|
|
*
|
|
* Sprawdzenie dostępności siedzi w `useCastAvailable` i jest asynchroniczne, więc samo
|
|
* w sobie niczego nie subskrybuje i jest bezpieczne wszędzie.
|
|
*/
|
|
import React from 'react';
|
|
import { Alert, Platform } from 'react-native';
|
|
import { CastButton, CastContext, useRemoteMediaClient } from 'react-native-google-cast';
|
|
|
|
import type { CastTarget } from '../lib/cast';
|
|
import { theme } from '../theme';
|
|
|
|
/**
|
|
* Huawei z EMUI i uslugami Google (Honor 20, YAL-L21, Android 10): nakladka systemowa daje
|
|
* polprzezroczysty kolor motywu, a MediaRouteButton przy tworzeniu liczy na nim kontrast
|
|
* (androidx ColorUtils.calculateContrast) i rzuca "background can not be translucent".
|
|
* Aplikacja padala przy KAZDYM otwarciu filmu (GOON-82, 2026-10-08). Bez przycisku Cast
|
|
* film gra normalnie; na nowszych Huawei bez uslug Google Cast i tak nie dziala.
|
|
*/
|
|
function castCrashesOnThisDevice(): boolean {
|
|
if (Platform.OS !== 'android') return false;
|
|
const c = Platform.constants as { Manufacturer?: string; Brand?: string };
|
|
const maker = `${c.Manufacturer ?? ''} ${c.Brand ?? ''}`.toLowerCase();
|
|
return maker.includes('huawei');
|
|
}
|
|
|
|
/** Czy na tym urządzeniu w ogóle da się castować. Null = jeszcze nie wiadomo. */
|
|
export function useCastAvailable(): boolean {
|
|
const [available, setAvailable] = React.useState(false);
|
|
React.useEffect(() => {
|
|
let alive = true;
|
|
if (castCrashesOnThisDevice()) return;
|
|
(async () => {
|
|
try {
|
|
const state = await CastContext.getPlayServicesState();
|
|
// `success` to jedyny stan, w którym SDK jest gotowe. Reszta (brak usług,
|
|
// wersja do aktualizacji, wyłączone) znaczy dla nas tyle samo: bez castowania.
|
|
if (alive) setAvailable(state === 'success');
|
|
} catch {
|
|
// Natywnego modułu nie ma albo rzucił. Zostajemy przy braku castowania.
|
|
if (alive) setAvailable(false);
|
|
}
|
|
})();
|
|
return () => {
|
|
alive = false;
|
|
};
|
|
}, []);
|
|
return available;
|
|
}
|
|
|
|
type Props = {
|
|
target: CastTarget;
|
|
title?: string;
|
|
/** Pozycja w sekundach, od której ma zacząć telewizor. */
|
|
startTimeSec: number;
|
|
/** Wołane po udanym przejęciu odtwarzania, żeby telefon przestał pobierać. */
|
|
onCastStarted: () => void;
|
|
};
|
|
|
|
export function CastControl({ target, title, startTimeSec, onCastStarted }: Props) {
|
|
const client = useRemoteMediaClient();
|
|
const sentRef = React.useRef<string | null>(null);
|
|
|
|
React.useEffect(() => {
|
|
if (!client) return;
|
|
// Jedno wysłanie na adres. Bez tej blokady każdy re-render ładowałby materiał od
|
|
// nowa i telewizor wracałby do początku.
|
|
if (sentRef.current === target.contentUrl) return;
|
|
sentRef.current = target.contentUrl;
|
|
client
|
|
.loadMedia({
|
|
autoplay: true,
|
|
startTime: Math.max(0, Math.floor(startTimeSec)),
|
|
mediaInfo: {
|
|
contentUrl: target.contentUrl,
|
|
contentType: target.contentType,
|
|
metadata: { type: 'movie', title: title || 'Goon' },
|
|
},
|
|
})
|
|
.then(onCastStarted)
|
|
.catch((e: unknown) => {
|
|
sentRef.current = null;
|
|
Alert.alert('Cast failed', e instanceof Error ? e.message : 'unknown error');
|
|
});
|
|
}, [client, target, title, startTimeSec, onCastStarted]);
|
|
|
|
// Sam przycisk chowa się, gdy w sieci nie widać żadnego urządzenia, więc u kogoś bez
|
|
// Chromecasta pasek playera wygląda dokładnie jak dotąd.
|
|
return <CastButton style={{ width: 28, height: 28, marginHorizontal: 4 }} tintColor={theme.fg} />;
|
|
}
|