Wzorowane na reelu _code_and_chill_ ("the bar melts around it"), ale
technika wzieta z jego drugiego zdania: "every socket is solved for
tangency, so the liquid never creases". Czyli sciezka LICZONA, nie
rozmywana.
Filtr "goo" (rozmycie + prog alfy) byl pierwszym pomyslem i zostal
ODRZUCONY po zajrzeniu w zrodlo react-native-svg. Filtry na Androidzie
dzialaja (GroupView extends RenderableView, ktory obsluguje mFilter), ale
RenderableView.java:376 przy KAZDYM rysowaniu robi Bitmap.createBitmap(
canvas.getWidth(), canvas.getHeight(), ARGB_8888), rysuje do niej element,
naklada programowy filtr i wrzuca wynik z powrotem. Przy animowanej kropli
to pelnoekranowa alokacja na klatke, w nagłowku KAZDEGO ekranu.
liquidTabPath: gniazdo to dwa luki styczne jednoczesnie do krawedzi paska
(srodek na y=top+k) i do kropli (odleglosc srodkow = r+k), stad
dx = sqrt((r+k)^2 - (top+k-cy)^2). Jedna <Path>, zero alokacji.
Weryfikacja liczbowa (nie wizualna): trzy niezmienniki stycznosci trzymaja
sie do 1e-9, piec przypadkow brzegowych OK. Test wylapal brakujacy warunek
`cy - r < top`: bez niego wzor ma rozwiazanie takze gdy kropla siedzi CALA
w pasku (top=26, cy=45, r=17, k=12 -> dx≈28,1) i powstawaloby gniazdo, w
ktore nie ma sie co wtopic.
Animacja: Animated.Value + listener przepisujacy cx do stanu, bo sciezka
to string i nie da sie jej podac Animated wprost (brak Reanimated w
projekcie). ~12 re-renderow SVG na 200 ms przejscia, poza tym komponent stoi.
useNativeDriver=false swiadomie: animujemy geometrie, nie transform.
Etykieta aktywnej zakladki siedzi na wypelnieniu akcentu — jasny tekst daje
tam 5,03:1, jedyny wariant tego koloru przechodzacy AA (patrz theme.ts).
|
||
|---|---|---|
| .. | ||
| android | ||
| assets | ||
| src | ||
| .gitignore | ||
| app.json | ||
| App.tsx | ||
| babel.config.js | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
goon mobile (Expo / React Native)
Mobile client do self-hosted goon backendu. iOS/Android jeden codebase.
Setup
cd mobile
npm install
npx expo start
Następnie:
- Expo Go (iOS/Android): zeskanuj QR z terminala
- Android emulator:
npm run android - iOS simulator:
npm run ios(tylko macOS) - Web preview:
npm run web
Konfiguracja
Po pierwszym uruchomieniu zobaczysz ekran logowania:
- Backend URL — adres twojego goon backendu, np.:
http://192.168.1.10:8000(LAN)https://goon.tvojadomena.dev(przez Cloudflare Tunnel/Caddy)http://100.x.x.x:8000(przez Tailscale)
- API Key — klucz wygenerowany dla backendu, ustawiony w
.env:
Generowanie:API_KEYS=<klucz>python -c "import secrets; print(secrets.token_urlsafe(32))"
Klucz jest trzymany w expo-secure-store (Keychain na iOS, Keystore na Androidzie).
Ekrany
- Login — backend URL + API key, weryfikacja przez
/healthz+/scenes - Scenes — lista scen z search; pull-to-refresh
- Scene detail — performerzy, tagi, źródła, opis
- Merge queue — pending merge candidates, sortowane po score (desc)
- Merge detail — side-by-side dwóch scen + reasons + akcje:
- Merge → keep LEFT (default — left to wcześniejsza/kanoniczna)
- Merge → keep RIGHT (gdy nowsza wersja ma lepsze metadane)
- Reject (keep both) — to nie jest duplikat, zostawiamy oddzielnie
Stack
- Expo SDK 52 + React Native 0.76 (new architecture)
- TypeScript strict
- React Navigation 7 (native stack)
- TanStack Query 5 (cache, optimistic invalidation)
- expo-secure-store (credentials)
Build .apk (sideload na Androida)
npx eas build --profile preview --platform android
Wymaga konta Expo. Bez niego można użyć expo prebuild + gradlew assembleRelease.
Dlaczego nie PWA
Wybrany RN+Expo zamiast PWA bo:
- swipe gestures pasują do triage merge queue
- secure-store jest natywny (Keychain/Keystore) zamiast localStorage
- pull-to-refresh i FlatList virtualization out-of-the-box
- Jeden .apk można rozdać testerom bez App Store