replica: one folder per app (replica/wetter)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-17 14:56:22 +02:00
co-authored by Claude Opus 5.5
parent 4b5c9bdf85
commit 64d664fbe9
31 changed files with 11 additions and 11 deletions
+110
View File
@@ -0,0 +1,110 @@
# Architecture: Wetter-Klon (Android, ohne Google)
Basis: `recon.md`, `features.csv`. Schwerpunkt: **modernes Design und Homescreen-Widgets**.
Es gibt keinen Server, keine Accounts und keine Zahlungen. Die App läuft komplett auf dem Gerät und spricht nur öffentliche, schlüsselfreie APIs an.
## Stack
| Schicht | Wahl | warum |
| --- | --- | --- |
| Sprache/UI | Kotlin + Jetpack Compose, **Material 3 Expressive** | native Animationen, Shape-Morphing, Dynamic Color: genau der Look des Originals |
| Hintergründe | Compose Canvas + **AGSL Runtime Shaders** (API 33+), Fallback auf Canvas-Partikel | flüssiger Regen/Schnee/Wolken/Sonne bei 60–120 fps, eigene Grafik statt Google-Assets |
| Widgets | **Jetpack Glance** (+ RemoteViews nur wo nötig) | Compose-artige Widget-API, responsive Größen, Dynamic Color |
| Persistenz | Room (SQLite) + DataStore | eine lokale DB, offline-first |
| Netzwerk | Ktor Client + kotlinx.serialization | leicht, keine Google-Abhängigkeit |
| Hintergrundjobs | WorkManager | Refresh, Warnungen, Widget-Updates ohne Play Services |
| Standort | `android.location.LocationManager` (kein Fused Provider) | funktioniert ohne GMS (GrapheneOS, /e/, LineageOS) |
| Karte | MapLibre Native + OSM-/OpenFreeMap-Tiles, RainViewer-Radarkacheln | Open Source, keine Google Maps |
| DI | Hilt oder manuell (Koin) | Koin: weniger Magie, reicht für eine App |
| Build/Verteilung | Gradle KTS, minSdk 29, targetSdk aktuell, F-Droid-tauglich (keine proprietären Libs) | |
## Datenquellen (offiziell, öffentlich)
| Zweck | API | Hinweise |
| --- | --- | --- |
| Vorhersage aktuell/stündlich/10 Tage | Open-Meteo `/v1/forecast` (`models=best_match`, für DE `icon_seamless`) | kostenlos nichtkommerziell, CC BY 4.0, Attribution in den Einstellungen |
| Minutengenauer Niederschlag | Open-Meteo `minutely_15` | Basis für „Regen in 20 Min“ |
| Luftqualität + Pollen | Open-Meteo Air Quality API | Pollen nur in Europa |
| Ortssuche | Open-Meteo Geocoding API | Name/PLZ |
| Reverse Geocoding (GPS → Name) | Nominatim (OSM), max. 1 Req/s, eigener User-Agent | nur beim Standortwechsel |
| Warnungen DE | DWD Warnungen (`warnings.json`/CAP) über Bright Sky API | |
| Warnungen EU | MeteoAlarm CAP/Atom Feeds | |
| Radar | RainViewer Weather Maps API (Vergangenheit + Nowcast) | Attribution Pflicht |
## Schema
Siehe `schema.sql`: 6 Tabellen (`locations`, `card_layout`, `forecast_cache`, `alerts`, `widget_config`, `notification_pref`), die Einstellungen liegen in DataStore.
Zugriffsregeln: Es gibt keinen Mehrbenutzerbetrieb, Row Level Security entfällt. Die Daten liegen app-privat im Sandbox-Speicher, Backup über Auto Backup ist optional (ausschaltbar).
Harte Constraints: nur ein `is_current`-Ort (partieller Unique-Index), keine doppelten Orte (gerundete Koordinaten), Warnungen werden über die Provider-ID dedupliziert.
## Interne „API“ (Repositories / Use Cases)
| Aufruf | macht | Input | Output | Flow |
| --- | --- | --- | --- | --- |
| `ForecastRepo.observe(locId)` | Flow aus Cache, löst Refresh aus, wenn älter als 30 min | locId | `Forecast` + Status (fresh/stale/offline) | F01 |
| `ForecastRepo.refresh(locId, force)` | Open-Meteo forecast + air laden, Cache schreiben | locId | Result | F01 |
| `LocationRepo.updateCurrent()` | GPS → Reverse Geocoding → `is_current`-Zeile | – | Location | F01 |
| `LocationRepo.search(q)` | Geocoding, entprellt 300 ms | query | List<Place> | F02 |
| `LocationRepo.add/delete/reorder` | CRUD + Standard-`card_layout` anlegen | Place / id / order | – | F02, F03, F04 |
| `LayoutRepo.move(locId, card, toPos)` | Kartenreihenfolge pro Stadt | – | – | F07 |
| `AlertRepo.sync(locId)` | DWD/MeteoAlarm abrufen, abgelaufene löschen | locId | List<Alert> | F08 |
| `RadarRepo.frames()` | RainViewer-Frame-Liste (−2 h … +30 min) | – | Frames | F05 |
| `BriefGenerator.build(forecast, alerts)` | regelbasierter Satz („Ab 15 Uhr Regen, morgen 8° kühler“) | Forecast | String | S01 |
| `SettingsRepo` | Einheiten, Theme, Animationen | – | Flow<Settings> | F06 |
### Hintergrundjobs (WorkManager)
| Job | Takt | macht |
| --- | --- | --- |
| `RefreshWorker` | periodisch 30 min (Netz vorhanden), Batterie nicht niedrig | alle Orte + Widgets aktualisieren |
| `AlertWorker` | 15 min | Warnungen abrufen, neue severe/extreme → Notification |
| `RainSoonWorker` | 15 min, nur Orte mit `rain_alerts` | `minutely_15` prüfen → „Regen in ~X Min“ |
| `TomorrowWorker` | täglich zur eingestellten Uhrzeit | Morgen-Vorhersage als Notification |
| Widget-Update | nach jedem Refresh + `updatePeriodMillis` als Fallback | `GlanceAppWidget.updateAll()` |
## Design-Architektur (Priorität)
- **Theme:** Material 3 Expressive, Dynamic Color (Monet) **plus** eine eigene, zur Wetterlage passende Farbpalette (z. B. klar/Tag → Blau-Gelb, Gewitter → Violett-Grau). `WeatherTheme` liefert Tokens aus `replica-design`.
- **Hintergrund-Engine:** `WeatherScene(condition, isDay, intensity, windDir)` → AGSL-Shader-Layer (Himmelsverlauf, Wolken, Niederschlagspartikel, Blitz, Nebel), mit Parallax beim Scrollen. Bei aktiviertem Battery Saver bzw. „Animationen reduzieren“ wird ein statischer Verlauf gezeigt.
- **Karten:** wiederverwendbare `MetricCard` mit expressiven Formen (Cookie-/Pill-Shapes), eigene Gauges (UV-Bogen, Windkompass, Sonnenbogen, AQI-Skala) in Canvas, alles eigene Icons (z. B. Material Symbols, Apache 2.0, plus eigene Wettericons).
- **Motion:** Shared-Element-Übergang von der Städteliste zur Stadt, Kartendrag mit Spring-Animationen, Haptik beim Einrasten.
- **Edge-to-Edge**, prädiktive Zurück-Geste, Two-Pane-Layout ab Breite „medium“ (Tablet/Foldable).
## Widgets (Priorität)
| Widget | Größen | Inhalt |
| --- | --- | --- |
| Jetzt | 2×1, 2×2 | Icon, Temperatur, H/T, Ort |
| Stündlich | 4×2 | Jetzt + 5–6 Stunden |
| Täglich | 4×2, 4×3 | Jetzt + 5 Tage mit Temperaturbalken |
| Groß/Komplett | 4×4 | Jetzt, stündlich, täglich, Warnhinweis |
| Pille/Glance | 4×1 | Datum + Wetter in einer Zeile (eigener At-a-Glance-Ersatz) |
- Responsive `SizeMode.Responsive`, ein Widget-Typ pro Inhalt mit Größen-Breakpoints.
- Stile: Dynamic Color, Wetterlage als Hintergrund (vorgerenderte Bitmaps, weil Widgets keine Shader können), transparent.
- Konfigurations-Activity: Ort wählen (oder „aktueller Standort“) und den Stil, mit Live-Vorschau.
- Tippen öffnet direkt die jeweilige Stadt, das Refresh-Icon stößt `RefreshWorker` an.
## Was beißt
- **Zeitzonen/DST:** Stunden und Tage immer in der Zeitzone des Orts (`timezone=auto`) zeigen, nicht in der des Geräts.
- **Doze/Akkuoptimierung:** WorkManager-Takte sind nicht garantiert. Bei OEMs mit aggressivem Energiesparen auf die Ausnahme hinweisen.
- **Ohne Play Services:** Kein Fused Location. Für schnelle Ergebnisse `getCurrentLocation` des Netzwerk-Providers mit Timeout nutzen, sonst die letzte bekannte Position.
- **Rate Limits:** Open-Meteo erlaubt 10k Aufrufe am Tag, kein Problem. Nominatim erlaubt 1 Req/s, also cachen. RainViewer-Tiles müssen gecacht werden.
- **Widget-Grenzen:** RemoteViews können keine Animationen und keine Shader, daher Hintergründe vorrendern. Updates sind limitiert, also ereignisgesteuert aktualisieren.
- **Offline:** Der Cache wird immer angezeigt, mit dem Stand „vor X Min“. Ab 3 h wird er als veraltet markiert.
- **Lizenz/Attribution:** Open-Meteo, DWD, RainViewer und OSM kommen in die About-Seite und ins Kartenoverlay.
- **Performance:** Shader auf alten Geräten drosseln (adaptive Qualität), Hintergrund pausieren, wenn er nicht sichtbar ist.
- **DSGVO:** Es gibt kein Tracking und kein Analytics. Koordinaten gehen nur an die Wetter-APIs, auf 2 Nachkommastellen gerundet.
## Build-Reihenfolge
1. **Vertical Slice** (S01, S14): GPS → Open-Meteo → Room → Home mit Hero, stündlicher Leiste und 10-Tage-Liste, schlicht gestaltet. Tabellen `locations`, `forecast_cache`. Beweist den Stack ohne GMS.
2. **Design-Fundament** (S01): Theme, `WeatherScene`-Shader für die 6 Grundlagen (klar, bewölkt, Regen, Schnee, Gewitter, Nebel × Tag/Nacht), `MetricCard` und alle Must-Karten.
3. **Widgets** (S12): Jetzt, Stündlich, Täglich, Konfiguration, `RefreshWorker`. Tabelle `widget_config`.
4. **Orte** (S02, S03): Suche, Hinzufügen, Swipe-to-Delete, Pager zwischen Städten.
5. **Warnungen** (S11, S13): DWD/MeteoAlarm, Banner, `AlertWorker`. Tabelle `alerts`.
6. **Einstellungen** (S07–S09): Einheiten, Theme, Animationen.
7. **Should:** Radar (S06), Kartenreihenfolge (S10, `card_layout`), AQI, Druck, Sicht, Detailansichten (S04, S05), großes Widget und Pillen-Widget, Regen-Benachrichtigung.
8. **Could:** Brief, Pollen, Morgen-Benachrichtigung, Vibrationen, Tablet-Layout.
9. **Fixes** aus `/replica-entrepreneur`.
+19
View File
@@ -0,0 +1,19 @@
# Build log
Stack as in architecture.md. App in `wetter/`. Screenshots are rendered on the JVM (Roborazzi/Robolectric, no KVM for an emulator) from a real Open-Meteo response: `./gradlew :app:recordRoborazziDebug` → `replica/clone-screens/`.
| ID | date | status | missing | harder than expected |
| --- | --- | --- | --- | --- |
| shell | 2026-10-17 | done | – | no JDK on the machine at first; no emulator (no /dev/kvm) → JVM screenshots instead of device screenshots |
| S01 Home | 2026-10-17 | partial | alert banner (no alerts yet), card reordering (S10), true two-pane on tablets | brief contradicted the nowcast while it rained (fixed); sun curve was inverted (fixed) |
| S14 Permissions | 2026-10-17 | done | – | – |
| S02 Locations | 2026-10-17 | done | drag reordering of cities | – |
| S03 Search | 2026-10-17 | done | recent searches | – |
| S15 Preview | 2026-10-17 | done | – | – |
| S04 Day detail | 2026-10-17 | done | – | tiles need day-specific values (noon), not today's |
| S08/S09 Settings | 2026-10-17 | done | notification settings (no notifications yet) | – |
| S12 Widgets | 2026-10-17 | done | – | Glance 1.1 could not render outside the launcher; upgraded to 1.2 for compose() and picker previews, checked on the phone via a debug gallery |
| S12 Widgets (first pass) | 2026-10-17 | superseded | config activity (pick a location), 4×4 and 4×1 types, dynamic-colour style; **no screenshot** (Glance does not render in Robolectric), only verified to compile | widgets cannot draw shaders → the scene is pre-rendered to a bitmap |
| S05, S06, S10, S11, S13 | – | not started | card detail, radar map, reordering, alert detail, notifications | – |
Not yet verified on a real device: location without Play Services, network on device, widgets on the launcher, WorkManager timing.
Binary file not shown.

After

Width:  |  Height:  |  Size: 906 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 896 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 800 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 846 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 978 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 890 KiB

+229
View File
@@ -0,0 +1,229 @@
// Generated from tokens.json by gen_theme.py. Do not edit by hand.
package app.wetter.ui.theme
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
object WColor {
val bg = Color(0xFFF8F9FF)
val surface = Color(0xFFFFFFFF)
val surfaceContainer = Color(0xFFECEEF4)
val surfaceContainerHigh = Color(0xFFE1E3EA)
val border = Color(0xFFC3C6CF)
val borderInput = Color(0xFF73777F)
val text = Color(0xFF191C20)
val textMuted = Color(0xFF43474E)
val accent = Color(0xFF2A5EA8)
val onAccent = Color(0xFFFFFFFF)
val accentContainer = Color(0xFFD6E3FF)
val onAccentContainer = Color(0xFF001B3E)
val danger = Color(0xFFBA1A1A)
val warning = Color(0xFF8A5100)
val success = Color(0xFF2E6B30)
val darkBg = Color(0xFF111318)
val darkSurface = Color(0xFF1D2024)
val darkSurfaceContainer = Color(0xFF272A2F)
val darkBorder = Color(0xFF43474E)
val darkBorderInput = Color(0xFF8D9199)
val darkText = Color(0xFFE2E2E9)
val darkTextMuted = Color(0xFFC3C6CF)
val darkAccent = Color(0xFFA9C7FF)
val darkOnAccent = Color(0xFF003063)
val darkDanger = Color(0xFFFFB4AB)
val darkWarning = Color(0xFFFFB870)
val sceneClearDayTop = Color(0xFF7CC4EF)
val sceneClearDayBottom = Color(0xFFBFE3F7)
val sceneClearNightTop = Color(0xFF3D4F9A)
val sceneClearNightBottom = Color(0xFF5B4F8F)
val sceneCloudyDayTop = Color(0xFFC9CDD2)
val sceneCloudyDayBottom = Color(0xFFDFE2E5)
val sceneRainTop = Color(0xFF2E3D4F)
val sceneRainBottom = Color(0xFF5A6B7D)
val sceneSnowTop = Color(0xFF7C8EA3)
val sceneSnowBottom = Color(0xFFD4DDE6)
val sceneStormTop = Color(0xFF1F1A33)
val sceneStormBottom = Color(0xFF4A3F63)
val sceneFogTop = Color(0xFF8A929B)
val sceneFogBottom = Color(0xFFC4C9CE)
val onScene = Color(0xFFFFFFFF)
val onSceneMuted = Color(0xFFE8EDF3)
val onSceneDark = Color(0xFF14181D)
val sceneCard = Color(0xFF2A3644)
val sceneCardLight = Color(0xFFF2F5F8)
val alertMinor = Color(0xFFFFF2B8)
val onAlertMinor = Color(0xFF3D2F00)
val alertSevere = Color(0xFFFFDBC8)
val onAlertSevere = Color(0xFF4A1A00)
val alertExtreme = Color(0xFF93000A)
val onAlertExtreme = Color(0xFFFFFFFF)
val aqiGood = Color(0xFF2E7D32)
val aqiModerate = Color(0xFFA06A00)
val aqiBad = Color(0xFFC62828)
val uvHigh = Color(0xFFB34700)
val card = Color(0xFFFDF8F3)
val onCard = Color(0xFF1F1B17)
val onCardMuted = Color(0xFF5A524B)
val darkCard = Color(0xFF1F1714)
val darkOnCard = Color(0xFFEDE0DB)
val darkOnCardMuted = Color(0xFFC4B6B0)
val fillPrimary = Color(0xFFD3E3FD)
val onFillPrimary = Color(0xFF0B2A55)
val fillSecondary = Color(0xFFE9DDFF)
val onFillSecondary = Color(0xFF2A1A4D)
val fillTertiary = Color(0xFFC8E6C2)
val onFillTertiary = Color(0xFF0F3A12)
val darkFillPrimary = Color(0xFFA8C7FA)
val darkOnFillPrimary = Color(0xFF062E6F)
val darkFillTertiary = Color(0xFF2D5A27)
val darkOnFillTertiary = Color(0xFFB7F0AC)
val darkFillOlive = Color(0xFF3E4A1E)
val pill = Color(0xFFF1EBE5)
val darkPill = Color(0xFF2B221E)
val pillSelected = Color(0xFFA8C7FA)
val onPillSelected = Color(0xFF0B2A55)
val nowChip = Color(0xFF4E4038)
val onNowChip = Color(0xFFFFFFFF)
val precipText = Color(0xFF2A5EA8)
val darkPrecipText = Color(0xFFA8C7FA)
val nowcastDot = Color(0xFF2E7D32)
val iconSun = Color(0xFFFFB02E)
val iconSunCore = Color(0xFFFF9A1F)
val iconMoon = Color(0xFFA9C4FF)
val iconCloud = Color(0xFFF4F6F9)
val iconCloudShade = Color(0xFFCFD6DF)
val iconCloudDark = Color(0xFF9AA5B3)
val iconDrop = Color(0xFF3D7BF0)
val iconSnow = Color(0xFFFFFFFF)
val iconSnowOutline = Color(0xFF9FB3CC)
val iconBolt = Color(0xFFFFC93C)
val iconFog = Color(0xFFC3CAD3)
val sceneGlowSun = Color(0xFFFFF3C4)
val sceneCloud = Color(0xFFFFFFFF)
val sceneHill = Color(0xFF9FC5E6)
val darkSceneHill = Color(0xFF2F3F78)
val rangeCold = Color(0xFF6FA8FF)
val rangeWarm = Color(0xFFFFB02E)
val aqiBar1 = Color(0xFF4FD1C5)
val aqiBar2 = Color(0xFF9CCC65)
val aqiBar3 = Color(0xFFFFD54F)
val aqiBar4 = Color(0xFFFF8A65)
val aqiBar5 = Color(0xFFE53935)
val aqiBar6 = Color(0xFF8E24AA)
val uvDotLow = Color(0xFF5CCF8A)
val uvDotOff = Color(0xFF6B5A52)
val widgetOceanLightBg = Color(0xFFCCE4F0)
val widgetOceanLightOn = Color(0xFF0F2733)
val widgetOceanLightAccent = Color(0xFF20546F)
val widgetOceanLightPanel = Color(0xFFF9FAFB)
val widgetOceanLightOnpanel = Color(0xFF192124)
val widgetOceanLightBadge = Color(0xFFA8D1E6)
val widgetOceanLightOnbadge = Color(0xFF0F2733)
val widgetOceanDarkBg = Color(0xFF194257)
val widgetOceanDarkOn = Color(0xFFDBECF5)
val widgetOceanDarkAccent = Color(0xFFB0D5E8)
val widgetOceanDarkPanel = Color(0xFF0E1315)
val widgetOceanDarkOnpanel = Color(0xFFE7ECEF)
val widgetOceanDarkBadge = Color(0xFF1F516B)
val widgetOceanDarkOnbadge = Color(0xFFDFEEF6)
val widgetForestLightBg = Color(0xFFD2E9DA)
val widgetForestLightOn = Color(0xFF162D1D)
val widgetForestLightAccent = Color(0xFF2E603F)
val widgetForestLightPanel = Color(0xFFF9FBFA)
val widgetForestLightOnpanel = Color(0xFF1B221D)
val widgetForestLightBadge = Color(0xFFB3DBC0)
val widgetForestLightOnbadge = Color(0xFF162D1D)
val widgetForestDarkBg = Color(0xFF244C32)
val widgetForestDarkOn = Color(0xFFE0F0E5)
val widgetForestDarkAccent = Color(0xFFBADEC6)
val widgetForestDarkPanel = Color(0xFF101411)
val widgetForestDarkOnpanel = Color(0xFFE8EDEA)
val widgetForestDarkBadge = Color(0xFF2D5D3D)
val widgetForestDarkOnbadge = Color(0xFFE3F2E8)
val widgetSunLightBg = Color(0xFFF7E4C5)
val widgetSunLightOn = Color(0xFF3A2808)
val widgetSunLightAccent = Color(0xFF7D5612)
val widgetSunLightPanel = Color(0xFFFBFAF9)
val widgetSunLightOnpanel = Color(0xFF272117)
val widgetSunLightBadge = Color(0xFFF1D29D)
val widgetSunLightOnbadge = Color(0xFF3A2808)
val widgetSunDarkBg = Color(0xFF62430E)
val widgetSunDarkOn = Color(0xFFF9EDD7)
val widgetSunDarkAccent = Color(0xFFF2D6A6)
val widgetSunDarkPanel = Color(0xFF17130D)
val widgetSunDarkOnpanel = Color(0xFFF0ECE5)
val widgetSunDarkBadge = Color(0xFF785311)
val widgetSunDarkOnbadge = Color(0xFFFAEFDB)
val widgetLavenderLightBg = Color(0xFFDBCFED)
val widgetLavenderLightOn = Color(0xFF1F1230)
val widgetLavenderLightAccent = Color(0xFF422768)
val widgetLavenderLightPanel = Color(0xFFFAF9FB)
val widgetLavenderLightOnpanel = Color(0xFF1E1A23)
val widgetLavenderLightBadge = Color(0xFFC3AEE0)
val widgetLavenderLightOnbadge = Color(0xFF1F1230)
val widgetLavenderDarkBg = Color(0xFF341F51)
val widgetLavenderDarkOn = Color(0xFFE6DEF2)
val widgetLavenderDarkAccent = Color(0xFFC8B5E3)
val widgetLavenderDarkPanel = Color(0xFF110F15)
val widgetLavenderDarkOnpanel = Color(0xFFEAE7EE)
val widgetLavenderDarkBadge = Color(0xFF402664)
val widgetLavenderDarkOnbadge = Color(0xFFE9E1F4)
val widgetGraphiteLightBg = Color(0xFFDBDDE1)
val widgetGraphiteLightOn = Color(0xFF1E2124)
val widgetGraphiteLightAccent = Color(0xFF42464D)
val widgetGraphiteLightPanel = Color(0xFFFAFAFA)
val widgetGraphiteLightOnpanel = Color(0xFF1E1E1F)
val widgetGraphiteLightBadge = Color(0xFFC2C6CB)
val widgetGraphiteLightOnbadge = Color(0xFF1E2124)
val widgetGraphiteDarkBg = Color(0xFF34373D)
val widgetGraphiteDarkOn = Color(0xFFE6E8EA)
val widgetGraphiteDarkAccent = Color(0xFFC8CBD0)
val widgetGraphiteDarkPanel = Color(0xFF111212)
val widgetGraphiteDarkOnpanel = Color(0xFFEAEBEB)
val widgetGraphiteDarkBadge = Color(0xFF3F444A)
val widgetGraphiteDarkOnbadge = Color(0xFFE9EAEC)
val darkAccentContainer = Color(0xFF244A80)
val darkOnAccentContainer = Color(0xFFD6E3FF)
}
object WType {
val labelSm = TextStyle(fontSize = 11.sp, lineHeight = 16.sp, fontWeight = FontWeight.W500)
val label = TextStyle(fontSize = 12.sp, lineHeight = 16.sp, fontWeight = FontWeight.W500)
val bodySm = TextStyle(fontSize = 14.sp, lineHeight = 20.sp, fontWeight = FontWeight.W400)
val body = TextStyle(fontSize = 16.sp, lineHeight = 24.sp, fontWeight = FontWeight.W400)
val title = TextStyle(fontSize = 18.sp, lineHeight = 24.sp, fontWeight = FontWeight.W500)
val headline = TextStyle(fontSize = 24.sp, lineHeight = 32.sp, fontWeight = FontWeight.W500)
val metric = TextStyle(fontSize = 32.sp, lineHeight = 40.sp, fontWeight = FontWeight.W500)
val display = TextStyle(fontSize = 112.sp, lineHeight = 112.sp, fontWeight = FontWeight.W700)
val displayWidget = TextStyle(fontSize = 44.sp, lineHeight = 48.sp, fontWeight = FontWeight.W400)
val dayRange = TextStyle(fontSize = 56.sp, lineHeight = 60.sp, fontWeight = FontWeight.W600)
val heroCondition = TextStyle(fontSize = 22.sp, lineHeight = 28.sp, fontWeight = FontWeight.W400)
val cardTitle = TextStyle(fontSize = 16.sp, lineHeight = 24.sp, fontWeight = FontWeight.W500)
}
object WSpace {
val s0 = 0.dp
val s2 = 2.dp
val s4 = 4.dp
val s8 = 8.dp
val s12 = 12.dp
val s16 = 16.dp
val s20 = 20.dp
val s24 = 24.dp
val s32 = 32.dp
val s48 = 48.dp
}
object WRadius {
val xs = 8.dp
val sm = 12.dp
val md = 16.dp
val lg = 24.dp
val xl = 28.dp
val widget = 24.dp
val pill = 999.dp
val card = 28.dp
val dayPill = 24.dp
}
+171
View File
@@ -0,0 +1,171 @@
# Komponenten: Wetter-Klon (Compose, Material 3 Expressive)
Tokens: `tokens.json` → `Tokens.kt` (über `gen_theme.py`). Komponenten verwenden nur Rollen (`WColor.onScene`), nie rohe Hex-Werte.
Icons: Material Symbols Rounded (Apache 2.0) für die UI. Die Wettericons sind eigene oder [Meteocons](https://github.com/basmilius/weather-icons) (MIT), animiert über Lottie. Kein Google-Artwork.
Schrift: Roboto Flex (OFL). Texte sind alle neu geschrieben, auf Deutsch.
## Grundregel Kontrast über Szenen
Text auf dem animierten Himmel nutzt `on-scene`, bei hellen Szenen (Schnee, Nebel, unterer Tagesverlauf) `on-scene-dark`. Die Szene meldet ihre Helligkeit (`scene.isLight`), und das Theme wählt danach.
Beim klaren Tageshimmel oben besteht weißer Text nur als Großtext (3,9:1). Kleiner Text dort braucht deshalb einen Scrim, einen Verlauf von 0 auf 25 % Schwarz hinter dem Hero-Kopf.
Karten auf der Szene: `scene-card` mit 85 % Deckkraft plus Blur (API 31+, `RenderEffect`). Ohne Blur wird `scene-card` voll deckend gezeigt.
---
```
WeatherScene (animierter Hintergrund)
variants clear, partly-cloudy, cloudy, rain, drizzle, snow, sleet, storm, fog × day/night; intensity 0..1
layers sky gradient (scene-*-top/bottom) → sun/moon+stars → clouds (AGSL noise) → precipitation particles → lightning flash → fog
states playing, paused (off-screen / lifecycle STOPPED), reduced (battery saver, "Animationen entfernen", setting off → static gradient), transitioning (crossfade 600ms between conditions)
motion parallax 0.3× on scroll, wind direction tilts rain, haptic tick on lightning (if vibrations on, never during severe alert)
a11y decorative: no semantics; condition always also stated in text
perf adaptive quality (particle count by frame time), 30fps cap on low-end
used on S01, S02 (row backgrounds, static), S12 (pre-rendered bitmap)
HeroBlock
content city name (title) + location pin if current, temperature (display 96sp, wdth 125), condition (headline), "Gefühlt 12°" + H/T (body)
states loading (shimmer on numbers, scene neutral), filled, stale ("Stand: vor 2 Std" chip), offline (cloud-off icon chip), no-permission (CTA "Standort erlauben")
tokens on-scene / on-scene-dark, space 24 top
motion temperature counts up on first load (300ms), collapses into app bar on scroll (large → small top app bar)
a11y one merged semantics node: "Berlin, 14 Grad, leicht bewölkt, gefühlt 12, Höchstwert 17, Tiefstwert 9"
used on S01
AlertBanner
variants minor (alert-minor), moderate/severe (alert-severe), extreme (alert-extreme)
content icon, event ("Sturmböen"), agency + validity ("DWD · bis 18:00"), chevron
states single, stacked (n more), expanded (inline first lines), expired (removed with fade)
tokens radius lg, padding 16
a11y liveRegion polite on appear; whole banner is one button "Warnung: Sturmböen, Deutscher Wetterdienst, bis 18 Uhr. Details"
used on S01 (top of cards, replaces Brief), S11
BriefCard
content 1–2 sentences, generated (e.g. "Ab 15 Uhr Regen, abends trocken. Morgen 6° kühler.")
states loading (two shimmer lines), filled, hidden (alert active or setting off)
tokens scene-card, body, radius xl
used on S01
HourlyStrip
content per hour: time label, icon, temp, precip % if ≥ 20 %; sunrise/sunset inserted as items; "Jetzt" first
states loading, filled, scroll; selected-hour (tap shows detail tooltip)
extra temperature line chart behind items (Canvas, smooth cubic)
a11y LazyRow, each item "15 Uhr, 14 Grad, Regen 40 Prozent"
used on S01 (fixed position), S04, S12 (widget variant)
DayRow / DailyList
content weekday ("Heute", "Mo"), icon, precip %, low temp, range bar (gradient cold→warm over the 10-day min..max, dot = current temp today), high temp
states default, pressed (ripple), expanded (inline hourly, optional)
a11y "Montag, Regen 60 Prozent, 8 bis 15 Grad"
used on S01, S12
MetricCard (base for all detail cards)
variants small 1×1, wide 2×1; shapes per tokens.shape (squircle, cookie, arch, pill)
slots header (icon + label), value (metric 32sp), visual (gauge/chart), footer (one-line explanation)
states default, pressed (morph radius→md, spring-fast), dragging (elevation card-drag, scale 1.04, haptic), placeholder (dashed border while dragging), loading, unavailable ("In deiner Region nicht verfügbar")
tokens scene-card / scene-card-light, on-scene, radius lg
a11y button → opens S05; reorder via accessibility actions "Nach oben/unten verschieben"
used on S01, S10
instances
Precip next-hour bar chart (minutely_15) + "Regen in ~20 Min" / total today
Wind compass dial with arrow, speed, gusts, Beaufort label
UV cookie shape, arc 0–11+, colour by band (uv-high etc.), "Sonnenschutz 11–16 Uhr"
Humidity value + dew point, vertical fill
Pressure dial with trend arrow (rising/falling over 3h)
Visibility value + description
Sun arch with sun position dot, sunrise/sunset times, day length
AQI pill scale (aqi-good..aqi-bad), value, main pollutant
Pollen top 3 types with level dots (Europe only, else hidden)
Map static radar preview thumbnail → opens S06
RadarMap (S06)
content MapLibre + OSM base (light/dark style), RainViewer overlay, location dot, attribution
controls timeline scrubber (past 2h … +30min), play/pause FAB, time label, legend
states loading tiles, playing, paused, no-coverage, offline
a11y scrubber is a Slider with "13:40, Vorhersage" values
used on S06
CityRow
content name, admin area, local time, temp, condition icon, H/T; current-location row with pin and not deletable
states default, pressed, swiping (red bg + trash icon reveal), deleted (snackbar "Gelöscht · Rückgängig"), dragging (reorder)
bg static mini scene gradient for the city's condition
motion shared element (name + temp) into S01
used on S02
SearchField + ResultList
states empty (recents), typing (debounced 300ms, progress), results, no results ("Kein Ort gefunden"), error (retry)
a11y SearchBar (M3), results announce count
used on S03
TopAppBar
variants transparent over scene (S01), surface (S02, S08)
content menu/list button, city title (collapsed), settings icon (no account avatar)
used on S01, S02, S08
PagerIndicator
dots for saved cities, current one wide (expressive pill); swipe between cities
used on S01
Settings rows
variants radio group, switch, link row, section header
states default, disabled (with reason text), focused
used on S08, S09
Snackbar, Dialog (permission rationale), Shimmer placeholder: M3 standard with tokens.
```
## Widgets (Glance)
```
Widget shell
radius widget (24dp, follows system launcher radius on Android 12+)
styles dynamic_color (surface-container from wallpaper), condition (pre-rendered scene bitmap + on-scene text), transparent (text with shadow)
states loading ("…"), filled, stale (small clock icon), no location ("Ort wählen"), error (tap to retry)
tap opens S01 for that city; refresh icon only on ≥ 4×2
NowWidget 2×1: icon + temp | 2×2: + city, condition, H/T
HourlyWidget 4×2: now block + 6 hours (icon, temp, time)
DailyWidget 4×2: now + 5 days | 4×3: + range bars
FullWidget 4×4: now, 5 hours, 5 days, alert line if active
GlanceWidget 4×1: "Fr., 17. Okt. · ☀ 14°" one line, alert replaces date (two lines: event, agency)
type display-widget 44sp for temps, label for times; size by SizeMode.Responsive breakpoints
a11y contentDescription on root summarising the widget
config S12-config activity: location picker, style, live preview, "Fertig"
```
## Screens → Komponenten (Status)
| Screen | Komponenten |
| --- | --- |
| S01 | WeatherScene, TopAppBar, HeroBlock, AlertBanner, BriefCard, HourlyStrip, DailyList, MetricCard×n, PagerIndicator |
| S02 | TopAppBar, CityRow, SearchField |
| S03 | SearchField, ResultList |
| S04/S05 | HourlyStrip, chart, MetricCard expanded |
| S06 | RadarMap |
| S08/S09 | Settings rows |
| S10 | MetricCard (dragging/placeholder) |
| S11 | AlertBanner expanded, text |
| S12 | Widgets |
## Abgleich mit Screenshots (ref-01..04), überschreibt Abweichungen oben
Gemessen an `replica/screens/ref-*.png`. Das Layout-Muster übernehmen wir, die Grafik zeichnen wir selbst.
- **HeroBlock:** zentrierte Spalte. Oben die Wetterlage (22sp), darunter die Temperatur sehr groß und **fett** (112sp, 700, nicht dünn) mit dem Wettericon direkt rechts daneben, etwa 40 % der Ziffernhöhe, oben ausgerichtet. Dann „Gefühlt 19°“ und „Höchstwert 22° · Tiefstwert 14°“. Am Tag ist der Text dunkel auf hellem Himmel, nachts weiß. Es gibt keinen Scrim, die hellen Szenen sind entsprechend hell gewählt.
- **Szene:** ruhiger, weicher Verlauf mit großen, unscharfen, langsam treibenden Formen (Wolkenschemen, Hügel). Keine fotorealistische Darstellung. Bei Niederschlag fallen dezente Partikel.
- **Karten-Container:** deckend (`card` bzw. `dark-card`, warm getöntes Neutral), Radius 28, Padding 20. Die Kopfzeile besteht aus einem Outline-Icon und dem Titel (card-title). Zwischen den Karten 12 dp Abstand, nach außen 16 dp Rand.
- **HourlyStrip:** Die Spalten zeigen Temperatur, darunter Icon, darunter Niederschlag in % (`precip-text`, klein), darunter die Uhrzeit. Die aktuelle Stunde trägt einen **Cookie-Chip** (`now-chip`) hinter der Temperatur, das Label heißt „Jetzt“.
- **DailyList:** **horizontal** scrollende Reihe vertikaler **Kapseln** (`pill`, Radius 24, ca. 56×190 dp). In jeder Kapsel stehen Höchstwert, Tiefstwert (muted), Icon, Niederschlag in % und Tag plus Datum. Ein Tipp auf die Kapsel öffnet S04.
- **DayDetail (S04, bestätigt):** oben eine Kapselreihe, die gewählte Kapsel ist hervorgehoben (`pill-selected`). Darunter Datum, Ort, „24°/14°“ (day-range 56sp) mit Icon, Wetterlage, die stündliche Vorhersage für diesen Tag und darunter dieselben Detailkarten.
- **Detailkarten:** Raster mit 2 Spalten aus quadratischen Kacheln, **Formen statt einheitlicher Karten**:
- Wind: Kreis, darin ein abgerundetes Dreieck als Blob (`fill-*`), gedreht in die Windrichtung, mittig Wert und Einheit, darunter „Aus NW“
- UV: Cookie mit 12 Wellen, Wert und Stufe, Punkte auf einem unteren Bogen, der aktive Punkt farbig
- Sichtweite: Kreis mit gewellter Innenfläche (`fill-tertiary`), Wert und Einheit
- Luftfeuchte: Rechteck, Füllstand mit gewellter Oberkante, Höhe = %, unten ein runder Chip mit dem Taupunkt
- Luftdruck: Kreis mit 270°-Bogenanzeige, Wert groß, Einheit darunter
- Sonne: Rechteck mit gefülltem Sinus-Hügel über der Horizontlinie, der Nachtteil dunkler, Sonnenpunkt auf der Kurve, Aufgang und Untergang als Zeiten
- Luftqualität: Rechteck mit Verlaufsbalken und Knopf, Wert, Einstufung („Gute Luftqualität“); ohne Daten wird „– –“ gezeigt
- **NowcastCard (neu):** „Regen ab 09:15“ als Titel, Untertitel „Leichter Regen von 09:15 bis 10:45“. Ein Punkt-Balkendiagramm zeigt die nächsten 2–3 Stunden in 15-Minuten-Schritten, die Y-Achse in drei Stufen (leicht, mäßig, stark). Ein Marker-Chip am Beginn, die X-Achse mit Uhrzeiten. Die Karte erscheint nur, wenn in den nächsten 3 Stunden Niederschlag kommt. Daten: Open-Meteo `minutely_15`.
- **BriefCard:** wie eine normale Karte mit Icon, Titel „Kurz gesagt“ und einem Chevron zum Auf- und Zuklappen. Darunter 2–3 Sätze Text in body.
- **Vorschau eines Orts (S15, neu):** Ein gesuchter Ort öffnet die volle Ansicht mit einem Button „Speichern“ oben rechts (gefüllter Pill-Button, Tonal).
- **Abweichungen, die bewusst eigen bleiben:** eigene Icons und Szenen, ein eigener Akzent erst in `/replica-brand`, eigene Texte. Die Kartenformen sind M3-Expressive-Standardformen (`MaterialShapes`: Cookie, Circle, Triangle, Wavy), damit ist nichts vom Original nachgezeichnet.
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env python3
"""tokens.json -> Tokens.kt (Compose). Re-run after every token change."""
import json, re, sys, pathlib
src = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else "tokens.json")
out = pathlib.Path(sys.argv[2] if len(sys.argv) > 2 else src.with_name("Tokens.kt"))
t = json.loads(src.read_text())
camel = lambda s: re.sub(r"-(\w)", lambda m: m.group(1).upper(), s)
L = ["// Generated from tokens.json by gen_theme.py. Do not edit by hand.",
"package app.wetter.ui.theme", "",
"import androidx.compose.ui.graphics.Color",
"import androidx.compose.ui.text.TextStyle",
"import androidx.compose.ui.text.font.FontWeight",
"import androidx.compose.ui.unit.dp",
"import androidx.compose.ui.unit.sp", "",
"object WColor {"]
L += [f" val {camel(k)} = Color(0xFF{v.lstrip('#').upper()})" for k, v in t["color"].items()]
L += ["}", "", "object WType {"]
for k, v in t["type"].items():
L.append(f" val {camel(k)} = TextStyle(fontSize = {v['size']}.sp, lineHeight = {v['line']}.sp, fontWeight = FontWeight.W{v['weight']})")
L += ["}", "", "object WSpace {"]
L += [f" val s{v} = {v}.dp" for v in t["space"]]
L += ["}", "", "object WRadius {"]
L += [f" val {camel(k)} = {v}.dp" for k, v in t["radius"].items()]
L += ["}", ""]
out.write_text("\n".join(L))
print("wrote", out)
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 108 108" width="432" height="432">
<defs><linearGradient id="g" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#4E9BE6"/><stop offset="1" stop-color="#BFE3F7"/></linearGradient>
<clipPath id="c"><circle cx="54" cy="54" r="54"/></clipPath></defs>
<g clip-path="url(#c)"><rect width="108" height="108" fill="url(#g)"/>
<path d="M0.00,108.00 L0.00,77.13 L2.00,76.77 L4.00,76.47 L6.00,76.25 L8.00,76.09 L10.00,76.01 L12.00,76.01 L14.00,76.09 L16.00,76.24 L18.00,76.46 L20.00,76.76 L22.00,77.11 L24.00,77.53 L26.00,77.99 L28.00,78.49 L30.00,79.02 L32.00,79.57 L34.00,80.13 L36.00,80.68 L38.00,81.23 L40.00,81.74 L42.00,82.23 L44.00,82.67 L46.00,83.06 L48.00,83.39 L50.00,83.65 L52.00,83.84 L54.00,83.96 L56.00,84.00 L58.00,83.96 L60.00,83.85 L62.00,83.66 L64.00,83.40 L66.00,83.07 L68.00,82.68 L70.00,82.25 L72.00,81.76 L74.00,81.25 L76.00,80.71 L78.00,80.15 L80.00,79.59 L82.00,79.04 L84.00,78.51 L86.00,78.01 L88.00,77.55 L90.00,77.13 L92.00,76.77 L94.00,76.47 L96.00,76.25 L98.00,76.09 L100.00,76.01 L102.00,76.01 L104.00,76.09 L106.00,76.24 L108.00,76.46 L108.00,108.00 Z" fill="#8EC2EC"/><path d="M0.00,108.00 L0.00,85.97 L2.00,86.20 L4.00,86.44 L6.00,86.69 L8.00,86.96 L10.00,87.23 L12.00,87.52 L14.00,87.81 L16.00,88.10 L18.00,88.38 L20.00,88.67 L22.00,88.95 L24.00,89.22 L26.00,89.48 L28.00,89.72 L30.00,89.95 L32.00,90.16 L34.00,90.35 L36.00,90.52 L38.00,90.67 L40.00,90.79 L42.00,90.88 L44.00,90.95 L46.00,90.99 L48.00,91.00 L50.00,90.98 L52.00,90.94 L54.00,90.87 L56.00,90.77 L58.00,90.65 L60.00,90.50 L62.00,90.32 L64.00,90.13 L66.00,89.92 L68.00,89.69 L70.00,89.44 L72.00,89.18 L74.00,88.91 L76.00,88.63 L78.00,88.34 L80.00,88.05 L82.00,87.76 L84.00,87.47 L86.00,87.19 L88.00,86.92 L90.00,86.65 L92.00,86.40 L94.00,86.16 L96.00,85.94 L98.00,85.74 L100.00,85.56 L102.00,85.40 L104.00,85.27 L106.00,85.16 L108.00,85.08 L108.00,108.00 Z" fill="#A9D3F2"/>
<path d="M78.41,44.00 L78.32,44.76 L78.08,45.49 L77.70,46.20 L77.25,46.87 L76.77,47.50 L76.32,48.10 L75.94,48.71 L75.65,49.33 L75.47,49.99 L75.37,50.70 L75.33,51.46 L75.29,52.25 L75.21,53.05 L75.04,53.83 L74.76,54.55 L74.33,55.19 L73.78,55.71 L73.12,56.12 L72.38,56.42 L71.61,56.64 L70.83,56.81 L70.10,56.99 L69.41,57.21 L68.80,57.50 L68.23,57.89 L67.70,58.37 L67.18,58.92 L66.64,59.50 L66.07,60.07 L65.44,60.56 L64.75,60.93 L64.02,61.14 L63.26,61.19 L62.49,61.08 L61.73,60.83 L61.00,60.50 L60.30,60.14 L59.62,59.80 L58.96,59.53 L58.29,59.36 L57.61,59.29 L56.90,59.32 L56.14,59.41 L55.36,59.50 L54.55,59.56 L53.76,59.54 L52.99,59.38 L52.30,59.08 L51.68,58.62 L51.17,58.04 L50.74,57.37 L50.39,56.64 L50.09,55.91 L49.79,55.21 L49.45,54.58 L49.06,54.02 L48.57,53.53 L48.01,53.10 L47.38,52.68 L46.71,52.25 L46.06,51.78 L45.46,51.24 L44.98,50.64 L44.64,49.95 L44.46,49.21 L44.44,48.44 L44.55,47.65 L44.75,46.87 L44.99,46.11 L45.20,45.38 L45.35,44.68 L45.41,44.00 L45.35,43.32 L45.20,42.62 L44.99,41.89 L44.75,41.13 L44.55,40.35 L44.44,39.56 L44.46,38.79 L44.64,38.05 L44.98,37.36 L45.46,36.76 L46.06,36.22 L46.71,35.75 L47.38,35.32 L48.01,34.90 L48.57,34.47 L49.06,33.98 L49.45,33.42 L49.79,32.79 L50.09,32.09 L50.39,31.36 L50.74,30.63 L51.17,29.96 L51.68,29.38 L52.30,28.92 L52.99,28.62 L53.76,28.46 L54.55,28.44 L55.36,28.50 L56.14,28.59 L56.90,28.68 L57.61,28.71 L58.29,28.64 L58.96,28.47 L59.62,28.20 L60.30,27.86 L61.00,27.50 L61.73,27.17 L62.49,26.92 L63.26,26.81 L64.02,26.86 L64.75,27.07 L65.44,27.44 L66.07,27.93 L66.64,28.50 L67.18,29.08 L67.70,29.63 L68.23,30.11 L68.80,30.50 L69.41,30.79 L70.10,31.01 L70.83,31.19 L71.61,31.36 L72.38,31.58 L73.12,31.88 L73.78,32.29 L74.33,32.81 L74.76,33.45 L75.04,34.17 L75.21,34.95 L75.29,35.75 L75.33,36.54 L75.37,37.30 L75.47,38.01 L75.65,38.67 L75.94,39.29 L76.32,39.90 L76.77,40.50 L77.25,41.13 L77.70,41.80 L78.08,42.51 L78.32,43.24 Z" fill="#FFB02E"/><circle cx="61" cy="44" r="10" fill="#FF9A1F" opacity=".35"/>
<path d="M38,74 h30 a9,9 0 0 0 0,-18 a12,12 0 0 0 -22.5,-3.5 a10,10 0 0 0 -7.5,21.5 z" fill="#D5DEE8" transform="translate(0,2.5)"/><path d="M38,74 h30 a9,9 0 0 0 0,-18 a12,12 0 0 0 -22.5,-3.5 a10,10 0 0 0 -7.5,21.5 z" fill="#FFFFFF"/></g></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

+764
View File
@@ -0,0 +1,764 @@
{
"_about": "Wetter-Klon tokens. Roles measured by eye from replica/screens/ref-01..04 (Pixel Weather, 2024-25). Values are own approximations, no brand colour copied. Names stay, values change in /replica-brand. Check: python3 ~/.claude/skills/replica-design/contrast.py replica/wetter/design/tokens.json",
"color": {
"bg": "#f8f9ff",
"surface": "#ffffff",
"surface-container": "#eceef4",
"surface-container-high": "#e1e3ea",
"border": "#c3c6cf",
"border-input": "#73777f",
"text": "#191c20",
"text-muted": "#43474e",
"accent": "#2a5ea8",
"on-accent": "#ffffff",
"accent-container": "#d6e3ff",
"on-accent-container": "#001b3e",
"danger": "#ba1a1a",
"warning": "#8a5100",
"success": "#2e6b30",
"dark-bg": "#111318",
"dark-surface": "#1d2024",
"dark-surface-container": "#272a2f",
"dark-border": "#43474e",
"dark-border-input": "#8d9199",
"dark-text": "#e2e2e9",
"dark-text-muted": "#c3c6cf",
"dark-accent": "#a9c7ff",
"dark-on-accent": "#003063",
"dark-danger": "#ffb4ab",
"dark-warning": "#ffb870",
"scene-clear-day-top": "#7cc4ef",
"scene-clear-day-bottom": "#bfe3f7",
"scene-clear-night-top": "#3d4f9a",
"scene-clear-night-bottom": "#5b4f8f",
"scene-cloudy-day-top": "#c9cdd2",
"scene-cloudy-day-bottom": "#dfe2e5",
"scene-rain-top": "#2e3d4f",
"scene-rain-bottom": "#5a6b7d",
"scene-snow-top": "#7c8ea3",
"scene-snow-bottom": "#d4dde6",
"scene-storm-top": "#1f1a33",
"scene-storm-bottom": "#4a3f63",
"scene-fog-top": "#8a929b",
"scene-fog-bottom": "#c4c9ce",
"on-scene": "#ffffff",
"on-scene-muted": "#e8edf3",
"on-scene-dark": "#14181d",
"scene-card": "#2a3644",
"scene-card-light": "#f2f5f8",
"alert-minor": "#fff2b8",
"on-alert-minor": "#3d2f00",
"alert-severe": "#ffdbc8",
"on-alert-severe": "#4a1a00",
"alert-extreme": "#93000a",
"on-alert-extreme": "#ffffff",
"aqi-good": "#2e7d32",
"aqi-moderate": "#a06a00",
"aqi-bad": "#c62828",
"uv-high": "#b34700",
"card": "#fdf8f3",
"on-card": "#1f1b17",
"on-card-muted": "#5a524b",
"dark-card": "#1f1714",
"dark-on-card": "#ede0db",
"dark-on-card-muted": "#c4b6b0",
"fill-primary": "#d3e3fd",
"on-fill-primary": "#0b2a55",
"fill-secondary": "#e9ddff",
"on-fill-secondary": "#2a1a4d",
"fill-tertiary": "#c8e6c2",
"on-fill-tertiary": "#0f3a12",
"dark-fill-primary": "#a8c7fa",
"dark-on-fill-primary": "#062e6f",
"dark-fill-tertiary": "#2d5a27",
"dark-on-fill-tertiary": "#b7f0ac",
"dark-fill-olive": "#3e4a1e",
"pill": "#f1ebe5",
"dark-pill": "#2b221e",
"pill-selected": "#a8c7fa",
"on-pill-selected": "#0b2a55",
"now-chip": "#4e4038",
"on-now-chip": "#ffffff",
"precip-text": "#2a5ea8",
"dark-precip-text": "#a8c7fa",
"nowcast-dot": "#2e7d32",
"icon-sun": "#ffb02e",
"icon-sun-core": "#ff9a1f",
"icon-moon": "#a9c4ff",
"icon-cloud": "#f4f6f9",
"icon-cloud-shade": "#cfd6df",
"icon-cloud-dark": "#9aa5b3",
"icon-drop": "#3d7bf0",
"icon-snow": "#ffffff",
"icon-snow-outline": "#9fb3cc",
"icon-bolt": "#ffc93c",
"icon-fog": "#c3cad3",
"scene-glow-sun": "#fff3c4",
"scene-cloud": "#ffffff",
"scene-hill": "#9fc5e6",
"dark-scene-hill": "#2f3f78",
"range-cold": "#6fa8ff",
"range-warm": "#ffb02e",
"aqi-bar-1": "#4fd1c5",
"aqi-bar-2": "#9ccc65",
"aqi-bar-3": "#ffd54f",
"aqi-bar-4": "#ff8a65",
"aqi-bar-5": "#e53935",
"aqi-bar-6": "#8e24aa",
"uv-dot-low": "#5ccf8a",
"uv-dot-off": "#6b5a52",
"widget-ocean-light-bg": "#cce4f0",
"widget-ocean-light-on": "#0f2733",
"widget-ocean-light-accent": "#20546f",
"widget-ocean-light-panel": "#f9fafb",
"widget-ocean-light-onpanel": "#192124",
"widget-ocean-light-badge": "#a8d1e6",
"widget-ocean-light-onbadge": "#0f2733",
"widget-ocean-dark-bg": "#194257",
"widget-ocean-dark-on": "#dbecf5",
"widget-ocean-dark-accent": "#b0d5e8",
"widget-ocean-dark-panel": "#0e1315",
"widget-ocean-dark-onpanel": "#e7ecef",
"widget-ocean-dark-badge": "#1f516b",
"widget-ocean-dark-onbadge": "#dfeef6",
"widget-forest-light-bg": "#d2e9da",
"widget-forest-light-on": "#162d1d",
"widget-forest-light-accent": "#2e603f",
"widget-forest-light-panel": "#f9fbfa",
"widget-forest-light-onpanel": "#1b221d",
"widget-forest-light-badge": "#b3dbc0",
"widget-forest-light-onbadge": "#162d1d",
"widget-forest-dark-bg": "#244c32",
"widget-forest-dark-on": "#e0f0e5",
"widget-forest-dark-accent": "#badec6",
"widget-forest-dark-panel": "#101411",
"widget-forest-dark-onpanel": "#e8edea",
"widget-forest-dark-badge": "#2d5d3d",
"widget-forest-dark-onbadge": "#e3f2e8",
"widget-sun-light-bg": "#f7e4c5",
"widget-sun-light-on": "#3a2808",
"widget-sun-light-accent": "#7d5612",
"widget-sun-light-panel": "#fbfaf9",
"widget-sun-light-onpanel": "#272117",
"widget-sun-light-badge": "#f1d29d",
"widget-sun-light-onbadge": "#3a2808",
"widget-sun-dark-bg": "#62430e",
"widget-sun-dark-on": "#f9edd7",
"widget-sun-dark-accent": "#f2d6a6",
"widget-sun-dark-panel": "#17130d",
"widget-sun-dark-onpanel": "#f0ece5",
"widget-sun-dark-badge": "#785311",
"widget-sun-dark-onbadge": "#faefdb",
"widget-lavender-light-bg": "#dbcfed",
"widget-lavender-light-on": "#1f1230",
"widget-lavender-light-accent": "#422768",
"widget-lavender-light-panel": "#faf9fb",
"widget-lavender-light-onpanel": "#1e1a23",
"widget-lavender-light-badge": "#c3aee0",
"widget-lavender-light-onbadge": "#1f1230",
"widget-lavender-dark-bg": "#341f51",
"widget-lavender-dark-on": "#e6def2",
"widget-lavender-dark-accent": "#c8b5e3",
"widget-lavender-dark-panel": "#110f15",
"widget-lavender-dark-onpanel": "#eae7ee",
"widget-lavender-dark-badge": "#402664",
"widget-lavender-dark-onbadge": "#e9e1f4",
"widget-graphite-light-bg": "#dbdde1",
"widget-graphite-light-on": "#1e2124",
"widget-graphite-light-accent": "#42464d",
"widget-graphite-light-panel": "#fafafa",
"widget-graphite-light-onpanel": "#1e1e1f",
"widget-graphite-light-badge": "#c2c6cb",
"widget-graphite-light-onbadge": "#1e2124",
"widget-graphite-dark-bg": "#34373d",
"widget-graphite-dark-on": "#e6e8ea",
"widget-graphite-dark-accent": "#c8cbd0",
"widget-graphite-dark-panel": "#111212",
"widget-graphite-dark-onpanel": "#eaebeb",
"widget-graphite-dark-badge": "#3f444a",
"widget-graphite-dark-onbadge": "#e9eaec",
"dark-accent-container": "#244a80",
"dark-on-accent-container": "#d6e3ff"
},
"pairs": [
[
"text",
"bg"
],
[
"text",
"surface"
],
[
"text",
"surface-container"
],
[
"text",
"surface-container-high"
],
[
"text-muted",
"bg"
],
[
"text-muted",
"surface"
],
[
"text-muted",
"surface-container"
],
[
"on-accent",
"accent"
],
[
"on-accent-container",
"accent-container"
],
[
"accent",
"bg"
],
[
"accent",
"surface"
],
[
"danger",
"surface"
],
[
"warning",
"surface"
],
[
"success",
"surface"
],
[
"border-input",
"surface",
"ui"
],
[
"accent",
"surface",
"ui"
],
[
"dark-text",
"dark-bg"
],
[
"dark-text",
"dark-surface"
],
[
"dark-text",
"dark-surface-container"
],
[
"dark-text-muted",
"dark-bg"
],
[
"dark-text-muted",
"dark-surface"
],
[
"dark-text-muted",
"dark-surface-container"
],
[
"dark-on-accent",
"dark-accent"
],
[
"dark-accent",
"dark-surface"
],
[
"dark-danger",
"dark-surface"
],
[
"dark-warning",
"dark-surface"
],
[
"dark-border-input",
"dark-surface",
"ui"
],
[
"on-scene",
"scene-clear-night-top"
],
[
"on-scene",
"scene-clear-night-bottom"
],
[
"on-scene",
"scene-rain-top"
],
[
"on-scene",
"scene-rain-bottom"
],
[
"on-scene",
"scene-storm-top"
],
[
"on-scene",
"scene-storm-bottom"
],
[
"on-scene-dark",
"scene-clear-day-bottom"
],
[
"on-scene-dark",
"scene-cloudy-day-bottom"
],
[
"on-scene-dark",
"scene-snow-top"
],
[
"on-scene-dark",
"scene-snow-bottom"
],
[
"on-scene-dark",
"scene-fog-top"
],
[
"on-scene-dark",
"scene-fog-bottom"
],
[
"on-scene",
"scene-card"
],
[
"on-scene-muted",
"scene-card"
],
[
"on-scene-dark",
"scene-card-light"
],
[
"on-alert-minor",
"alert-minor"
],
[
"on-alert-severe",
"alert-severe"
],
[
"on-alert-extreme",
"alert-extreme"
],
[
"aqi-good",
"surface",
"ui"
],
[
"aqi-moderate",
"surface",
"ui"
],
[
"aqi-bad",
"surface",
"ui"
],
[
"uv-high",
"surface",
"ui"
],
[
"on-scene-dark",
"scene-clear-day-top"
],
[
"on-scene-dark",
"scene-cloudy-day-top"
],
[
"on-card",
"card"
],
[
"on-card-muted",
"card"
],
[
"dark-on-card",
"dark-card"
],
[
"dark-on-card-muted",
"dark-card"
],
[
"on-fill-primary",
"fill-primary"
],
[
"on-fill-secondary",
"fill-secondary"
],
[
"on-fill-tertiary",
"fill-tertiary"
],
[
"dark-on-fill-primary",
"dark-fill-primary"
],
[
"dark-on-fill-tertiary",
"dark-fill-tertiary"
],
[
"dark-on-card",
"dark-fill-olive"
],
[
"on-card",
"pill"
],
[
"dark-on-card",
"dark-pill"
],
[
"on-pill-selected",
"pill-selected"
],
[
"on-now-chip",
"now-chip"
],
[
"precip-text",
"card"
],
[
"precip-text",
"pill"
],
[
"dark-precip-text",
"dark-card"
],
[
"dark-precip-text",
"dark-pill"
],
[
"nowcast-dot",
"card",
"ui"
],
[
"widget-ocean-light-on",
"widget-ocean-light-bg"
],
[
"widget-ocean-light-accent",
"widget-ocean-light-bg"
],
[
"widget-ocean-light-onpanel",
"widget-ocean-light-panel"
],
[
"widget-ocean-light-onbadge",
"widget-ocean-light-badge"
],
[
"widget-ocean-dark-on",
"widget-ocean-dark-bg"
],
[
"widget-ocean-dark-accent",
"widget-ocean-dark-bg"
],
[
"widget-ocean-dark-onpanel",
"widget-ocean-dark-panel"
],
[
"widget-ocean-dark-onbadge",
"widget-ocean-dark-badge"
],
[
"widget-forest-light-on",
"widget-forest-light-bg"
],
[
"widget-forest-light-accent",
"widget-forest-light-bg"
],
[
"widget-forest-light-onpanel",
"widget-forest-light-panel"
],
[
"widget-forest-light-onbadge",
"widget-forest-light-badge"
],
[
"widget-forest-dark-on",
"widget-forest-dark-bg"
],
[
"widget-forest-dark-accent",
"widget-forest-dark-bg"
],
[
"widget-forest-dark-onpanel",
"widget-forest-dark-panel"
],
[
"widget-forest-dark-onbadge",
"widget-forest-dark-badge"
],
[
"widget-sun-light-on",
"widget-sun-light-bg"
],
[
"widget-sun-light-accent",
"widget-sun-light-bg"
],
[
"widget-sun-light-onpanel",
"widget-sun-light-panel"
],
[
"widget-sun-light-onbadge",
"widget-sun-light-badge"
],
[
"widget-sun-dark-on",
"widget-sun-dark-bg"
],
[
"widget-sun-dark-accent",
"widget-sun-dark-bg"
],
[
"widget-sun-dark-onpanel",
"widget-sun-dark-panel"
],
[
"widget-sun-dark-onbadge",
"widget-sun-dark-badge"
],
[
"widget-lavender-light-on",
"widget-lavender-light-bg"
],
[
"widget-lavender-light-accent",
"widget-lavender-light-bg"
],
[
"widget-lavender-light-onpanel",
"widget-lavender-light-panel"
],
[
"widget-lavender-light-onbadge",
"widget-lavender-light-badge"
],
[
"widget-lavender-dark-on",
"widget-lavender-dark-bg"
],
[
"widget-lavender-dark-accent",
"widget-lavender-dark-bg"
],
[
"widget-lavender-dark-onpanel",
"widget-lavender-dark-panel"
],
[
"widget-lavender-dark-onbadge",
"widget-lavender-dark-badge"
],
[
"widget-graphite-light-on",
"widget-graphite-light-bg"
],
[
"widget-graphite-light-accent",
"widget-graphite-light-bg"
],
[
"widget-graphite-light-onpanel",
"widget-graphite-light-panel"
],
[
"widget-graphite-light-onbadge",
"widget-graphite-light-badge"
],
[
"widget-graphite-dark-on",
"widget-graphite-dark-bg"
],
[
"widget-graphite-dark-accent",
"widget-graphite-dark-bg"
],
[
"widget-graphite-dark-onpanel",
"widget-graphite-dark-panel"
],
[
"widget-graphite-dark-onbadge",
"widget-graphite-dark-badge"
],
[
"dark-on-accent-container",
"dark-accent-container"
]
],
"font": {
"sans": "Roboto Flex (OFL), system default fallback",
"display": "Roboto Flex, weight 700-800, tight tracking (-2%) for the hero temperature",
"mono": "Roboto Mono"
},
"type": {
"label-sm": {
"size": 11,
"line": 16,
"weight": 500
},
"label": {
"size": 12,
"line": 16,
"weight": 500
},
"body-sm": {
"size": 14,
"line": 20,
"weight": 400
},
"body": {
"size": 16,
"line": 24,
"weight": 400
},
"title": {
"size": 18,
"line": 24,
"weight": 500
},
"headline": {
"size": 24,
"line": 32,
"weight": 500
},
"metric": {
"size": 32,
"line": 40,
"weight": 500
},
"display": {
"size": 112,
"line": 112,
"weight": 700
},
"display-widget": {
"size": 44,
"line": 48,
"weight": 400
},
"day-range": {
"size": 56,
"line": 60,
"weight": 600
},
"hero-condition": {
"size": 22,
"line": 28,
"weight": 400
},
"card-title": {
"size": 16,
"line": 24,
"weight": 500
}
},
"space": [
0,
2,
4,
8,
12,
16,
20,
24,
32,
48
],
"radius": {
"xs": 8,
"sm": 12,
"md": 16,
"lg": 24,
"xl": 28,
"widget": 24,
"pill": 999,
"card": 28,
"day-pill": 24
},
"shape": {
"wind": "circle container, inner rounded-triangle blob rotated to wind direction",
"uv": "scalloped 12-wave cookie, dots on a lower arc show the UV band",
"visibility": "circle container, inner wavy-edged circle fill",
"humidity": "rounded rect, wave-topped fill whose height = humidity",
"pressure": "circle container, 270° arc gauge",
"sun": "rounded rect, filled sine hill above a horizon line, night part darker",
"aqi": "rounded rect, gradient bar with knob",
"now-chip": "8-point soft cookie behind current-hour temp",
"day": "vertical capsule per day (10-day row scrolls horizontally)"
},
"elevation": {
"card": 0,
"card-drag": 6,
"sheet": 1,
"fab": 3
},
"motion": {
"spring-fast": "dampingRatio 0.9, stiffness 1400",
"spring-default": "dampingRatio 0.8, stiffness 380",
"spring-bouncy": "dampingRatio 0.6, stiffness 300",
"scene-crossfade": "600ms",
"fast": "120ms",
"base": "250ms"
},
"layout": {
"gutter": 16,
"card-gap": 12,
"grid-compact": 2,
"grid-expanded": 4,
"two-pane-from": "600dp",
"hero-height": "~45% of viewport",
"app-bar": 64,
"hero": "centered column: condition, temp + inline icon, feels like, high · low",
"card-padding": 20,
"detail-grid": "2 columns, circles left / rects right, equal squares"
}
}
+44
View File
@@ -0,0 +1,44 @@
feature,area,priority,original,clone,notes
Current conditions hero (temp feels-like high/low condition),home,must,yes,yes,
Condition-based animated background,home,must,yes,yes,original artwork not reusable; draw own
Hourly forecast strip (24h+),home,must,yes,yes,fixed position
10-day forecast list,home,must,yes,yes,
Daily detail view (tap a day),home,should,yes,yes,guess: exists in original
Precipitation card with chance and amount,cards,must,yes,yes,
Wind card (speed direction gusts),cards,must,yes,yes,
UV index card,cards,must,yes,yes,
Humidity / dew point card,cards,must,yes,yes,
Pressure card,cards,should,yes,yes,
Visibility card,cards,should,yes,yes,
Sunrise / sunset card,cards,must,yes,yes,
Air quality (AQI) card,cards,should,yes,yes,Open-Meteo air-quality API (CAMS)
Pollen card,cards,could,yes,yes,Open-Meteo pollen (Europe only)
Drag to reorder cards,customize,should,yes,no,
Card order saved per city,customize,could,yes,no,
Location search by name or postal code,locations,must,yes,yes,Open-Meteo geocoding
Current location via GPS,locations,must,yes,yes,no Google Play Services; use platform LocationManager
Saved locations list,locations,must,yes,yes,
Swipe to delete a location,locations,must,yes,yes,
Swipe/pager between saved cities,locations,should,yes,yes,
Temperature unit (default C F),settings,must,yes,yes,
Units for precipitation wind pressure visibility,settings,should,yes,yes,
Theme setting (system light dark),settings,should,yes,yes,
Precipitation map with 6h nowcast and timeline scrubber,map,should,yes,no,RainViewer or DWD radar tiles + OSM base map
Pan and zoom map,map,should,yes,no,
Severe weather alerts banner on top,alerts,must,yes,no,DWD / MeteoAlarm feeds
Severe weather notification,notifications,should,yes,no,background worker
Evening tomorrow-forecast notification,notifications,could,yes,no,
Precipitation-starting notification per city,notifications,could,yes,no,needs minute nowcast
Homescreen widget,widgets,must,yes,yes,5 widget types with config screen
AI weather brief summary,ai,could,yes,partial,"template-based text or local LLM; no Gemini; rule-based brief, no LLM"
Weather vibrations on animations,polish,could,yes,no,
Tablet two-column layout,layout,could,yes,partial,content capped at 720dp and centred; no true two-pane
Sync saved locations across devices via Google account,sync,could,yes,skip,no Google account by design
Personal insights from Calendar and Gmail,ai,could,yes,skip,needs Google data access; against project goal
Google nowcasting / MetNet models,data,could,yes,skip,proprietary model; use public forecast data
At a Glance integration,widgets,could,yes,skip,Pixel launcher system feature
Widget sizes small medium large with hourly/daily,widgets,must,yes,yes,Glance
Widget dynamic Material You colours and condition background,widgets,should,yes,yes,condition background only
Widget tap opens city / refresh on schedule,widgets,must,yes,yes,
Nowcast card (precip starting with 15-min dot chart),cards,should,yes,yes,Open-Meteo minutely_15
Location preview with Save,locations,should,yes,yes,S15
1 feature area priority original clone notes
2 Current conditions hero (temp feels-like high/low condition) home must yes yes
3 Condition-based animated background home must yes yes original artwork not reusable; draw own
4 Hourly forecast strip (24h+) home must yes yes fixed position
5 10-day forecast list home must yes yes
6 Daily detail view (tap a day) home should yes yes guess: exists in original
7 Precipitation card with chance and amount cards must yes yes
8 Wind card (speed direction gusts) cards must yes yes
9 UV index card cards must yes yes
10 Humidity / dew point card cards must yes yes
11 Pressure card cards should yes yes
12 Visibility card cards should yes yes
13 Sunrise / sunset card cards must yes yes
14 Air quality (AQI) card cards should yes yes Open-Meteo air-quality API (CAMS)
15 Pollen card cards could yes yes Open-Meteo pollen (Europe only)
16 Drag to reorder cards customize should yes no
17 Card order saved per city customize could yes no
18 Location search by name or postal code locations must yes yes Open-Meteo geocoding
19 Current location via GPS locations must yes yes no Google Play Services; use platform LocationManager
20 Saved locations list locations must yes yes
21 Swipe to delete a location locations must yes yes
22 Swipe/pager between saved cities locations should yes yes
23 Temperature unit (default C F) settings must yes yes
24 Units for precipitation wind pressure visibility settings should yes yes
25 Theme setting (system light dark) settings should yes yes
26 Precipitation map with 6h nowcast and timeline scrubber map should yes no RainViewer or DWD radar tiles + OSM base map
27 Pan and zoom map map should yes no
28 Severe weather alerts banner on top alerts must yes no DWD / MeteoAlarm feeds
29 Severe weather notification notifications should yes no background worker
30 Evening tomorrow-forecast notification notifications could yes no
31 Precipitation-starting notification per city notifications could yes no needs minute nowcast
32 Homescreen widget widgets must yes yes 5 widget types with config screen
33 AI weather brief summary ai could yes partial template-based text or local LLM; no Gemini; rule-based brief, no LLM
34 Weather vibrations on animations polish could yes no
35 Tablet two-column layout layout could yes partial content capped at 720dp and centred; no true two-pane
36 Sync saved locations across devices via Google account sync could yes skip no Google account by design
37 Personal insights from Calendar and Gmail ai could yes skip needs Google data access; against project goal
38 Google nowcasting / MetNet models data could yes skip proprietary model; use public forecast data
39 At a Glance integration widgets could yes skip Pixel launcher system feature
40 Widget sizes small medium large with hourly/daily widgets must yes yes Glance
41 Widget dynamic Material You colours and condition background widgets should yes yes condition background only
42 Widget tap opens city / refresh on schedule widgets must yes yes
43 Nowcast card (precip starting with 15-min dot chart) cards should yes yes Open-Meteo minutely_15
44 Location preview with Save locations should yes yes S15
+148
View File
@@ -0,0 +1,148 @@
# Recon map: Google Weather / Pixel Weather (Android)
Scope: the whole app: home forecast, detail cards, locations, settings, precipitation map, alerts, notifications, widget, AI brief
For: personal use, a weather app without Google services (project NoGooleApps)
Date: 2026-10-17
## Sources
| # | source | URL | notes |
| --- | --- | --- | --- |
| 1 | app store listing | https://play.google.com/store/apps/details?id=com.google.android.apps.weather | page did not load via fetch; no screenshots saved |
| 2 | help center | https://support.google.com/googlepixeltablet/answer/15266029?hl=en-GB | most complete feature/settings list |
| 3 | review | https://9to5google.com/2024/10/30/pixel-weather-rolling-out/ | cards, reorder, per-city layout, notifications |
| 4 | review | https://9to5google.com/2024/05/23/new-google-weather-android/ | redesign, NowCast |
| 5 | news | https://9to5google.com/2025/01/30/pixel-weather-units-of-measurement-update/ | units settings |
| 6 | news | https://www.notebookcheck.net/Google-is-quietly-killing-Android-s-weather-app-experience.1231441.0.html | 2026: non-Pixel weather redirected to Search |
| 7 | news | https://www.techradar.com/phones/google-pixel-phones/the-pixel-9s-amazing-new-weather-app-is-now-heading-to-older-pixels | AI report on Pixel 9 |
| 8 | news | https://9to5google.com/2021/05/17/pixel-launcher-weather-alerts/ | alert wording (type + issuing agency) |
| 9 | screenshots | replica/screens/ref-01..04.png | supplied by user (Android Authority / connect montages): hero, cards, day view, nowcast, light+dark. Reference only, never shipped |
## Core loop
Open the app and see at a glance what the weather is now, in the next hours and in the coming days for your location, plus whether something severe is coming.
## Screens
| ID | screen | route / how to reach | purpose | key components | states seen |
| --- | --- | --- | --- | --- | --- |
| S01 | Home / city forecast | app launch | current weather + all cards for one city | hero, alert banner, brief, nowcast card, hourly strip, 10-day capsules, shaped detail cards, animated background | loading, filled, alert active, no location permission, offline (guess) |
| S02 | Saved locations | top-left / back from S01 | switch, add, delete cities | current-location row, city rows with temp/condition, search field, swipe-to-delete | only current, many, permission denied |
| S03 | Location search | search on S02 | find a city | search input, result list, Add button | empty, results, no results |
| S04 | Day detail | tap a day capsule in 10-day | one day: range, hourly, cards | capsule picker (selected), day-range, hourly strip, detail cards | filled (ref-01) |
| S15 | Location preview | tap a search result | full forecast before saving | hero, cards, Save button | unsaved (ref-02) |
| S05 | Card detail | tap a card (guess) | expanded metric (e.g. AQI, pollen, UV) | chart, explanation text | filled (guess) |
| S06 | Weather map | map card on S01 | 6h precipitation nowcast | map, precip overlay, timeline scrubber, play | filled, region unavailable |
| S07 | Profile menu | avatar top-right | entry to settings, theme | menu list | |
| S08 | Settings | S07 -> Weather settings | units, vibrations, AI brief, notifications | radio lists, toggles | |
| S09 | Units | S08 -> Temperature / Weather units | change units | radio groups | |
| S10 | Rearrange mode | long-press a card on S01 | reorder cards | drag handles, drop placeholder | dragging |
| S11 | Alert detail | tap alert banner | full warning text, agency, validity | text, source link | |
| S12 | Homescreen widget | launcher | glance view | temp, icon, high/low, location | sizes |
| S13 | Notifications | system | rain soon, tomorrow forecast, severe | notification | |
| S14 | Onboarding / permissions | first launch (guess) | ask location + notifications | permission prompts | granted, denied |
## Flows
```
F01 Check weather at current location
S14 (first time) -> S01
happy path clicks: 0
edge: location denied, GPS off, offline with cached data, stale data
F02 Add a city
S01 -> S02 -> S03 -> S15 preview -> Save -> S01 (new city)
happy path clicks: 4 (open list, tap search, type, tap result/Add)
edge: ambiguous names, postal code, no results, duplicate city
F03 Delete a city
S02 swipe left
happy path clicks: 1 swipe
edge: undo, deleting the current-location row (not allowed)
F04 Switch between cities
S01 -> S02 -> tap city (or swipe pager on S01, guess)
happy path clicks: 2
F05 Watch rain coming in
S01 -> map card -> S06 -> scrub / play
happy path clicks: 2
edge: region without nowcast, tiles fail to load
F06 Change units
S01 -> S07 -> S08 -> S09 -> pick
happy path clicks: 4
F07 Reorder cards
S01 long-press -> S10 drag -> drop
edge: cannot move above brief/hourly, layout per city
F08 React to a severe warning
S13 notification -> S01 alert banner -> S11
happy path clicks: 2
edge: multiple alerts, expired alert, AI brief hidden while alert active
F09 Enable rain notifications
S08 -> notifications toggle -> per-city toggle
edge: notification permission denied, battery optimisation kills worker
```
## Components
| component | variants | states | used on |
| --- | --- | --- | --- |
| Hero block | city name, big temp, condition, feels-like, H/L | loading, stale | S01 |
| Alert banner | minor, moderate, severe, extreme | collapsed, expanded | S01, S11 |
| Brief card | text summary | loading, hidden (alert) | S01 |
| Hourly strip | temp + icon + precip % per hour | scroll | S01, S04 |
| Day row | icon, precip %, low-high bar | pressed | S01 |
| Metric card | small square, wide | default, dragging, pressed | S01, S10 |
| Gauge / dial | UV, AQI, pressure, wind compass | | metric cards |
| Sun arc | sunrise/sunset arc | day, night | sunrise card |
| City row | current location, saved | swiping, pressed | S02 |
| Search field | | empty, typing, results | S03 |
| Map + scrubber | | playing, paused, loading tiles | S06 |
| Settings rows | radio, toggle, link | disabled | S08, S09 |
| Animated background | per condition, day/night | reduced motion | S01 |
| Top app bar | with avatar/menu | | S01, S02 |
| Widget | small, medium | | S12 |
## Inferred data model
```
Location id, name, admin_area, country, lat, lon, timezone, is_current, sort_order
evidence: S02, S03, help "Add a location" confidence: high
CardLayout location_id, card_type, position
evidence: 9to5google "customisations unique per city" confidence: high
Forecast location_id, fetched_at, current{temp, feels_like, code, humidity, wind_speed,
wind_dir, gusts, pressure, visibility, uv, dew_point, is_day},
hourly[], daily[]{date, code, t_min, t_max, precip_prob, precip_sum, sunrise, sunset, uv_max}
evidence: S01 cards confidence: high (cache, not user data)
AirQuality location_id, aqi, pm2_5, pm10, o3, no2, pollen{grass, birch, ...}
evidence: AQI/pollen cards confidence: high
Alert id, location_id, event, severity, headline, description, agency, onset, expires
evidence: help + alert banner, At a Glance two-line alert confidence: medium
Settings temp_unit, precip_unit, wind_unit, pressure_unit, visibility_unit, theme,
vibrations, brief_enabled, notif_tomorrow, notif_severe
evidence: S08, S09 confidence: high
NotificationPref location_id, rain_alerts confidence: medium
```
Relationships: Location 1-n CardLayout, Location 1-1 Forecast (cached), Location 1-1 AirQuality, Location 1-n Alert, Location 1-1 NotificationPref. No user accounts needed (local only).
## Feature matrix
See `features.csv`. Must: 18, should: 14, could: 7, skip: 4 (design + widgets raised to must).
## Out of scope (cannot or should not be cloned)
- Google's own forecast and nowcasting models (MetNet / NowCast): proprietary. Replace with Open-Meteo (forecast, AQI, pollen, geocoding), DWD / MeteoAlarm (alerts), RainViewer or DWD radar (map).
- Gemini-based Weather Brief and personal insights from Calendar/Gmail: Google AI and Google data. A rule- or template-based summary is the replacement, or optionally a local LLM.
- Location sync via Google account: deliberately left out.
- At a Glance / Pixel launcher integration: a system feature.
- Google's illustrations, icons, animations and the "Froggy" art: original assets have to be made from scratch.
## Size
Screens 15, flows 9, entities 7. Hard parts: (1) the precipitation map with timeline (tile sources, licences, animation), (2) reliable background notifications/alerts without Play Services (WorkManager, Doze), (3) polished animated backgrounds + homescreen widget. Size: M (a few weeks) for a native Android app (Kotlin/Compose).
+75
View File
@@ -0,0 +1,75 @@
-- Local SQLite schema (Room). No server, no accounts: all data is on the device.
-- Times are INTEGER epoch milliseconds UTC; local display uses Location.timezone.
CREATE TABLE locations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
admin_area TEXT,
country_code TEXT NOT NULL,
lat REAL NOT NULL CHECK (lat BETWEEN -90 AND 90),
lon REAL NOT NULL CHECK (lon BETWEEN -180 AND 180),
timezone TEXT NOT NULL, -- IANA, from geocoding
is_current INTEGER NOT NULL DEFAULT 0 CHECK (is_current IN (0,1)),
sort_order INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE UNIQUE INDEX locations_one_current ON locations(is_current) WHERE is_current = 1;
CREATE UNIQUE INDEX locations_no_dupe ON locations(round(lat,2), round(lon,2)) WHERE is_current = 0;
CREATE INDEX locations_sort ON locations(sort_order);
CREATE TABLE card_layout (
location_id INTEGER NOT NULL REFERENCES locations(id) ON DELETE CASCADE,
card_type TEXT NOT NULL CHECK (card_type IN
('precip','wind','uv','humidity','pressure','visibility','sun','aqi','pollen','map')),
position INTEGER NOT NULL,
hidden INTEGER NOT NULL DEFAULT 0 CHECK (hidden IN (0,1)),
PRIMARY KEY (location_id, card_type)
);
CREATE INDEX card_layout_order ON card_layout(location_id, position);
-- Forecast cache: raw provider JSON + parsed columns the UI and widgets read fast.
CREATE TABLE forecast_cache (
location_id INTEGER PRIMARY KEY REFERENCES locations(id) ON DELETE CASCADE,
fetched_at INTEGER NOT NULL,
current_json TEXT NOT NULL,
hourly_json TEXT NOT NULL, -- 48h
daily_json TEXT NOT NULL, -- 10 days
air_json TEXT, -- AQI + pollen
source TEXT NOT NULL DEFAULT 'open-meteo'
);
CREATE INDEX forecast_fetched ON forecast_cache(fetched_at);
CREATE TABLE alerts (
id TEXT PRIMARY KEY, -- provider identifier, dedupe key
location_id INTEGER NOT NULL REFERENCES locations(id) ON DELETE CASCADE,
event TEXT NOT NULL,
severity TEXT NOT NULL CHECK (severity IN ('minor','moderate','severe','extreme')),
headline TEXT NOT NULL,
description TEXT,
instruction TEXT,
agency TEXT NOT NULL,
onset INTEGER NOT NULL,
expires INTEGER NOT NULL CHECK (expires > onset),
notified INTEGER NOT NULL DEFAULT 0 CHECK (notified IN (0,1))
);
CREATE INDEX alerts_loc_active ON alerts(location_id, expires);
CREATE TABLE widget_config (
app_widget_id INTEGER PRIMARY KEY, -- Android appWidgetId
location_id INTEGER REFERENCES locations(id) ON DELETE SET NULL, -- NULL = follow current location
style TEXT NOT NULL DEFAULT 'auto' CHECK (style IN ('auto','dynamic_color','condition','transparent')),
content TEXT NOT NULL DEFAULT 'auto' CHECK (content IN ('auto','now','hourly','daily'))
);
CREATE INDEX widget_loc ON widget_config(location_id);
CREATE TABLE notification_pref (
location_id INTEGER PRIMARY KEY REFERENCES locations(id) ON DELETE CASCADE,
rain_alerts INTEGER NOT NULL DEFAULT 0,
severe INTEGER NOT NULL DEFAULT 1
);
-- Settings live in Jetpack DataStore (typed key-value), not here:
-- temp_unit, precip_unit, wind_unit, pressure_unit, visibility_unit,
-- theme (system|light|dark), dynamic_color, animations, vibrations,
-- brief_enabled, notif_tomorrow_time, data_source.
Binary file not shown.

After

Width:  |  Height:  |  Size: 499 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 866 KiB