Pomiar WCAG na realnych parach z apki (2026-08-13):
accent #B23A48 jako tekst: 3,21 na bg | 2,96 na bgElevated | 2,75 na card
border #3A3128 jako granica: 1,48 na bg | 1,27 na card
Czyli kolor MARKI byl ponizej nawet progu 3:1 dla elementow interfejsu,
a nie tylko ponizej 4,5 dla tekstu. Dotyczylo to aktywnych zakladek,
gwiazdek ulubionych, linkow i chipow. Ramki przy 1,27 nie byly granica,
tylko cieniem.
NIE zmieniam koloru marki. Podniesienie samego `accent` do progu tekstu
wymagaloby zejscia w roz (#CE6975), co likwiduje tozsamosc, ktorej broni
naglowek theme.ts. Zamiast tego rozdzielam ROLE tego samego koloru:
accent zostaje do WYPELNIEN — jasny tekst na nim ma 5,03, przechodzi
accentText #C75260 dla tekstu i obwodek — 4,30 / 3,96 / 3,68
62 podmiany w 24 plikach (34 color, 26 borderColor, 2 thumbColor).
38 wypelnien nietknietych. Sprawdzone skryptem, ze zaden zmieniony tekst
nie stoi na tle akcentu.
borderStrong #7B7065 dla granic NIOSACYCH ZNACZENIE (pola tekstowe, 12
miejsc); dekoracyjne wloski zostaja na `border`. Wartosc dobrana pod obie
powierzchnie, bo pola leza i na bg, i na card: #6B6157 dawal na karcie
2,66, a #73695E dokladnie 2,999, czyli wciaz ponizej progu.
226 lines
8 KiB
TypeScript
226 lines
8 KiB
TypeScript
/**
|
||
* Baner jednorazowej migracji na nowo podpisane APK.
|
||
*
|
||
* Dlaczego w ogóle: klucz podpisujący stare buildy przepadł, a Android pozwala nadpisać
|
||
* aplikację tylko buildem z tym samym podpisem. Nowa wersja wymaga więc odinstalowania
|
||
* starej, co czyści SecureStore razem z `goon.device_id`, czyli kodem synchronizacji.
|
||
* Dane (ulubione, zapisane wyszukiwania, ukryte, postęp) zostają na serwerze pod tym id,
|
||
* ale bez kodu nie ma jak ich odzyskać. Dlatego kod jest w banerze na wierzchu, a nie
|
||
* schowany w ustawieniach: liczy się, żeby user skopiował go PRZED odinstalowaniem.
|
||
*
|
||
* Zwinięty pasek siedzi nad listą scen (nie w ListHeaderComponent, tam znikałby przy
|
||
* wyszukiwaniu i odjeżdżał przy scrollu). Rozwija się w modal z krokami.
|
||
*/
|
||
import React from 'react';
|
||
import { Linking, Modal, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
|
||
import * as Clipboard from 'expo-clipboard';
|
||
|
||
import { MIGRATION_NOTICE, needsManualMigration } from '../migrationNotice';
|
||
import {
|
||
dismissMigrationNotice,
|
||
getDeviceId,
|
||
isMigrationNoticeHidden,
|
||
snoozeMigrationNotice,
|
||
} from '../storage';
|
||
import { theme } from '../theme';
|
||
|
||
export function MigrationBanner() {
|
||
const [show, setShow] = React.useState(false);
|
||
const [open, setOpen] = React.useState(false);
|
||
const [deviceId, setDeviceId] = React.useState('');
|
||
const [copied, setCopied] = React.useState(false);
|
||
|
||
React.useEffect(() => {
|
||
// Warunek na runtime, nie tylko na kanał OTA: ten sam bundle bywa publikowany na 1.2,
|
||
// a tam baner byłby szkodliwy (kazałby migrować z wersji, na którą user właśnie przeszedł).
|
||
if (!needsManualMigration()) return;
|
||
let cancelled = false;
|
||
(async () => {
|
||
try {
|
||
const [hidden, id] = await Promise.all([isMigrationNoticeHidden(), getDeviceId()]);
|
||
if (cancelled || hidden) return;
|
||
setDeviceId(id);
|
||
setShow(true);
|
||
} catch {
|
||
// brak SecureStore → nie ma czym migrować, nie strasz banerem
|
||
}
|
||
})();
|
||
return () => {
|
||
cancelled = true;
|
||
};
|
||
}, []);
|
||
|
||
const copy = React.useCallback(async () => {
|
||
await Clipboard.setStringAsync(deviceId);
|
||
setCopied(true);
|
||
}, [deviceId]);
|
||
|
||
const later = React.useCallback(() => {
|
||
setOpen(false);
|
||
setShow(false);
|
||
snoozeMigrationNotice().catch(() => {});
|
||
}, []);
|
||
|
||
const done = React.useCallback(() => {
|
||
setOpen(false);
|
||
setShow(false);
|
||
dismissMigrationNotice().catch(() => {});
|
||
}, []);
|
||
|
||
if (!show) return null;
|
||
|
||
return (
|
||
<>
|
||
<Pressable style={styles.bar} onPress={() => setOpen(true)}>
|
||
<View style={{ flex: 1 }}>
|
||
<Text style={styles.barTitle}>Update {MIGRATION_NOTICE.version} needs a manual install</Text>
|
||
<Text style={styles.barHint}>
|
||
Copy your sync code first, or your favourites do not come across. Tap for the steps.
|
||
</Text>
|
||
</View>
|
||
<Text style={styles.barChevron}>›</Text>
|
||
</Pressable>
|
||
|
||
<Modal visible={open} transparent animationType="fade" onRequestClose={later}>
|
||
<View style={styles.backdrop}>
|
||
<View style={styles.card}>
|
||
<Text style={styles.title}>Update {MIGRATION_NOTICE.version}</Text>
|
||
<ScrollView style={styles.scroll} contentContainerStyle={{ paddingBottom: 4 }}>
|
||
<Text style={styles.lead}>
|
||
The new build is signed with a different key, so Android will not install it
|
||
over the current app. Uninstall first, install the download second. That wipes
|
||
what is stored on the phone, so copy your sync code before you start.
|
||
</Text>
|
||
|
||
<Text style={styles.step}>1. Copy your sync code</Text>
|
||
<Pressable style={styles.codeRow} onPress={copy}>
|
||
<Text style={styles.code} numberOfLines={1}>
|
||
{deviceId || '…'}
|
||
</Text>
|
||
<Text style={styles.copyBtn}>{copied ? 'Copied' : 'Copy'}</Text>
|
||
</Pressable>
|
||
<Text style={styles.note}>
|
||
Paste it somewhere safe. Without it your favourites, saved searches, hidden
|
||
list and watch progress cannot be found again.
|
||
</Text>
|
||
|
||
<Text style={styles.step}>2. Download the new version</Text>
|
||
<Pressable
|
||
style={styles.dlBtn}
|
||
onPress={() => Linking.openURL(MIGRATION_NOTICE.downloadUrl).catch(() => {})}
|
||
>
|
||
<Text style={styles.dlText}>Open {MIGRATION_NOTICE.downloadUrl.replace(/^https?:\/\//, '').replace(/\/$/, '')}</Text>
|
||
</Pressable>
|
||
|
||
<Text style={styles.step}>3. Uninstall Goon, then open the file</Text>
|
||
<Text style={styles.note}>
|
||
Long-press the app icon, Uninstall. Then open the .apk from your notifications
|
||
or Downloads.
|
||
</Text>
|
||
|
||
<Text style={styles.step}>4. Paste the code back</Text>
|
||
<Text style={styles.note}>
|
||
Settings, Backup & sync, Restore: paste the code, tap Import.
|
||
</Text>
|
||
|
||
<Text style={styles.sorry}>
|
||
A one-off, sorry. Signing keys cannot be recovered, so this once it has to be
|
||
done by hand. Later updates install themselves again.
|
||
</Text>
|
||
</ScrollView>
|
||
|
||
<Pressable style={styles.primaryBtn} onPress={done}>
|
||
<Text style={styles.primaryText}>Done, do not show again</Text>
|
||
</Pressable>
|
||
<Pressable style={styles.secondaryBtn} onPress={later}>
|
||
<Text style={styles.secondaryText}>Remind me tomorrow</Text>
|
||
</Pressable>
|
||
</View>
|
||
</View>
|
||
</Modal>
|
||
</>
|
||
);
|
||
}
|
||
|
||
const styles = StyleSheet.create({
|
||
bar: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 10,
|
||
marginHorizontal: 12,
|
||
marginBottom: 10,
|
||
paddingVertical: 10,
|
||
paddingHorizontal: 12,
|
||
borderRadius: 12,
|
||
borderWidth: 1,
|
||
borderColor: theme.accentText,
|
||
backgroundColor: theme.bgElevated,
|
||
},
|
||
barTitle: { color: theme.fg, fontSize: 14, fontWeight: '700' },
|
||
barHint: { color: theme.muted, fontSize: 12, marginTop: 2, lineHeight: 16 },
|
||
barChevron: { color: theme.accentText, fontSize: 22, fontWeight: '700' },
|
||
|
||
backdrop: {
|
||
flex: 1,
|
||
backgroundColor: 'rgba(0,0,0,0.7)',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
padding: 24,
|
||
},
|
||
card: {
|
||
width: '100%',
|
||
maxHeight: '85%',
|
||
backgroundColor: theme.bgElevated,
|
||
borderRadius: 16,
|
||
borderColor: theme.border,
|
||
borderWidth: 1,
|
||
padding: 20,
|
||
},
|
||
title: { color: theme.fg, fontSize: 20, fontWeight: '800', marginBottom: 12 },
|
||
scroll: { flexGrow: 0 },
|
||
lead: { color: theme.fg, fontSize: 14, lineHeight: 20, marginBottom: 16 },
|
||
step: {
|
||
color: theme.accentText,
|
||
fontSize: 12,
|
||
fontWeight: '800',
|
||
textTransform: 'uppercase',
|
||
letterSpacing: 0.5,
|
||
marginTop: 8,
|
||
marginBottom: 8,
|
||
},
|
||
codeRow: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
gap: 10,
|
||
backgroundColor: theme.card,
|
||
borderRadius: 10,
|
||
borderWidth: 1,
|
||
borderColor: theme.border,
|
||
paddingVertical: 10,
|
||
paddingHorizontal: 12,
|
||
},
|
||
code: { color: theme.fg, fontSize: 13, flex: 1 },
|
||
copyBtn: { color: theme.accentText, fontSize: 13, fontWeight: '800' },
|
||
note: { color: theme.muted, fontSize: 13, lineHeight: 19, marginTop: 8 },
|
||
dlBtn: {
|
||
backgroundColor: theme.card,
|
||
borderRadius: 10,
|
||
borderWidth: 1,
|
||
borderColor: theme.accentText,
|
||
paddingVertical: 12,
|
||
alignItems: 'center',
|
||
},
|
||
dlText: { color: theme.fg, fontSize: 14, fontWeight: '700' },
|
||
sorry: { color: theme.mutedDim, fontSize: 12, lineHeight: 18, marginTop: 18 },
|
||
|
||
primaryBtn: {
|
||
marginTop: 14,
|
||
backgroundColor: theme.accent,
|
||
borderRadius: 10,
|
||
paddingVertical: 12,
|
||
alignItems: 'center',
|
||
},
|
||
primaryText: { color: theme.fg, fontSize: 15, fontWeight: '700' },
|
||
secondaryBtn: { marginTop: 8, paddingVertical: 10, alignItems: 'center' },
|
||
secondaryText: { color: theme.muted, fontSize: 14, fontWeight: '600' },
|
||
});
|