Read this plus CLAUDE.md before continuing. It records what the app is made
of and which rules exist, not a change log — git log has that.
Every one of these exists because the same thing had been hand-built three times with three different results. Adding a fourth variant is the bug.
| Widget | What it is |
|---|---|
lib/shared/widgets/app_scaffold.dart |
THE page frame. AppScaffold gives the title row, an optional headerBottom, the body and a lifted FAB. AppPageHeader/AppHeaderAction are its parts. No page builds its own Scaffold+AppBar. |
lib/shared/widgets/app_drawer_panel.dart |
THE side panel. AppDrawerPanel + AppDrawerSection + AppDrawerTile + AppDrawerActionTile. Every section's drawer is built from these. |
lib/shared/widgets/entity_edit_sheet.dart |
THE form for anything with a name, a colour and maybe an icon: project, calendar, ICS feed, note folder, habit. EntityFlushGrid lays out swatches and icons so a full row ends flush right. |
lib/shared/widgets/picker_sheet.dart |
THE modals. showAppPicker (card that flies in), showPickerSheet (option list), showDateTimeSheet. No bare AlertDialog, no bare showModalBottomSheet. |
lib/shared/widgets/app_field.dart |
THE input. Filled block, never an outline. |
lib/shared/widgets/app_check.dart |
THE tick. Task rows, habit days, calendar visibility. |
lib/shared/widgets/connected_group.dart |
THE segmented control. The SELECTED segment is strongly rounded on both sides; its neighbours stay nearly square. |
lib/shared/widgets/rounded_group.dart |
A list section as one group. |
lib/core/theme/app_shapes.dart |
Radii, gaps, and AppShapes.contentBottom(context). |
- No
Border.all, noBorderSide, noDivideras a separator. Separation is a filled block plus the 3px gap. The one sanctioned exception is the selection ring around a colour swatch or an icon tile. - Colours only from
AppColors, radii only fromAppShapes. AppShapes.contentBottom(context)is the bottom padding of every scrollable. It reads the gesture inset from the view, not fromMediaQuery: inside a page theScaffoldhas already eaten that inset andMediaQuery.viewPaddingOfanswers 0, which silently made every padding one gesture bar too short.- A page paints no background. The shell's background is what the floating nav bar blurs; a page that paints its own turns the glass into a grey slab.
- The shell (
features/todos/presentation/pages/home_page.dart) picks the side panel of the current tab. Settings has none, so its page shows no hamburger.
- Tasks — list, quick-add dock, projects. Swiping a row left crosses
release-driven zones (Delete, Pin, Date, Move) with a haptic tick per zone;
the action runs on release, a full swipe opens the menu instead of deleting.
swipe_zone_row.dartowns that;flutter_slidableis gone from the tile (the dependency and the leftoverSlidableAutoCloseBehaviorwrappers can be removed). - Calendar — day, three days, week, month, agenda. Periods are pages
(
period_pager.dart) with a guard between page index and focused date.DayWindow.coveringwidens the drawn day window until every item fits, so a 06:00 item shows under an 08:00 setting. Blocks are placed by the minute; only creating and dragging snap to a quarter hour. Two fingers zoom the hour height (28–140, persisted); in the month view they scale the week row height (72–220, or "fit to screen"). Both go throughPinchScaler, which reads raw pointers — a scale recognizer would steal the page swipe. The month strip is a connected bar, years are quiet markers. - Notes — folders, markdown. The editor is one writing surface with autosave (600ms, on pop, on background) and a format bar over the keyboard; preview checkboxes write back into the source.
- Habits — daily/weekly filter from its panel, ticks from
AppCheck. - Settings — a hub with a sub-page per section (tasks, calendar, notes, habits). Everything is stored in the Hive settings box; the calendar and the notes read their settings.
Two bugs in this session were invisible in pictures and cost hours:
- A
GestureDetectorinside a horizontal list loses the gesture arena to the scroll view as soon as the finger travels a pixel, so the month strip looked right and never reacted to a real finger. It reads the raw pointer now (_TapTolerantinmonth_strip.dart). - A
ScaleGestureRecognizerenters the arena with a single pointer and would steal the page swipe, so the pinch zoom listens to raw pointers too and only starts on the second finger.
Test interaction with widget tests, not screenshots. Screenshots are for
layout, and only from the device (adb exec-out screencap -p > shot.png).
_scratch/walkthrough.sh walks every screen and drops a picture per step; it
restarts the app between sections because one stuck sheet used to swallow all
following taps and the rest of the run photographed the wrong screen.
Four separate widgets, each its own provider, layout, appwidget-provider
XML and picker entry: Tasks, Calendar, Notes, Habits. The code
lives in android/app/src/main/kotlin/doo/chuk/dev/widgets/; every provider
extends ChukdooWidgetProvider (header, list, empty state) and every list is
drawn by one WidgetListFactory parameterised by WidgetKind.
- Data goes out through SharedPreferences, not through a channel. A widget
is redrawn by the launcher at times when no Flutter engine exists — after a
reboot, for instance — so
lib/features/widget/widget_service.dartwrites one JSON blob per widget intoFlutterSharedPreferencesand the widgets read it there.WidgetService.startWatching()(called once inmain.dart) watches the Hive boxes, so a sync pull refreshes the widgets exactly like a swipe does. Do not add per-notifierupdateWidget()calls; the box watcher already sees every write. - Ticks come back as a pending queue. A widget cannot write to Hive. A tick
writes
widget_pending_tasks/widget_pending_habitsand updates the stored blob optimistically, so the row looks right at once;processPending()applies it for real on the next start or resume (AutoSyncManager). Nothing is lost, but nothing reaches Supabase until the app is opened again. - Every tap goes through
WidgetActionActivity. A row has to be able to tick and to open, and a collection carries exactly one PendingIntent template — so the template is an activity PendingIntent and the branch happens in that invisible activity. It must not become a broadcast: a receiver may not start an activity from the background, which is what a widget tap is. - Previews are real. Each widget ships
previewLayout(a static twin of the layout with sample rows) for API 31+ and a hand-built vectorpreviewImageas the fallback. Never point one at@mipmap/ic_launcheragain. - The old single
ChukdooWidgetis gone. Widgets placed by an earlier build disappear from the home screen and have to be added again.
Run supabase/apply_pending.sql in the SQL editor (idempotent). It creates
calendars, calendar_events, habits and note_folders; notes already
exists. supabase/dump_schema.sql prints the whole schema in one cell.
todos and projects are blob-only (id, user_id, encrypted_payload, updated_at) — do not add columns, the rest lives in the encrypted payload.
That is also where a task's end_time and a note's folderId ride.
Synced: todos, projects, calendars, calendar events, habits, notes, note
folders. Subscribed ICS feeds stay on the device (user_id starts with
feed:), and so does their visibility.
material_design_icons_flutter does not compile on Flutter 3.47 (IconData is
final now) and upstream is dead — a patched copy lives in third_party/ and is
wired through dependency_overrides.
source .env.local
memguard-allow 8G flutter build apk --target-platform android-arm64 \
--no-tree-shake-icons \
--dart-define=SUPABASE_URL=$SUPABASE_URL \
--dart-define=SUPABASE_ANON_KEY=$SUPABASE_ANON_KEY \
--dart-define=REVENUECAT_API_KEY=$REVENUECAT_API_KEY
adb install -r build/app/outputs/flutter-apk/app-release.apkKill the Gradle daemon when a build is done; it holds ~2GB and this machine runs out.
- The quick-add dock cannot set a task's end time yet; only the detail page can.
- No "all day" switch in the event card — that still needs the full editor.
- Calendar metrics were set from Google Calendar's published sizing, not measured: Google Calendar lives on a second Android user on the test phone and the shell cannot reach it.
auto_sync_managerrefreshes every provider after a pull now, but a pull still never runs while the app is in the foreground without connectivity changes.