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>
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.accent,
|
||
backgroundColor: theme.bgElevated,
|
||
},
|
||
barTitle: { color: theme.fg, fontSize: 14, fontWeight: '700' },
|
||
barHint: { color: theme.muted, fontSize: 12, marginTop: 2, lineHeight: 16 },
|
||
barChevron: { color: theme.accent, 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.accent,
|
||
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.accent, fontSize: 13, fontWeight: '800' },
|
||
note: { color: theme.muted, fontSize: 13, lineHeight: 19, marginTop: 8 },
|
||
dlBtn: {
|
||
backgroundColor: theme.card,
|
||
borderRadius: 10,
|
||
borderWidth: 1,
|
||
borderColor: theme.accent,
|
||
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' },
|
||
});
|