Compare commits
21 Commits
2a4b14cb43
...
v1.1.4
| Author | SHA1 | Date | |
|---|---|---|---|
| 92de2bd7de | |||
| bca7e391ad | |||
| 3902755f61 | |||
| 8d635970d2 | |||
| 51dba090d6 | |||
| fc5a612b81 | |||
| fa778a238a | |||
| d220dbe5ce | |||
| edce11dd78 | |||
| 0ea79e0853 | |||
| 772034cba1 | |||
| a3e4d0224b | |||
| e5eccb74e5 | |||
| 9398193c1e | |||
| 3697e4efc4 | |||
| 13c7d623ba | |||
| a9f298350e | |||
| a44f2b80b5 | |||
| 27f18d4f39 | |||
| a94d41b7f7 | |||
| 99358ed704 |
102
.gitea/workflows/ci.yaml
Normal file
@@ -0,0 +1,102 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
tags-ignore:
|
||||
- '**'
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: docker
|
||||
env:
|
||||
ANDROID_HOME: /opt/android-sdk
|
||||
ANDROID_SDK_ROOT: /opt/android-sdk
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
|
||||
- name: Install Android SDK packages
|
||||
run: |
|
||||
sdkmanager --licenses >/dev/null <<'EOF'
|
||||
y
|
||||
y
|
||||
y
|
||||
y
|
||||
y
|
||||
y
|
||||
y
|
||||
y
|
||||
y
|
||||
y
|
||||
EOF
|
||||
sdkmanager "platform-tools" "platforms;android-36" "build-tools;36.0.0"
|
||||
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
|
||||
- name: Trust Flutter SDK git directory
|
||||
run: |
|
||||
set -e
|
||||
FLUTTER_BIN_DIR="$(dirname "$(command -v flutter)")"
|
||||
FLUTTER_SDK_DIR="$(cd "$FLUTTER_BIN_DIR/.." && pwd -P)"
|
||||
git config --global --add safe.directory "$FLUTTER_SDK_DIR"
|
||||
if [ -n "${FLUTTER_ROOT:-}" ]; then
|
||||
git config --global --add safe.directory "$FLUTTER_ROOT"
|
||||
fi
|
||||
git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.41.4-x64 || true
|
||||
|
||||
- name: Verify Android + Flutter toolchain
|
||||
run: flutter doctor -v
|
||||
|
||||
- name: Install dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Static analysis
|
||||
run: flutter analyze --no-pub
|
||||
|
||||
- name: Run tests
|
||||
run: flutter test
|
||||
|
||||
- name: Check outdated dependencies
|
||||
run: dart pub outdated
|
||||
continue-on-error: true
|
||||
|
||||
- name: Security audit
|
||||
run: dart pub audit
|
||||
|
||||
- name: Trivy filesystem scan
|
||||
run: |
|
||||
set -e
|
||||
SUDO=""
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
SUDO="sudo"
|
||||
fi
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
$SUDO apt-get update
|
||||
$SUDO apt-get install -y wget apt-transport-https gnupg lsb-release
|
||||
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | gpg --dearmor | $SUDO tee /usr/share/keyrings/trivy.gpg > /dev/null
|
||||
echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb generic main" | $SUDO tee /etc/apt/sources.list.d/trivy.list
|
||||
$SUDO apt-get update
|
||||
$SUDO apt-get install -y trivy
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
$SUDO apk add --no-cache trivy || (wget -qO trivy.tar.gz https://github.com/aquasecurity/trivy/releases/latest/download/trivy_0.62.1_Linux-64bit.tar.gz && tar xzf trivy.tar.gz trivy && $SUDO mv trivy /usr/local/bin/)
|
||||
fi
|
||||
trivy filesystem --severity HIGH,CRITICAL --exit-code 0 .
|
||||
continue-on-error: true
|
||||
|
||||
- name: Build debug APK
|
||||
run: flutter build apk --debug
|
||||
@@ -7,7 +7,100 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: docker
|
||||
env:
|
||||
ANDROID_HOME: /opt/android-sdk
|
||||
ANDROID_SDK_ROOT: /opt/android-sdk
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
|
||||
- name: Install Android SDK packages
|
||||
run: |
|
||||
sdkmanager --licenses >/dev/null <<'EOF'
|
||||
y
|
||||
y
|
||||
y
|
||||
y
|
||||
y
|
||||
y
|
||||
y
|
||||
y
|
||||
y
|
||||
y
|
||||
EOF
|
||||
sdkmanager "platform-tools" "platforms;android-36" "build-tools;36.0.0"
|
||||
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
|
||||
- name: Trust Flutter SDK git directory
|
||||
run: |
|
||||
set -e
|
||||
FLUTTER_BIN_DIR="$(dirname "$(command -v flutter)")"
|
||||
FLUTTER_SDK_DIR="$(cd "$FLUTTER_BIN_DIR/.." && pwd -P)"
|
||||
git config --global --add safe.directory "$FLUTTER_SDK_DIR"
|
||||
if [ -n "${FLUTTER_ROOT:-}" ]; then
|
||||
git config --global --add safe.directory "$FLUTTER_ROOT"
|
||||
fi
|
||||
git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.41.4-x64 || true
|
||||
|
||||
- name: Verify Android + Flutter toolchain
|
||||
run: flutter doctor -v
|
||||
|
||||
- name: Install dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Static analysis
|
||||
run: flutter analyze --no-pub
|
||||
|
||||
- name: Run tests
|
||||
run: flutter test
|
||||
|
||||
- name: Check outdated dependencies
|
||||
run: dart pub outdated
|
||||
continue-on-error: true
|
||||
|
||||
- name: Security audit
|
||||
run: dart pub audit
|
||||
|
||||
- name: Trivy filesystem scan
|
||||
run: |
|
||||
set -e
|
||||
SUDO=""
|
||||
if command -v sudo >/dev/null 2>&1; then
|
||||
SUDO="sudo"
|
||||
fi
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
$SUDO apt-get update
|
||||
$SUDO apt-get install -y wget apt-transport-https gnupg lsb-release
|
||||
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | gpg --dearmor | $SUDO tee /usr/share/keyrings/trivy.gpg > /dev/null
|
||||
echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb generic main" | $SUDO tee /etc/apt/sources.list.d/trivy.list
|
||||
$SUDO apt-get update
|
||||
$SUDO apt-get install -y trivy
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
$SUDO apk add --no-cache trivy || (wget -qO trivy.tar.gz https://github.com/aquasecurity/trivy/releases/latest/download/trivy_0.62.1_Linux-64bit.tar.gz && tar xzf trivy.tar.gz trivy && $SUDO mv trivy /usr/local/bin/)
|
||||
fi
|
||||
trivy filesystem --severity HIGH,CRITICAL --exit-code 0 .
|
||||
continue-on-error: true
|
||||
|
||||
- name: Build debug APK
|
||||
run: flutter build apk --debug
|
||||
|
||||
build-and-deploy:
|
||||
needs: ci
|
||||
runs-on: docker
|
||||
env:
|
||||
ANDROID_HOME: /opt/android-sdk
|
||||
@@ -86,6 +179,30 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Set version from git tag
|
||||
run: |
|
||||
set -e
|
||||
RAW_TAG="${GITHUB_REF_NAME:-${GITHUB_REF##*/}}"
|
||||
|
||||
# Strip leading 'v' if present (v1.2.3 -> 1.2.3)
|
||||
VERSION="${RAW_TAG#v}"
|
||||
|
||||
# Extract a numeric build number for versionCode.
|
||||
# Converts semver x.y.z into a single integer: x*10000 + y*100 + z
|
||||
# e.g. 1.1.1 -> 10101, 2.3.15 -> 20315
|
||||
MAJOR=$(echo "$VERSION" | cut -d. -f1)
|
||||
MINOR=$(echo "$VERSION" | cut -d. -f2)
|
||||
PATCH=$(echo "$VERSION" | cut -d. -f3)
|
||||
MAJOR=${MAJOR:-0}; MINOR=${MINOR:-0}; PATCH=${PATCH:-0}
|
||||
VERSION_CODE=$(( MAJOR * 10000 + MINOR * 100 + PATCH ))
|
||||
|
||||
echo "Version: $VERSION, VersionCode: $VERSION_CODE"
|
||||
|
||||
# Update pubspec.yaml: replace the version line
|
||||
sed -i "s/^version: .*/version: ${VERSION}+${VERSION_CODE}/" pubspec.yaml
|
||||
|
||||
grep '^version:' pubspec.yaml
|
||||
|
||||
# ADD THIS NEW STEP
|
||||
- name: Setup Android Keystore
|
||||
env:
|
||||
@@ -168,6 +285,10 @@ jobs:
|
||||
|
||||
cp build/app/outputs/flutter-apk/app-release.apk "fdroid/repo/my_flutter_app_${SAFE_REF_NAME}.apk"
|
||||
|
||||
- name: Copy metadata to F-Droid repo
|
||||
run: |
|
||||
cp -r fdroid-metadata/* fdroid/metadata/
|
||||
|
||||
- name: Generate F-Droid Index
|
||||
run: |
|
||||
cd fdroid
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
# Milestones
|
||||
|
||||
## v1.1 Calendar & Polish (Shipped: 2026-03-16)
|
||||
|
||||
**Phases completed:** 3 phases, 5 plans, 11 tasks
|
||||
**Codebase:** 13,031 LOC Dart (9,051 lib + 3,980 test), 108 tests, 41 commits
|
||||
**Timeline:** 2 days (2026-03-15 to 2026-03-16)
|
||||
|
||||
**Key accomplishments:**
|
||||
1. Horizontal 181-day calendar strip with German day cards, month boundaries, and floating Today button — replaces the stacked daily-plan HomeScreen
|
||||
2. Date-parameterized CalendarDao with reactive Drift streams for day tasks and overdue tasks
|
||||
3. Task completion history bottom sheet with per-task reverse-chronological log
|
||||
4. Alphabetical, interval, and effort sort options persisted via SharedPreferences
|
||||
5. SortDropdown widget integrated in both HomeScreen and TaskListScreen AppBars
|
||||
|
||||
**Archive:** See `milestones/v1.1-ROADMAP.md` and `milestones/v1.1-REQUIREMENTS.md`
|
||||
|
||||
---
|
||||
|
||||
## v1.0 MVP (Shipped: 2026-03-16)
|
||||
|
||||
**Phases completed:** 4 phases, 13 plans
|
||||
|
||||
@@ -2,24 +2,12 @@
|
||||
|
||||
## What This Is
|
||||
|
||||
A local-first Flutter app for organizing household chores, built for personal/couple use on Android. Uses a room-based task scheduling model where users create rooms, add recurring tasks with frequency intervals, and the app auto-calculates the next due date after each completion. Features a daily plan home screen, bundled German-language task templates, room cleanliness indicators, and daily summary notifications. Fully offline, free, privacy-respecting — all data stays on-device.
|
||||
A local-first Flutter app for organizing household chores, built for personal/couple use on Android. Uses a room-based task scheduling model where users create rooms, add recurring tasks with frequency intervals, and the app auto-calculates the next due date after each completion. Features a horizontal calendar strip home screen with day-by-day task navigation, task completion history, configurable sorting (alphabetical, interval, effort), bundled German-language task templates, room cleanliness indicators, and daily summary notifications. Fully offline, free, privacy-respecting — all data stays on-device.
|
||||
|
||||
## Core Value
|
||||
|
||||
Users can see what needs doing today, mark it done, and trust the app to schedule the next occurrence — without thinking about it.
|
||||
|
||||
## Current Milestone: v1.1 Calendar & Polish
|
||||
|
||||
**Goal:** Replace the stacked daily plan with a horizontal calendar strip UI, add task completion history, and task sorting options.
|
||||
|
||||
**Target features:**
|
||||
- Horizontal date-strip calendar with day abbreviation + date number cards
|
||||
- Month color shift for visual boundary between months
|
||||
- Day-selection shows tasks in a list below the strip
|
||||
- Undone tasks carry over to the next day with color accent (overdue marker)
|
||||
- Task completion history log
|
||||
- Additional task sorting (alphabetical, interval, effort)
|
||||
|
||||
## Requirements
|
||||
|
||||
### Validated
|
||||
@@ -32,16 +20,16 @@ Users can see what needs doing today, mark it done, and trust the app to schedul
|
||||
- Daily summary notification with configurable time — v1.0
|
||||
- Light/dark theme with calm Material 3 palette — v1.0
|
||||
- Cleanliness indicator per room (based on overdue vs on-time) — v1.0
|
||||
- Horizontal calendar strip home screen replacing stacked daily plan — v1.1
|
||||
- Overdue task carry-over with red/orange visual accent — v1.1
|
||||
- Task completion history with per-task reverse-chronological log — v1.1
|
||||
- Alphabetical, interval, and effort task sorting with persistence — v1.1
|
||||
|
||||
### Active
|
||||
|
||||
- [ ] Horizontal calendar strip home screen (replacing stacked daily plan)
|
||||
- [ ] Overdue task carry-over with visual accent
|
||||
- [ ] Task completion history log
|
||||
- [ ] Additional task sorting (alphabetical, interval, effort)
|
||||
- [ ] Data export/import (JSON) — deferred
|
||||
- [ ] English localization — deferred
|
||||
- [ ] Room cover photos from camera or gallery — deferred
|
||||
- [ ] Data export/import (JSON)
|
||||
- [ ] English localization
|
||||
- [ ] Room cover photos from camera or gallery
|
||||
|
||||
### Out of Scope
|
||||
|
||||
@@ -55,18 +43,22 @@ Users can see what needs doing today, mark it done, and trust the app to schedul
|
||||
- Firebase or any Google cloud services — contradicts local-first design
|
||||
- Real-time cross-device sync — potential future self-hosted feature
|
||||
- Tablet-optimized layout — future enhancement
|
||||
- Weekly/monthly calendar views — date strip is sufficient for task app
|
||||
- Drag tasks between days — tasks auto-schedule based on frequency
|
||||
- Calendar sync (Google/Apple) — contradicts local-first, offline-only design
|
||||
- Statistics & insights dashboard — v2.0
|
||||
- Onboarding wizard — v2.0
|
||||
- Custom accent color picker — v2.0
|
||||
|
||||
## Context
|
||||
|
||||
- Shipped v1.0 MVP with 10,588 LOC Dart (7,773 lib + 2,815 test), 89 tests
|
||||
- Tech stack: Flutter + Dart, Riverpod 3 + code generation, Drift 2.31 SQLite, GoRouter, flutter_local_notifications
|
||||
- Shipped v1.1 with 13,031 LOC Dart (9,051 lib + 3,980 test), 108 tests
|
||||
- Tech stack: Flutter + Dart, Riverpod 3 + code generation, Drift 2.31 SQLite, GoRouter, flutter_local_notifications, SharedPreferences
|
||||
- Inspired by BeTidy (iOS/Android household cleaning app) — room-based model, no cloud/social
|
||||
- Built for personal use with partner on a shared Android device; may publish publicly later
|
||||
- Code and comments in English; UI strings German-only for v1.0
|
||||
- Code and comments in English; UI strings German-only through v1.1
|
||||
- Gitea (self-hosted on Hetzner) for version control; no CI/CD pipeline yet
|
||||
- Dead code from v1.0: daily_plan_providers.dart, daily_plan_task_row.dart, progress_card.dart (DailyPlanDao still used by notification service)
|
||||
|
||||
## Constraints
|
||||
|
||||
@@ -74,7 +66,7 @@ Users can see what needs doing today, mark it done, and trust the app to schedul
|
||||
- **Platform**: Android-first (iOS later)
|
||||
- **Offline**: 100% offline-capable, zero network dependencies
|
||||
- **Privacy**: No data leaves the device, no analytics, no tracking
|
||||
- **Language**: German-only UI for v1.0, English code/comments
|
||||
- **Language**: German-only UI through v1.1, English code/comments
|
||||
- **No CI**: No automated build pipeline initially
|
||||
|
||||
## Key Decisions
|
||||
@@ -91,6 +83,11 @@ Users can see what needs doing today, mark it done, and trust the app to schedul
|
||||
| Calendar-anchored scheduling | Monthly/quarterly/yearly tasks anchor to original day-of-month with clamping | Good — handles Feb 28/31 edge cases correctly with anchor memory |
|
||||
| flutter_local_notifications v21 | Standard Flutter notification package, TZ-aware scheduling | Good — inexactAllowWhileIdle avoids SCHEDULE_EXACT_ALARM complexity |
|
||||
| Manual StreamProvider for drift types | riverpod_generator throws InvalidTypeException with drift Task type | Revisit — may be fixed in future riverpod_generator versions |
|
||||
| Calendar strip replaces daily plan | v1.1 goal — stacked overdue/today/upcoming sections replaced by horizontal 181-day strip | Good — cleaner navigation, day-by-day browsing |
|
||||
| NotifierProvider over StateProvider | Riverpod 3.x removed StateProvider | Good — minimal Notifier subclass works cleanly |
|
||||
| In-memory sort over SQL ORDER BY | Sort preference changes without re-querying DB | Good — stream.map applies sort after DB emit, reactive to preference changes |
|
||||
| SharedPreferences for sort | Simple enum.name string persistence for sort preference | Good — lightweight, no DB migration needed, survives app restart |
|
||||
| PopupMenuButton for sort UI | Material 3 AppBar action pattern, overlay menu | Good — clean integration in both HomeScreen and TaskListScreen AppBars |
|
||||
|
||||
---
|
||||
*Last updated: 2026-03-16 after v1.1 milestone started*
|
||||
*Last updated: 2026-03-16 after v1.1 milestone completed*
|
||||
|
||||
@@ -46,6 +46,50 @@
|
||||
|
||||
---
|
||||
|
||||
## Milestone: v1.1 — Calendar & Polish
|
||||
|
||||
**Shipped:** 2026-03-16
|
||||
**Phases:** 3 | **Plans:** 5
|
||||
|
||||
### What Was Built
|
||||
- Horizontal 181-day calendar strip replacing the stacked daily plan HomeScreen
|
||||
- CalendarDao with date-parameterized reactive Drift streams for day tasks and overdue tasks
|
||||
- Task completion history bottom sheet with per-task reverse-chronological log
|
||||
- Alphabetical, interval, and effort sort options with SharedPreferences persistence
|
||||
- SortDropdown widget in both HomeScreen and TaskListScreen AppBars
|
||||
|
||||
### What Worked
|
||||
- Phase dependency ordering (5 → 6+7 parallel-capable) meant calendar strip was stable before building features on top
|
||||
- TDD red-green cycle continued smoothly — every plan had failing tests before implementation
|
||||
- Auto-advance mode enabled rapid phase chaining with minimal manual intervention
|
||||
- Existing patterns from v1.0 (DAO, provider, widget test) were reused directly — no new patterns invented unnecessarily
|
||||
- CalendarStripController (VoidCallback holder) was simpler than GlobalKey approach — good architecture call
|
||||
|
||||
### What Was Inefficient
|
||||
- StateProvider removal in Riverpod 3.x was discovered during execution rather than research — same category of issue as v1.0's riverpod_generator problem
|
||||
- ROADMAP.md plan checkboxes still not auto-checked by executor (same bookkeeping gap as v1.0)
|
||||
- Phase 5 plan split (data layer + UI) could have been a single plan given the small scope — overhead of 2 separate plans wasn't justified for ~13 min total
|
||||
|
||||
### Patterns Established
|
||||
- CalendarStripController: VoidCallback holder for parent-to-child imperative scroll communication
|
||||
- CalendarDayList state machine: first-run → celebration → emptyDay → hasTasks (5 states)
|
||||
- In-memory sort via stream.map after DB stream emit — sort preference changes without re-querying
|
||||
- SortPreferenceNotifier: sync default + async _loadPersisted() — matches ThemeNotifier pattern
|
||||
- Nested Scaffold pattern for per-tab AppBars in StatefulShellRoute.indexedStack
|
||||
|
||||
### Key Lessons
|
||||
1. Riverpod API surface changes (StateProvider removal) should be caught during phase research, not during execution — pattern repeats from v1.0
|
||||
2. Plans under ~5 min execution can be merged into a single plan to reduce orchestration overhead
|
||||
3. In-memory sort is the right approach when sort criteria don't affect DB queries — avoids re-streaming
|
||||
4. Bottom sheets for one-shot modals (history) don't need dedicated Riverpod providers — ref.read() in ConsumerWidget is sufficient
|
||||
|
||||
### Cost Observations
|
||||
- Model mix: orchestrator on opus, executors/checkers on sonnet
|
||||
- Total execution: ~26 min for 5 plans across 3 phases
|
||||
- Notable: Each plan averaged ~5 min — significantly faster than v1.0's ~6 min average due to established patterns
|
||||
|
||||
---
|
||||
|
||||
## Cross-Milestone Trends
|
||||
|
||||
### Process Evolution
|
||||
@@ -53,13 +97,17 @@
|
||||
| Milestone | Phases | Plans | Key Change |
|
||||
|-----------|--------|-------|------------|
|
||||
| v1.0 | 4 | 13 | Initial project — established all patterns |
|
||||
| v1.1 | 3 | 5 | Reused v1.0 patterns — faster execution, auto-advance mode |
|
||||
|
||||
### Cumulative Quality
|
||||
|
||||
| Milestone | Tests | Key Metric |
|
||||
|-----------|-------|------------|
|
||||
| v1.0 | 89 | dart analyze clean, 0 issues |
|
||||
| Milestone | Tests | LOC (lib) | Key Metric |
|
||||
|-----------|-------|-----------|------------|
|
||||
| v1.0 | 89 | 7,773 | dart analyze clean, 0 issues |
|
||||
| v1.1 | 108 | 9,051 | dart analyze clean, 0 issues |
|
||||
|
||||
### Top Lessons (Verified Across Milestones)
|
||||
|
||||
1. (Single milestone — lessons above will be cross-validated as more milestones ship)
|
||||
1. **Research must verify current package API signatures** — v1.0 hit riverpod_generator type incompatibility, v1.1 hit StateProvider removal. Same root cause: outdated API assumptions in plans.
|
||||
2. **Established patterns compound** — v1.1 plans averaged ~5 min vs v1.0's ~6 min. Reusing DAO, provider, and test patterns eliminated design decisions.
|
||||
3. **Verification gates are cheap insurance** — Consistently ~2 min per phase, caught regressions in both milestones.
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
## Milestones
|
||||
|
||||
- **v1.0 MVP** — Phases 1-4 (shipped 2026-03-16)
|
||||
- **v1.1 Calendar & Polish** — Phases 5-7 (in progress)
|
||||
- ✅ **v1.0 MVP** — Phases 1-4 (shipped 2026-03-16)
|
||||
- ✅ **v1.1 Calendar & Polish** — Phases 5-7 (shipped 2026-03-16)
|
||||
|
||||
## Phases
|
||||
|
||||
<details>
|
||||
<summary>v1.0 MVP (Phases 1-4) — SHIPPED 2026-03-16</summary>
|
||||
<summary>✅ v1.0 MVP (Phases 1-4) — SHIPPED 2026-03-16</summary>
|
||||
|
||||
- [x] Phase 1: Foundation (2/2 plans) — completed 2026-03-15
|
||||
- [x] Phase 2: Rooms and Tasks (5/5 plans) — completed 2026-03-15
|
||||
@@ -19,51 +19,16 @@ See `milestones/v1.0-ROADMAP.md` for full phase details.
|
||||
|
||||
</details>
|
||||
|
||||
**v1.1 Calendar & Polish (Phases 5-7):**
|
||||
<details>
|
||||
<summary>✅ v1.1 Calendar & Polish (Phases 5-7) — SHIPPED 2026-03-16</summary>
|
||||
|
||||
- [x] **Phase 5: Calendar Strip** - Replace the stacked daily plan home screen with a horizontal scrollable date-strip and day-task list (completed 2026-03-16)
|
||||
- [x] **Phase 6: Task History** - Record every task completion with a timestamp and expose a per-task history view (completed 2026-03-16)
|
||||
- [ ] **Phase 7: Task Sorting** - Add alphabetical, interval, and effort sort options to task lists
|
||||
- [x] Phase 5: Calendar Strip (2/2 plans) — completed 2026-03-16
|
||||
- [x] Phase 6: Task History (1/1 plans) — completed 2026-03-16
|
||||
- [x] Phase 7: Task Sorting (2/2 plans) — completed 2026-03-16
|
||||
|
||||
## Phase Details
|
||||
See `milestones/v1.1-ROADMAP.md` for full phase details.
|
||||
|
||||
### Phase 5: Calendar Strip
|
||||
**Goal**: Users navigate their tasks through a horizontal date-strip that replaces the stacked daily plan, seeing today's tasks by default and any day's tasks on tap
|
||||
**Depends on**: Phase 4 (v1.0 shipped — all data layer and scheduling in place)
|
||||
**Requirements**: CAL-01, CAL-02, CAL-03, CAL-04, CAL-05
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. The home screen shows a horizontal scrollable strip of day cards, each displaying the German day abbreviation (Mo, Di, Mi...) and the date number
|
||||
2. Tapping any day card updates the task list below the strip to show that day's tasks, with the selected card visually highlighted
|
||||
3. On app launch the strip auto-scrolls so today's card is centered and selected by default
|
||||
4. When two adjacent day cards span a month boundary, a subtle color shift or divider makes the boundary visible without extra chrome
|
||||
5. Tasks that were not completed on their due date appear in subsequent days' lists with a red/orange accent marking them as overdue
|
||||
**Plans:** 2/2 plans complete
|
||||
Plans:
|
||||
- [ ] 05-01-PLAN.md — Data layer: CalendarDao, CalendarDayState model, Riverpod providers, localization, DAO tests
|
||||
- [ ] 05-02-PLAN.md — UI: CalendarStrip, CalendarDayList, CalendarTaskRow widgets, HomeScreen replacement
|
||||
|
||||
### Phase 6: Task History
|
||||
**Goal**: Users can see exactly when each task was completed in the past, building trust that the scheduling loop is working correctly
|
||||
**Depends on**: Phase 5
|
||||
**Requirements**: HIST-01, HIST-02
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. Every task completion (tap done in any view) is recorded in the database with a precise timestamp — data persists across app restarts
|
||||
2. From a task's detail or context menu the user can open a history view listing all past completion dates for that task in reverse-chronological order
|
||||
3. The history view shows a meaningful empty state if the task has never been completed
|
||||
**Plans:** 1/1 plans complete
|
||||
Plans:
|
||||
- [ ] 06-01-PLAN.md — DAO query + history bottom sheet + TaskFormScreen integration + CalendarTaskRow navigation
|
||||
|
||||
### Phase 7: Task Sorting
|
||||
**Goal**: Users can reorder task lists by the dimension most useful to them — name, how often the task recurs, or how much effort it requires
|
||||
**Depends on**: Phase 5
|
||||
**Requirements**: SORT-01, SORT-02, SORT-03
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. A sort control (dropdown, segmented button, or similar) is visible on task list screens and persists the chosen sort across app restarts
|
||||
2. Selecting alphabetical sort orders tasks A-Z by name within the visible list
|
||||
3. Selecting interval sort orders tasks from most-frequent (daily) to least-frequent (yearly/custom) intervals
|
||||
4. Selecting effort sort orders tasks from lowest effort to highest effort level
|
||||
**Plans**: TBD
|
||||
</details>
|
||||
|
||||
## Progress
|
||||
|
||||
@@ -73,6 +38,6 @@ Plans:
|
||||
| 2. Rooms and Tasks | v1.0 | 5/5 | Complete | 2026-03-15 |
|
||||
| 3. Daily Plan and Cleanliness | v1.0 | 3/3 | Complete | 2026-03-16 |
|
||||
| 4. Notifications | v1.0 | 3/3 | Complete | 2026-03-16 |
|
||||
| 5. Calendar Strip | 2/2 | Complete | 2026-03-16 | - |
|
||||
| 6. Task History | 1/1 | Complete | 2026-03-16 | - |
|
||||
| 7. Task Sorting | v1.1 | 0/? | Not started | - |
|
||||
| 5. Calendar Strip | v1.1 | 2/2 | Complete | 2026-03-16 |
|
||||
| 6. Task History | v1.1 | 1/1 | Complete | 2026-03-16 |
|
||||
| 7. Task Sorting | v1.1 | 2/2 | Complete | 2026-03-16 |
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
---
|
||||
gsd_state_version: 1.0
|
||||
milestone: v1.0
|
||||
milestone_name: milestone
|
||||
milestone: v1.1
|
||||
milestone_name: Calendar & Polish
|
||||
status: completed
|
||||
stopped_at: Completed Phase 6 Plan 01 (task history)
|
||||
last_updated: "2026-03-16T21:01:58.162Z"
|
||||
last_activity: 2026-03-16 — Completed Phase 6 Plan 01 (task completion history)
|
||||
stopped_at: Milestone v1.1 archived
|
||||
last_updated: "2026-03-16T23:26:00.000Z"
|
||||
last_activity: 2026-03-16 — Milestone v1.1 archived
|
||||
progress:
|
||||
total_phases: 3
|
||||
completed_phases: 2
|
||||
total_plans: 3
|
||||
completed_plans: 3
|
||||
completed_phases: 3
|
||||
total_plans: 5
|
||||
completed_plans: 5
|
||||
percent: 100
|
||||
---
|
||||
|
||||
@@ -21,48 +21,32 @@ progress:
|
||||
See: .planning/PROJECT.md (updated 2026-03-16)
|
||||
|
||||
**Core value:** Users can see what needs doing today, mark it done, and trust the app to schedule the next occurrence — without thinking about it.
|
||||
**Current focus:** v1.1 Calendar & Polish — Phase 6: Task History
|
||||
**Current focus:** Planning next milestone
|
||||
|
||||
## Current Position
|
||||
|
||||
Phase: 6 — Task History
|
||||
Plan: 1/1 complete (Phase 6 done)
|
||||
Status: Phase Complete
|
||||
Last activity: 2026-03-16 — Completed Phase 6 Plan 01 (task completion history)
|
||||
Milestone: v1.1 Calendar & Polish — SHIPPED
|
||||
Status: Milestone Complete
|
||||
Last activity: 2026-03-16 — Archived milestone v1.1
|
||||
|
||||
```
|
||||
Progress: [██████████] 100% (1/1 plans in Phase 6)
|
||||
Progress: [██████████] 100% (v1.1 shipped)
|
||||
```
|
||||
|
||||
## Performance Metrics
|
||||
|
||||
| Metric | v1.0 | v1.1 |
|
||||
|--------|------|------|
|
||||
| Phases | 4 | 3 planned |
|
||||
| Plans | 13 | TBD |
|
||||
| LOC (lib) | 7,773 | TBD |
|
||||
| Tests | 89 | TBD |
|
||||
| Phase 05-calendar-strip P01 | 5 | 2 tasks | 10 files |
|
||||
| Phase 05-calendar-strip P02 | 8 | 3 tasks | 9 files |
|
||||
| Phase 06-task-history P01 | 5 | 2 tasks | 9 files |
|
||||
| Phases | 4 | 3 |
|
||||
| Plans | 13 | 5 |
|
||||
| LOC (lib) | 7,773 | 9,051 |
|
||||
| Tests | 89 | 108 |
|
||||
|
||||
## Accumulated Context
|
||||
|
||||
### Decisions
|
||||
|
||||
| Decision | Rationale |
|
||||
|----------|-----------|
|
||||
| Calendar strip replaces daily plan home screen | v1.1 goal per PROJECT.md — not additive, the stacked overdue/today/upcoming sections are removed |
|
||||
| Phase 5 before Phase 6 and 7 | Calendar strip is the primary UI surface; history and sorting operate within or alongside it |
|
||||
| Phase 6 and 7 both depend on Phase 5 only | History and sorting are independent of each other — could execute in either order |
|
||||
| HIST-01 and HIST-02 in same phase | Data layer (HIST-01) is only 1-2 DAO additions; grouping with the UI (HIST-02) keeps the phase coherent |
|
||||
| Used NotifierProvider<SelectedDateNotifier> instead of deprecated StateProvider | Riverpod 3.x removed StateProvider; NotifierProvider is the correct replacement |
|
||||
| calendarDayProvider fetches overdue tasks with .first in asyncMap when isToday | Consistent with dailyPlanProvider pattern; avoids combining two streams |
|
||||
| watchTasksForDate sorts alphabetically by task name | Same-day tasks have no meaningful time-based order; alpha sort is deterministic and user-friendly |
|
||||
| CalendarStripController as VoidCallback holder | Avoids GlobalKey for single imperative scroll-to-today action — simpler |
|
||||
| Tests use pump()+pump(Duration) instead of pumpAndSettle() | CalendarStrip animation controllers cause pumpAndSettle timeout — fixed-duration pump steps are reliable |
|
||||
| No separate Riverpod provider for history sheet | ref.read(appDatabaseProvider) directly in ConsumerWidget — one-shot modals do not need a dedicated provider |
|
||||
| CalendarTaskRow onTap navigates to task edit form | Makes history accessible in one tap from home screen, consistent with GoRouter route patterns |
|
||||
Decisions archived to PROJECT.md Key Decisions table.
|
||||
|
||||
### Pending Todos
|
||||
|
||||
@@ -70,11 +54,11 @@ None.
|
||||
|
||||
### Blockers/Concerns
|
||||
|
||||
- Phase 5 complete. daily_plan_providers.dart, daily_plan_task_row.dart, and progress_card.dart are now dead code (safe to clean up in a future phase). DailyPlanDao must NOT be deleted — still used by the notification service.
|
||||
- Dead code from v1.0: daily_plan_providers.dart, daily_plan_task_row.dart, progress_card.dart (DailyPlanDao still used by notification service)
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-03-16T20:57:30Z
|
||||
Stopped at: Completed Phase 6 Plan 01 (task history)
|
||||
Resume file: .planning/phases/06-task-history/06-01-SUMMARY.md
|
||||
Next action: Phase 7 (task sorting) or release
|
||||
Last session: 2026-03-16
|
||||
Stopped at: Milestone v1.1 archived
|
||||
Resume file: None
|
||||
Next action: /gsd:new-milestone
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"commit_docs": true,
|
||||
"model_profile": "balanced",
|
||||
"workflow": {
|
||||
"research": true,
|
||||
"research": false,
|
||||
"plan_check": true,
|
||||
"verifier": true,
|
||||
"nyquist_validation": true,
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
# Requirements Archive: v1.1 Calendar & Polish
|
||||
|
||||
**Archived:** 2026-03-16
|
||||
**Status:** SHIPPED
|
||||
|
||||
For current requirements, see `.planning/REQUIREMENTS.md`.
|
||||
|
||||
---
|
||||
|
||||
# Requirements: HouseHoldKeaper
|
||||
|
||||
**Defined:** 2026-03-16
|
||||
@@ -22,9 +31,9 @@ Requirements for milestone v1.1 Calendar & Polish. Each maps to roadmap phases.
|
||||
|
||||
### Task Sorting
|
||||
|
||||
- [ ] **SORT-01**: User can sort tasks alphabetically
|
||||
- [ ] **SORT-02**: User can sort tasks by frequency interval
|
||||
- [ ] **SORT-03**: User can sort tasks by effort level
|
||||
- [x] **SORT-01**: User can sort tasks alphabetically
|
||||
- [x] **SORT-02**: User can sort tasks by frequency interval
|
||||
- [x] **SORT-03**: User can sort tasks by effort level
|
||||
|
||||
## Future Requirements
|
||||
|
||||
@@ -67,9 +76,9 @@ Which phases cover which requirements. Updated during roadmap creation.
|
||||
| CAL-05 | Phase 5 | Complete |
|
||||
| HIST-01 | Phase 6 | Complete |
|
||||
| HIST-02 | Phase 6 | Complete |
|
||||
| SORT-01 | Phase 7 | Pending |
|
||||
| SORT-02 | Phase 7 | Pending |
|
||||
| SORT-03 | Phase 7 | Pending |
|
||||
| SORT-01 | Phase 7 | Complete |
|
||||
| SORT-02 | Phase 7 | Complete |
|
||||
| SORT-03 | Phase 7 | Complete |
|
||||
|
||||
**Coverage:**
|
||||
- v1.1 requirements: 10 total
|
||||
81
.planning/milestones/v1.1-ROADMAP.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# Roadmap: HouseHoldKeaper
|
||||
|
||||
## Milestones
|
||||
|
||||
- **v1.0 MVP** — Phases 1-4 (shipped 2026-03-16)
|
||||
- **v1.1 Calendar & Polish** — Phases 5-7 (in progress)
|
||||
|
||||
## Phases
|
||||
|
||||
<details>
|
||||
<summary>v1.0 MVP (Phases 1-4) — SHIPPED 2026-03-16</summary>
|
||||
|
||||
- [x] Phase 1: Foundation (2/2 plans) — completed 2026-03-15
|
||||
- [x] Phase 2: Rooms and Tasks (5/5 plans) — completed 2026-03-15
|
||||
- [x] Phase 3: Daily Plan and Cleanliness (3/3 plans) — completed 2026-03-16
|
||||
- [x] Phase 4: Notifications (3/3 plans) — completed 2026-03-16
|
||||
|
||||
See `milestones/v1.0-ROADMAP.md` for full phase details.
|
||||
|
||||
</details>
|
||||
|
||||
**v1.1 Calendar & Polish (Phases 5-7):**
|
||||
|
||||
- [x] **Phase 5: Calendar Strip** - Replace the stacked daily plan home screen with a horizontal scrollable date-strip and day-task list (completed 2026-03-16)
|
||||
- [x] **Phase 6: Task History** - Record every task completion with a timestamp and expose a per-task history view (completed 2026-03-16)
|
||||
- [x] **Phase 7: Task Sorting** - Add alphabetical, interval, and effort sort options to task lists (completed 2026-03-16)
|
||||
|
||||
## Phase Details
|
||||
|
||||
### Phase 5: Calendar Strip
|
||||
**Goal**: Users navigate their tasks through a horizontal date-strip that replaces the stacked daily plan, seeing today's tasks by default and any day's tasks on tap
|
||||
**Depends on**: Phase 4 (v1.0 shipped — all data layer and scheduling in place)
|
||||
**Requirements**: CAL-01, CAL-02, CAL-03, CAL-04, CAL-05
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. The home screen shows a horizontal scrollable strip of day cards, each displaying the German day abbreviation (Mo, Di, Mi...) and the date number
|
||||
2. Tapping any day card updates the task list below the strip to show that day's tasks, with the selected card visually highlighted
|
||||
3. On app launch the strip auto-scrolls so today's card is centered and selected by default
|
||||
4. When two adjacent day cards span a month boundary, a subtle color shift or divider makes the boundary visible without extra chrome
|
||||
5. Tasks that were not completed on their due date appear in subsequent days' lists with a red/orange accent marking them as overdue
|
||||
**Plans:** 2/2 plans complete
|
||||
Plans:
|
||||
- [ ] 05-01-PLAN.md — Data layer: CalendarDao, CalendarDayState model, Riverpod providers, localization, DAO tests
|
||||
- [ ] 05-02-PLAN.md — UI: CalendarStrip, CalendarDayList, CalendarTaskRow widgets, HomeScreen replacement
|
||||
|
||||
### Phase 6: Task History
|
||||
**Goal**: Users can see exactly when each task was completed in the past, building trust that the scheduling loop is working correctly
|
||||
**Depends on**: Phase 5
|
||||
**Requirements**: HIST-01, HIST-02
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. Every task completion (tap done in any view) is recorded in the database with a precise timestamp — data persists across app restarts
|
||||
2. From a task's detail or context menu the user can open a history view listing all past completion dates for that task in reverse-chronological order
|
||||
3. The history view shows a meaningful empty state if the task has never been completed
|
||||
**Plans:** 1/1 plans complete
|
||||
Plans:
|
||||
- [ ] 06-01-PLAN.md — DAO query + history bottom sheet + TaskFormScreen integration + CalendarTaskRow navigation
|
||||
|
||||
### Phase 7: Task Sorting
|
||||
**Goal**: Users can reorder task lists by the dimension most useful to them — name, how often the task recurs, or how much effort it requires
|
||||
**Depends on**: Phase 5
|
||||
**Requirements**: SORT-01, SORT-02, SORT-03
|
||||
**Success Criteria** (what must be TRUE):
|
||||
1. A sort control (dropdown, segmented button, or similar) is visible on task list screens and persists the chosen sort across app restarts
|
||||
2. Selecting alphabetical sort orders tasks A-Z by name within the visible list
|
||||
3. Selecting interval sort orders tasks from most-frequent (daily) to least-frequent (yearly/custom) intervals
|
||||
4. Selecting effort sort orders tasks from lowest effort to highest effort level
|
||||
**Plans:** 2/2 plans complete
|
||||
Plans:
|
||||
- [ ] 07-01-PLAN.md — Sort model, persistence notifier, localization, provider integration
|
||||
- [ ] 07-02-PLAN.md — Sort dropdown widget, HomeScreen AppBar, TaskListScreen integration, tests
|
||||
|
||||
## Progress
|
||||
|
||||
| Phase | Milestone | Plans Complete | Status | Completed |
|
||||
|-------|-----------|----------------|--------|-----------|
|
||||
| 1. Foundation | v1.0 | 2/2 | Complete | 2026-03-15 |
|
||||
| 2. Rooms and Tasks | v1.0 | 5/5 | Complete | 2026-03-15 |
|
||||
| 3. Daily Plan and Cleanliness | v1.0 | 3/3 | Complete | 2026-03-16 |
|
||||
| 4. Notifications | v1.0 | 3/3 | Complete | 2026-03-16 |
|
||||
| 5. Calendar Strip | 2/2 | Complete | 2026-03-16 | - |
|
||||
| 6. Task History | 1/1 | Complete | 2026-03-16 | - |
|
||||
| 7. Task Sorting | 2/2 | Complete | 2026-03-16 | - |
|
||||
105
.planning/milestones/v1.1-phases/05-calendar-strip/5-CONTEXT.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# Phase 5: Calendar Strip - Context
|
||||
|
||||
**Gathered:** 2026-03-16
|
||||
**Status:** Ready for planning
|
||||
|
||||
<domain>
|
||||
## Phase Boundary
|
||||
|
||||
Replace the stacked daily plan home screen (overdue/today/tomorrow sections) with a horizontal scrollable date-strip and day-task list. Users navigate by tapping day cards to view that day's tasks below the strip. Requirements: CAL-01 through CAL-05.
|
||||
|
||||
</domain>
|
||||
|
||||
<decisions>
|
||||
## Implementation Decisions
|
||||
|
||||
### Day card appearance
|
||||
- Each card shows: German day abbreviation (Mo, Di, Mi...) and date number only
|
||||
- No task-count badges, dots, or indicators on the cards
|
||||
- All cards have a light sage/green tint
|
||||
- Selected card has a noticeably stronger green and is always centered in the strip
|
||||
- Today's card uses bold text with an accent underline
|
||||
- When today is selected, both treatments combine (bold + underline + stronger green + centered)
|
||||
|
||||
### Month boundary treatment (CAL-03)
|
||||
- A slightly wider gap between the last day of one month and the first of the next
|
||||
- A small month name label (e.g., "Apr") inserted in the gap between months
|
||||
|
||||
### Scroll range & navigation
|
||||
- Strip scrolls both into the past and into the future (Claude picks a reasonable range balancing performance and usefulness)
|
||||
- On app launch, the strip auto-scrolls to center on today with a quick slide animation (~200ms)
|
||||
- A floating "Today" button appears when the user has scrolled away from today; tap to snap back. Hidden when today is already visible.
|
||||
|
||||
### Task list below the strip
|
||||
- No ProgressCard — task list appears directly under the strip
|
||||
- Overdue tasks (CAL-05) appear in a separate section with coral accent header above the day's own tasks, same pattern as current "Überfällig" section
|
||||
- Task rows show: task name, tappable room tag, and checkbox — no relative date (strip already communicates which day)
|
||||
- Checkboxes are interactive — tapping completes the task with the existing slide-out animation
|
||||
|
||||
### Empty and celebration states
|
||||
- If a selected day had tasks that were all completed: show celebration state (icon + message, same spirit as current AllClear)
|
||||
- If a selected day never had any tasks: simple centered "Keine Aufgaben" message with subtle icon
|
||||
- First-run empty state (no rooms/tasks at all): keep the current pattern pointing user to create rooms
|
||||
|
||||
### Overdue carry-over behavior (CAL-05)
|
||||
- Overdue tasks (due before today, not yet completed) appear in a separate "Überfällig" section when viewing today
|
||||
- When viewing past days: show what was due that day (tasks whose nextDueDate matches that day)
|
||||
- When viewing future days: show only tasks due that day, no overdue carry-over
|
||||
- Overdue tasks use the existing warm coral/terracotta accent (#E07A5F)
|
||||
|
||||
### Claude's Discretion
|
||||
- Exact scroll range (past and future day count)
|
||||
- Day card dimensions, spacing, and border radius
|
||||
- Animation curves and durations beyond the ~200ms auto-scroll
|
||||
- Floating "Today" button styling and position
|
||||
- How the celebration state adapts to the calendar context (may simplify from current full-screen version)
|
||||
- Whether to reuse DailyPlanDao or create a new CalendarDao
|
||||
- Widget architecture and state management approach
|
||||
|
||||
</decisions>
|
||||
|
||||
<specifics>
|
||||
## Specific Ideas
|
||||
|
||||
- Day cards should feel like a unified strip with a light green wash — the selected day stands out by being a "marginally stronger green," not a completely different color. Think cohesive gradient, not toggle buttons.
|
||||
- The selected card is always centered — the strip scrolls to keep the selection in the middle, giving a carousel feel.
|
||||
- Month labels in the gap between months act as wayfinding, similar to section headers in a contact list.
|
||||
|
||||
</specifics>
|
||||
|
||||
<code_context>
|
||||
## Existing Code Insights
|
||||
|
||||
### Reusable Assets
|
||||
- `DailyPlanTaskRow`: Existing task row widget — can be adapted by removing the relative date display and keeping name + room tag + checkbox
|
||||
- `_CompletingTaskRow`: Animated slide-out on task completion — reuse directly for calendar task list
|
||||
- `ProgressCard`: Will NOT be used in the new view, but the pattern of a card above a list is established
|
||||
- `_overdueColor` (#E07A5F): Warm coral constant already defined for overdue indicators — reuse as-is
|
||||
- `TaskWithRoom` model: Pairs task with room name/ID — directly usable for calendar task list
|
||||
|
||||
### Established Patterns
|
||||
- Riverpod `StreamProvider.autoDispose` for reactive data (see `dailyPlanProvider`) — calendar provider follows same pattern
|
||||
- Manual provider definition (not `@riverpod`) because of drift's generated types — same constraint applies
|
||||
- Feature folder structure: `features/home/data/`, `domain/`, `presentation/` — new calendar code lives here (replaces daily plan)
|
||||
- German-only localization via `.arb` files and `AppLocalizations`
|
||||
|
||||
### Integration Points
|
||||
- `HomeScreen` at route `/` in `router.dart` — the calendar screen replaces this widget entirely
|
||||
- `AppShell` with bottom NavigationBar — home tab stays as-is, just the screen content changes
|
||||
- `DailyPlanDao.watchAllTasksWithRoomName()` — returns all tasks sorted by nextDueDate; may need a new query or adapted filtering for arbitrary date selection
|
||||
- `TaskActionsProvider` — `completeTask(taskId)` already handles task completion and nextDueDate advancement
|
||||
- `AppDatabase` with `DailyPlanDao` registered — any new DAO must be registered here
|
||||
|
||||
</code_context>
|
||||
|
||||
<deferred>
|
||||
## Deferred Ideas
|
||||
|
||||
None — discussion stayed within phase scope
|
||||
|
||||
</deferred>
|
||||
|
||||
---
|
||||
|
||||
*Phase: 05-calendar-strip*
|
||||
*Context gathered: 2026-03-16*
|
||||
276
.planning/milestones/v1.1-phases/07-task-sorting/07-01-PLAN.md
Normal file
@@ -0,0 +1,276 @@
|
||||
---
|
||||
phase: 07-task-sorting
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- lib/features/tasks/domain/task_sort_option.dart
|
||||
- lib/features/tasks/presentation/sort_preference_notifier.dart
|
||||
- lib/features/tasks/presentation/sort_preference_notifier.g.dart
|
||||
- lib/l10n/app_de.arb
|
||||
- lib/l10n/app_localizations.dart
|
||||
- lib/l10n/app_localizations_de.dart
|
||||
- lib/features/home/presentation/calendar_providers.dart
|
||||
- lib/features/tasks/presentation/task_providers.dart
|
||||
- test/features/tasks/presentation/sort_preference_notifier_test.dart
|
||||
autonomous: true
|
||||
requirements: [SORT-01, SORT-02, SORT-03]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Sort preference persists across app restarts"
|
||||
- "CalendarDayList tasks are sorted according to the active sort preference"
|
||||
- "TaskListScreen tasks are sorted according to the active sort preference"
|
||||
- "Default sort is alphabetical (matches current CalendarDayList behavior)"
|
||||
artifacts:
|
||||
- path: "lib/features/tasks/domain/task_sort_option.dart"
|
||||
provides: "TaskSortOption enum with alphabetical, interval, effort values"
|
||||
exports: ["TaskSortOption"]
|
||||
- path: "lib/features/tasks/presentation/sort_preference_notifier.dart"
|
||||
provides: "SortPreferenceNotifier with SharedPreferences persistence"
|
||||
exports: ["SortPreferenceNotifier", "sortPreferenceProvider"]
|
||||
- path: "lib/features/home/presentation/calendar_providers.dart"
|
||||
provides: "calendarDayProvider sorts dayTasks by active sort preference"
|
||||
contains: "sortPreferenceProvider"
|
||||
- path: "lib/features/tasks/presentation/task_providers.dart"
|
||||
provides: "tasksInRoomProvider sorts tasks by active sort preference"
|
||||
contains: "sortPreferenceProvider"
|
||||
- path: "test/features/tasks/presentation/sort_preference_notifier_test.dart"
|
||||
provides: "Unit tests for sort preference persistence and default"
|
||||
key_links:
|
||||
- from: "lib/features/home/presentation/calendar_providers.dart"
|
||||
to: "sortPreferenceProvider"
|
||||
via: "ref.watch in calendarDayProvider"
|
||||
pattern: "ref\\.watch\\(sortPreferenceProvider\\)"
|
||||
- from: "lib/features/tasks/presentation/task_providers.dart"
|
||||
to: "sortPreferenceProvider"
|
||||
via: "ref.watch in tasksInRoomProvider"
|
||||
pattern: "ref\\.watch\\(sortPreferenceProvider\\)"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create the task sort domain model, SharedPreferences-backed persistence provider, and integrate sort logic into both task list providers (calendarDayProvider and tasksInRoomProvider).
|
||||
|
||||
Purpose: Establishes the data layer and sort logic so that task lists react to sort preference changes. The UI plan (07-02) will add the dropdown widget that writes to this provider.
|
||||
|
||||
Output: TaskSortOption enum, SortPreferenceNotifier, updated calendarDayProvider and tasksInRoomProvider with in-memory sorting, German localization strings for sort labels.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/jlmak/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/07-task-sorting/07-CONTEXT.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and contracts the executor needs. Extracted from codebase. -->
|
||||
|
||||
From lib/features/tasks/domain/effort_level.dart:
|
||||
```dart
|
||||
enum EffortLevel {
|
||||
low, // 0
|
||||
medium, // 1
|
||||
high, // 2
|
||||
}
|
||||
```
|
||||
|
||||
From lib/features/tasks/domain/frequency.dart:
|
||||
```dart
|
||||
enum IntervalType {
|
||||
daily, // 0
|
||||
everyNDays, // 1
|
||||
weekly, // 2
|
||||
biweekly, // 3
|
||||
monthly, // 4
|
||||
everyNMonths, // 5
|
||||
quarterly, // 6
|
||||
yearly, // 7
|
||||
}
|
||||
```
|
||||
|
||||
From lib/features/home/domain/daily_plan_models.dart:
|
||||
```dart
|
||||
class TaskWithRoom {
|
||||
final Task task;
|
||||
final String roomName;
|
||||
final int roomId;
|
||||
}
|
||||
```
|
||||
|
||||
From lib/features/home/domain/calendar_models.dart:
|
||||
```dart
|
||||
class CalendarDayState {
|
||||
final DateTime selectedDate;
|
||||
final List<TaskWithRoom> dayTasks;
|
||||
final List<TaskWithRoom> overdueTasks;
|
||||
final int totalTaskCount;
|
||||
}
|
||||
```
|
||||
|
||||
From lib/core/theme/theme_provider.dart (pattern to follow for SharedPreferences notifier):
|
||||
```dart
|
||||
@riverpod
|
||||
class ThemeNotifier extends _$ThemeNotifier {
|
||||
@override
|
||||
ThemeMode build() {
|
||||
_loadPersistedThemeMode();
|
||||
return ThemeMode.system; // sync default, async load overrides
|
||||
}
|
||||
Future<void> _loadPersistedThemeMode() async { ... }
|
||||
Future<void> setThemeMode(ThemeMode mode) async {
|
||||
state = mode;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_themeModeKey, _themeModeToString(mode));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
From lib/features/home/presentation/calendar_providers.dart:
|
||||
```dart
|
||||
final calendarDayProvider = StreamProvider.autoDispose<CalendarDayState>((ref) {
|
||||
final db = ref.watch(appDatabaseProvider);
|
||||
final selectedDate = ref.watch(selectedDateProvider);
|
||||
// ... fetches dayTasks, overdueTasks, totalTaskCount
|
||||
// dayTasks come from watchTasksForDate which sorts alphabetically in SQL
|
||||
});
|
||||
```
|
||||
|
||||
From lib/features/tasks/presentation/task_providers.dart:
|
||||
```dart
|
||||
final tasksInRoomProvider = StreamProvider.family.autoDispose<List<Task>, int>((ref, roomId) {
|
||||
final db = ref.watch(appDatabaseProvider);
|
||||
return db.tasksDao.watchTasksInRoom(roomId);
|
||||
// watchTasksInRoom sorts by nextDueDate in SQL
|
||||
});
|
||||
```
|
||||
|
||||
From lib/core/database/database.dart (Task table columns relevant to sorting):
|
||||
```dart
|
||||
class Tasks extends Table {
|
||||
TextColumn get name => text().withLength(min: 1, max: 200)();
|
||||
IntColumn get intervalType => intEnum<IntervalType>()();
|
||||
IntColumn get intervalDays => integer().withDefault(const Constant(1))();
|
||||
IntColumn get effortLevel => intEnum<EffortLevel>()();
|
||||
}
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Create TaskSortOption enum, SortPreferenceNotifier, and localization strings</name>
|
||||
<files>
|
||||
lib/features/tasks/domain/task_sort_option.dart,
|
||||
lib/features/tasks/presentation/sort_preference_notifier.dart,
|
||||
lib/features/tasks/presentation/sort_preference_notifier.g.dart,
|
||||
lib/l10n/app_de.arb,
|
||||
lib/l10n/app_localizations.dart,
|
||||
lib/l10n/app_localizations_de.dart,
|
||||
test/features/tasks/presentation/sort_preference_notifier_test.dart
|
||||
</files>
|
||||
<behavior>
|
||||
- Default sort preference is TaskSortOption.alphabetical
|
||||
- setSortOption(TaskSortOption.interval) updates state to interval
|
||||
- Sort preference persists: after setSortOption(effort), a fresh notifier reads back effort from SharedPreferences
|
||||
- TaskSortOption enum has exactly 3 values: alphabetical, interval, effort
|
||||
</behavior>
|
||||
<action>
|
||||
1. Create `lib/features/tasks/domain/task_sort_option.dart`:
|
||||
- `enum TaskSortOption { alphabetical, interval, effort }` — three values only, no index stability concern since this is NOT stored as intEnum in drift (stored as string in SharedPreferences)
|
||||
|
||||
2. Create `lib/features/tasks/presentation/sort_preference_notifier.dart`:
|
||||
- Follow the exact ThemeNotifier pattern from `lib/core/theme/theme_provider.dart`
|
||||
- `@riverpod class SortPreferenceNotifier extends _$SortPreferenceNotifier`
|
||||
- `build()` returns `TaskSortOption.alphabetical` synchronously (default = alphabetical per user decision for continuity with current A-Z sort in CalendarDayList), then calls `_loadPersisted()` async
|
||||
- `_loadPersisted()` reads `SharedPreferences.getString('task_sort_option')` and maps to enum
|
||||
- `setSortOption(TaskSortOption option)` sets state immediately then persists string to SharedPreferences
|
||||
- Static helpers `_fromString` / `_toString` for serialization (use enum .name property)
|
||||
- The generated provider will be named `sortPreferenceProvider` (Riverpod 3 naming convention, consistent with themeProvider)
|
||||
|
||||
3. Run `dart run build_runner build --delete-conflicting-outputs` to generate `.g.dart`
|
||||
|
||||
4. Add localization strings to `lib/l10n/app_de.arb`:
|
||||
- `"sortAlphabetical": "A\u2013Z"` (A-Z with en-dash, concise label per user decision)
|
||||
- `"sortInterval": "Intervall"` (German for interval/frequency)
|
||||
- `"sortEffort": "Aufwand"` (German for effort, matches existing taskFormEffortLabel context)
|
||||
- `"sortLabel": "Sortierung"` (label for accessibility/semantics on the dropdown)
|
||||
|
||||
5. Run `flutter gen-l10n` to regenerate localization files
|
||||
|
||||
6. Write tests in `test/features/tasks/presentation/sort_preference_notifier_test.dart`:
|
||||
- Follow the pattern from notification_settings test: `makeContainer()` helper that creates ProviderContainer, awaits `Future.delayed(Duration.zero)` for async load
|
||||
- `SharedPreferences.setMockInitialValues({})` in setUp
|
||||
- Test: default is alphabetical
|
||||
- Test: setSortOption updates state
|
||||
- Test: persisted value is loaded on restart (set mock initial values with key, verify state after load)
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter test test/features/tasks/presentation/sort_preference_notifier_test.dart && flutter analyze --no-fatal-infos</automated>
|
||||
</verify>
|
||||
<done>TaskSortOption enum exists with 3 values. SortPreferenceNotifier persists to SharedPreferences. 3+ unit tests pass. ARB file has 4 new sort strings. dart analyze clean.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Integrate sort logic into calendarDayProvider and tasksInRoomProvider</name>
|
||||
<files>
|
||||
lib/features/home/presentation/calendar_providers.dart,
|
||||
lib/features/tasks/presentation/task_providers.dart
|
||||
</files>
|
||||
<action>
|
||||
1. Edit `lib/features/home/presentation/calendar_providers.dart`:
|
||||
- Add import for `sort_preference_notifier.dart` and `task_sort_option.dart`
|
||||
- Inside `calendarDayProvider`, add `final sortOption = ref.watch(sortPreferenceProvider);`
|
||||
- After constructing `CalendarDayState`, apply in-memory sort to `dayTasks` list before returning. Do NOT sort overdueTasks (overdue section stays pinned at top in its existing order per user discretion decision).
|
||||
- Sort implementation — create a top-level helper function `List<TaskWithRoom> _sortTasks(List<TaskWithRoom> tasks, TaskSortOption sortOption)` that returns a new sorted list:
|
||||
- `alphabetical`: sort by `task.name.toLowerCase()` (case-insensitive A-Z)
|
||||
- `interval`: sort by `task.intervalType.index` ascending (daily=0 is most frequent, yearly=7 is least), then by `task.intervalDays` ascending as tiebreaker
|
||||
- `effort`: sort by `task.effortLevel.index` ascending (low=0, medium=1, high=2)
|
||||
- Apply: `dayTasks: _sortTasks(dayTasks, sortOption)` in the CalendarDayState constructor call
|
||||
- Note: The SQL `orderBy([OrderingTerm.asc(tasks.name)])` in CalendarDao.watchTasksForDate still runs, but the in-memory sort overrides it. This is intentional — the SQL sort provides a stable baseline, the in-memory sort applies the user's preference.
|
||||
|
||||
2. Edit `lib/features/tasks/presentation/task_providers.dart`:
|
||||
- Add import for `sort_preference_notifier.dart` and `task_sort_option.dart`
|
||||
- In `tasksInRoomProvider`, add `final sortOption = ref.watch(sortPreferenceProvider);`
|
||||
- Map the stream to apply in-memory sorting: `return db.tasksDao.watchTasksInRoom(roomId).map((tasks) => _sortTasksRaw(tasks, sortOption));`
|
||||
- Create a top-level helper `List<Task> _sortTasksRaw(List<Task> tasks, TaskSortOption sortOption)` that sorts raw Task objects (not TaskWithRoom):
|
||||
- `alphabetical`: sort by `task.name.toLowerCase()`
|
||||
- `interval`: sort by `task.intervalType.index`, then `task.intervalDays`
|
||||
- `effort`: sort by `task.effortLevel.index`
|
||||
- Returns a new sorted list (do not mutate the original)
|
||||
|
||||
3. Verify both providers react to sort preference changes by running existing tests (they should still pass since default sort is alphabetical and current data is already alphabetically sorted or test data is single-item).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter test && flutter analyze --no-fatal-infos</automated>
|
||||
</verify>
|
||||
<done>calendarDayProvider watches sortPreferenceProvider and sorts dayTasks accordingly. tasksInRoomProvider watches sortPreferenceProvider and sorts tasks accordingly. All 106+ existing tests pass. dart analyze clean.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `flutter test` — all 106+ tests pass (existing + new sort preference tests)
|
||||
- `flutter analyze --no-fatal-infos` — zero issues
|
||||
- `sortPreferenceProvider` is watchable and defaults to alphabetical
|
||||
- Both calendarDayProvider and tasksInRoomProvider react to sort preference changes
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- TaskSortOption enum exists with alphabetical, interval, effort values
|
||||
- SortPreferenceNotifier persists sort preference to SharedPreferences
|
||||
- Default sort is alphabetical (continuity with existing A-Z sort)
|
||||
- calendarDayProvider sorts dayTasks by active sort (overdue section unsorted)
|
||||
- tasksInRoomProvider sorts tasks by active sort
|
||||
- All tests pass, analyze clean
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/07-task-sorting/07-01-SUMMARY.md`
|
||||
</output>
|
||||
@@ -0,0 +1,138 @@
|
||||
---
|
||||
phase: 07-task-sorting
|
||||
plan: 01
|
||||
subsystem: ui
|
||||
tags: [flutter, riverpod, shared_preferences, sorting, localization]
|
||||
|
||||
# Dependency graph
|
||||
requires:
|
||||
- phase: 05-calendar-strip
|
||||
provides: calendarDayProvider and CalendarDayState used by sort integration
|
||||
- phase: 06-task-history
|
||||
provides: task domain model and CalendarTaskRow context
|
||||
|
||||
provides:
|
||||
- TaskSortOption enum (alphabetical, interval, effort)
|
||||
- SortPreferenceNotifier with SharedPreferences persistence
|
||||
- sortPreferenceProvider (keepAlive Riverpod provider)
|
||||
- calendarDayProvider with in-memory sort of dayTasks
|
||||
- tasksInRoomProvider with in-memory sort via stream.map
|
||||
- German localization strings: sortAlphabetical, sortInterval, sortEffort, sortLabel
|
||||
|
||||
affects: [07-02-sort-ui, any phase using calendarDayProvider or tasksInRoomProvider]
|
||||
|
||||
# Tech tracking
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "SortPreferenceNotifier: sync default return, async _loadPersisted() — same pattern as ThemeNotifier"
|
||||
- "In-memory sort helper functions (_sortTasks, _sortTasksRaw) applied after DB stream emit"
|
||||
- "overdueTasks intentionally unsorted — only dayTasks sorted"
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- lib/features/tasks/domain/task_sort_option.dart
|
||||
- lib/features/tasks/presentation/sort_preference_notifier.dart
|
||||
- lib/features/tasks/presentation/sort_preference_notifier.g.dart
|
||||
- test/features/tasks/presentation/sort_preference_notifier_test.dart
|
||||
modified:
|
||||
- lib/features/home/presentation/calendar_providers.dart
|
||||
- lib/features/tasks/presentation/task_providers.dart
|
||||
- lib/l10n/app_de.arb
|
||||
- lib/l10n/app_localizations.dart
|
||||
- lib/l10n/app_localizations_de.dart
|
||||
|
||||
key-decisions:
|
||||
- "Default sort is alphabetical — continuity with existing A-Z SQL sort in CalendarDayList"
|
||||
- "overdueTasks are NOT sorted — they stay pinned at the top in existing order"
|
||||
- "Sort stored as string (enum.name) in SharedPreferences — not intEnum, so reordering enum is safe"
|
||||
- "SortPreferenceNotifier uses keepAlive: true — global preference should never be disposed"
|
||||
|
||||
patterns-established:
|
||||
- "SortPreferenceNotifier pattern: sync default + async _loadPersisted() — matches ThemeNotifier"
|
||||
- "In-memory sort via stream.map in StreamProvider — DB SQL sort provides stable baseline, in-memory overrides"
|
||||
|
||||
requirements-completed: [SORT-01, SORT-02, SORT-03]
|
||||
|
||||
# Metrics
|
||||
duration: 4min
|
||||
completed: 2026-03-16
|
||||
---
|
||||
|
||||
# Phase 07 Plan 01: Task Sort Domain and Provider Summary
|
||||
|
||||
**TaskSortOption enum + SharedPreferences-backed SortPreferenceNotifier wired into calendarDayProvider and tasksInRoomProvider with in-memory alphabetical/interval/effort sorting**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** 4 min
|
||||
- **Started:** 2026-03-16T21:29:32Z
|
||||
- **Completed:** 2026-03-16T21:33:37Z
|
||||
- **Tasks:** 2
|
||||
- **Files modified:** 9
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- TaskSortOption enum (alphabetical, interval, effort) with SharedPreferences persistence via SortPreferenceNotifier
|
||||
- calendarDayProvider now watches sortPreferenceProvider and sorts dayTasks in-memory; overdueTasks intentionally unsorted
|
||||
- tasksInRoomProvider now watches sortPreferenceProvider and applies sort via stream.map
|
||||
- 7 new unit tests for SortPreferenceNotifier covering default, state update, persistence, and restart recovery
|
||||
- 4 German localization strings added (sortAlphabetical, sortInterval, sortEffort, sortLabel)
|
||||
|
||||
## Task Commits
|
||||
|
||||
Each task was committed atomically:
|
||||
|
||||
1. **TDD RED: Failing sort preference tests** - `a9f2983` (test)
|
||||
2. **Task 1: TaskSortOption enum, SortPreferenceNotifier, localization** - `13c7d62` (feat)
|
||||
3. **Task 2: Sort integration into calendarDayProvider and tasksInRoomProvider** - `3697e4e` (feat)
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `lib/features/tasks/domain/task_sort_option.dart` - TaskSortOption enum with alphabetical/interval/effort values
|
||||
- `lib/features/tasks/presentation/sort_preference_notifier.dart` - SortPreferenceNotifier with SharedPreferences persistence
|
||||
- `lib/features/tasks/presentation/sort_preference_notifier.g.dart` - Generated Riverpod provider code
|
||||
- `lib/features/home/presentation/calendar_providers.dart` - Added sortPreferenceProvider watch + _sortTasks helper
|
||||
- `lib/features/tasks/presentation/task_providers.dart` - Added sortPreferenceProvider watch + _sortTasksRaw helper + stream.map
|
||||
- `lib/l10n/app_de.arb` - Added sortAlphabetical, sortInterval, sortEffort, sortLabel strings
|
||||
- `lib/l10n/app_localizations.dart` - Regenerated with sort string getters
|
||||
- `lib/l10n/app_localizations_de.dart` - Regenerated with German sort string implementations
|
||||
- `test/features/tasks/presentation/sort_preference_notifier_test.dart` - 7 unit tests for sort preference
|
||||
|
||||
## Decisions Made
|
||||
|
||||
- Default sort is alphabetical for continuity with existing SQL A-Z sort in CalendarDayList
|
||||
- overdueTasks section is explicitly NOT sorted — stays pinned at top in existing order
|
||||
- Sort preference stored as enum.name string in SharedPreferences (not intEnum) so enum reordering is always safe
|
||||
- SortPreferenceNotifier uses `keepAlive: true` — global app preference must not be disposed
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
None - plan executed exactly as written.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
None.
|
||||
|
||||
## User Setup Required
|
||||
|
||||
None - no external service configuration required.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
- sortPreferenceProvider is live and defaults to alphabetical
|
||||
- Both task list providers react to sort preference changes immediately
|
||||
- Ready for 07-02: sort UI (dropdown in AppBar) to write to sortPreferenceProvider
|
||||
|
||||
---
|
||||
*Phase: 07-task-sorting*
|
||||
*Completed: 2026-03-16*
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
- FOUND: lib/features/tasks/domain/task_sort_option.dart
|
||||
- FOUND: lib/features/tasks/presentation/sort_preference_notifier.dart
|
||||
- FOUND: lib/features/tasks/presentation/sort_preference_notifier.g.dart
|
||||
- FOUND: test/features/tasks/presentation/sort_preference_notifier_test.dart
|
||||
- FOUND: .planning/phases/07-task-sorting/07-01-SUMMARY.md
|
||||
- Commits a9f2983, 13c7d62, 3697e4e all verified in git log
|
||||
214
.planning/milestones/v1.1-phases/07-task-sorting/07-02-PLAN.md
Normal file
@@ -0,0 +1,214 @@
|
||||
---
|
||||
phase: 07-task-sorting
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["07-01"]
|
||||
files_modified:
|
||||
- lib/features/tasks/presentation/sort_dropdown.dart
|
||||
- lib/features/home/presentation/home_screen.dart
|
||||
- lib/features/tasks/presentation/task_list_screen.dart
|
||||
- test/features/home/presentation/home_screen_test.dart
|
||||
autonomous: true
|
||||
requirements: [SORT-01, SORT-02, SORT-03]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "A sort dropdown is visible in the HomeScreen AppBar showing the current sort label"
|
||||
- "A sort dropdown is visible in the TaskListScreen AppBar showing the current sort label"
|
||||
- "Tapping the dropdown shows three options: A-Z, Intervall, Aufwand"
|
||||
- "Selecting a sort option updates the task list order immediately"
|
||||
- "The sort preference persists across screen navigations and app restarts"
|
||||
artifacts:
|
||||
- path: "lib/features/tasks/presentation/sort_dropdown.dart"
|
||||
provides: "Reusable SortDropdown ConsumerWidget"
|
||||
exports: ["SortDropdown"]
|
||||
- path: "lib/features/home/presentation/home_screen.dart"
|
||||
provides: "HomeScreen with AppBar containing SortDropdown"
|
||||
contains: "SortDropdown"
|
||||
- path: "lib/features/tasks/presentation/task_list_screen.dart"
|
||||
provides: "TaskListScreen AppBar with SortDropdown alongside edit/delete"
|
||||
contains: "SortDropdown"
|
||||
key_links:
|
||||
- from: "lib/features/tasks/presentation/sort_dropdown.dart"
|
||||
to: "sortPreferenceProvider"
|
||||
via: "ref.watch for display, ref.read for mutation"
|
||||
pattern: "ref\\.watch\\(sortPreferenceProvider\\)"
|
||||
- from: "lib/features/home/presentation/home_screen.dart"
|
||||
to: "lib/features/tasks/presentation/sort_dropdown.dart"
|
||||
via: "SortDropdown widget in AppBar actions"
|
||||
pattern: "SortDropdown"
|
||||
- from: "lib/features/tasks/presentation/task_list_screen.dart"
|
||||
to: "lib/features/tasks/presentation/sort_dropdown.dart"
|
||||
via: "SortDropdown widget in AppBar actions"
|
||||
pattern: "SortDropdown"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Build the sort dropdown widget and wire it into both task list screens (HomeScreen and TaskListScreen), adding an AppBar to HomeScreen.
|
||||
|
||||
Purpose: Gives users visible access to the sort controls. The data layer from Plan 01 already sorts reactively; this plan adds the UI trigger.
|
||||
|
||||
Output: SortDropdown reusable widget, updated HomeScreen with AppBar, updated TaskListScreen with dropdown in existing AppBar, updated tests.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/jlmak/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/jlmak/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/07-task-sorting/07-CONTEXT.md
|
||||
@.planning/phases/07-task-sorting/07-01-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Interfaces from Plan 01 that this plan depends on -->
|
||||
|
||||
From lib/features/tasks/domain/task_sort_option.dart (created in 07-01):
|
||||
```dart
|
||||
enum TaskSortOption { alphabetical, interval, effort }
|
||||
```
|
||||
|
||||
From lib/features/tasks/presentation/sort_preference_notifier.dart (created in 07-01):
|
||||
```dart
|
||||
@riverpod
|
||||
class SortPreferenceNotifier extends _$SortPreferenceNotifier {
|
||||
TaskSortOption build(); // returns alphabetical by default
|
||||
Future<void> setSortOption(TaskSortOption option);
|
||||
}
|
||||
// Generated as: sortPreferenceProvider
|
||||
```
|
||||
|
||||
From lib/l10n/app_de.arb (strings added in 07-01):
|
||||
```
|
||||
sortAlphabetical: "A-Z"
|
||||
sortInterval: "Intervall"
|
||||
sortEffort: "Aufwand"
|
||||
sortLabel: "Sortierung"
|
||||
```
|
||||
|
||||
<!-- Existing interfaces being modified -->
|
||||
|
||||
From lib/features/home/presentation/home_screen.dart:
|
||||
```dart
|
||||
class HomeScreen extends ConsumerStatefulWidget {
|
||||
// Currently: Stack with CalendarStrip + CalendarDayList + floating Today FAB
|
||||
// No AppBar — body sits directly inside AppShell's Scaffold
|
||||
}
|
||||
```
|
||||
|
||||
From lib/features/tasks/presentation/task_list_screen.dart:
|
||||
```dart
|
||||
class TaskListScreen extends ConsumerWidget {
|
||||
// Has its own Scaffold with AppBar containing edit + delete IconButtons
|
||||
// AppBar actions: [edit, delete]
|
||||
}
|
||||
```
|
||||
|
||||
From lib/features/home/presentation/calendar_strip.dart:
|
||||
```dart
|
||||
class CalendarStrip extends StatefulWidget {
|
||||
const CalendarStrip({super.key, required this.controller, this.onTodayVisibilityChanged});
|
||||
final CalendarStripController controller;
|
||||
final ValueChanged<bool>? onTodayVisibilityChanged;
|
||||
}
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Build SortDropdown widget and integrate into HomeScreen and TaskListScreen</name>
|
||||
<files>
|
||||
lib/features/tasks/presentation/sort_dropdown.dart,
|
||||
lib/features/home/presentation/home_screen.dart,
|
||||
lib/features/tasks/presentation/task_list_screen.dart
|
||||
</files>
|
||||
<action>
|
||||
1. Create `lib/features/tasks/presentation/sort_dropdown.dart`:
|
||||
- A `ConsumerWidget` named `SortDropdown`
|
||||
- Uses `PopupMenuButton<TaskSortOption>` (Material 3, better than DropdownButton for AppBar trailing actions — it opens a menu overlay rather than inline expansion)
|
||||
- `ref.watch(sortPreferenceProvider)` to get current sort option
|
||||
- The button child shows the current sort label as a Text widget using l10n strings:
|
||||
- `alphabetical` -> `l10n.sortAlphabetical` (A-Z)
|
||||
- `interval` -> `l10n.sortInterval` (Intervall)
|
||||
- `effort` -> `l10n.sortEffort` (Aufwand)
|
||||
- Style the button child as a Row with `Icon(Icons.sort)` + `SizedBox(width: 4)` + label Text. Use `theme.textTheme.labelLarge` for the text.
|
||||
- `itemBuilder` returns 3 `PopupMenuItem<TaskSortOption>` entries with check marks: for each option, show a Row with `Icon(Icons.check, size: 18)` (visible only when selected, invisible when not via `Opacity(opacity: isSelected ? 1 : 0)`) + `SizedBox(width: 8)` + label Text
|
||||
- `onSelected`: `ref.read(sortPreferenceProvider.notifier).setSortOption(value)`
|
||||
- Helper method `String _label(TaskSortOption option, AppLocalizations l10n)` that maps enum to l10n string
|
||||
|
||||
2. Edit `lib/features/home/presentation/home_screen.dart`:
|
||||
- HomeScreen currently returns a `Stack` with `Column(CalendarStrip, Expanded(CalendarDayList))` + optional floating Today button
|
||||
- Wrap the entire current Stack in a `Scaffold` with an `AppBar`:
|
||||
- `AppBar(title: Text(l10n.tabHome), actions: [const SortDropdown()])`
|
||||
- The `tabHome` l10n string already exists ("Ubersicht") — reuse it as the AppBar title for the home screen
|
||||
- body: the existing Stack content
|
||||
- Keep CalendarStrip, CalendarDayList, and floating Today FAB exactly as they are
|
||||
- Import `sort_dropdown.dart`
|
||||
- Note: HomeScreen is inside AppShell's Scaffold body. Adding a nested Scaffold is fine and standard for per-tab AppBars in StatefulShellRoute.indexedStack. The AppShell Scaffold provides the bottom nav; the inner Scaffold provides the AppBar.
|
||||
|
||||
3. Edit `lib/features/tasks/presentation/task_list_screen.dart`:
|
||||
- In the existing `AppBar.actions` list, add `const SortDropdown()` BEFORE the edit and delete IconButtons. Order: [SortDropdown, edit, delete].
|
||||
- Import `sort_dropdown.dart`
|
||||
- No other changes to TaskListScreen
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter analyze --no-fatal-infos</automated>
|
||||
</verify>
|
||||
<done>SortDropdown widget exists showing current sort label with sort icon. HomeScreen has AppBar with title "Ubersicht" and SortDropdown. TaskListScreen AppBar has SortDropdown before edit/delete buttons. dart analyze clean.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Update tests for HomeScreen AppBar and sort dropdown</name>
|
||||
<files>
|
||||
test/features/home/presentation/home_screen_test.dart
|
||||
</files>
|
||||
<action>
|
||||
1. Edit `test/features/home/presentation/home_screen_test.dart`:
|
||||
- Add import for `sort_preference_notifier.dart` and `task_sort_option.dart`
|
||||
- In the `_buildApp` helper, add a provider override for `sortPreferenceProvider`:
|
||||
```dart
|
||||
sortPreferenceProvider.overrideWith(SortPreferenceNotifier.new),
|
||||
```
|
||||
This will use the real notifier with mock SharedPreferences (already set up in setUp).
|
||||
- Add a new test group `'HomeScreen sort dropdown'`:
|
||||
- Test: "shows sort dropdown in AppBar" — pump the app with tasks, verify `find.byType(PopupMenuButton<TaskSortOption>)` findsOneWidget
|
||||
- Test: "shows AppBar with title" — verify `find.text('Ubersicht')` findsOneWidget (the tabHome l10n string)
|
||||
- Verify all existing tests still pass. The addition of an AppBar wrapping the existing content should not break existing assertions since they look for specific widgets/text within the tree.
|
||||
|
||||
2. Run full test suite to confirm no regressions.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jlmak/Projects/jlmak/HouseHoldKeaper && flutter test && flutter analyze --no-fatal-infos</automated>
|
||||
</verify>
|
||||
<done>Home screen tests verify AppBar with sort dropdown is present. All 108+ tests pass (106 existing + 2+ new). dart analyze clean.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `flutter test` — all tests pass including new sort dropdown tests
|
||||
- `flutter analyze --no-fatal-infos` — zero issues
|
||||
- HomeScreen has AppBar with SortDropdown visible
|
||||
- TaskListScreen has SortDropdown in AppBar actions
|
||||
- Tapping dropdown shows 3 options with check mark on current selection
|
||||
- Selecting a different sort option reorders the task list reactively
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- SortDropdown widget is reusable and shows current sort with icon
|
||||
- HomeScreen has AppBar titled "Ubersicht" with SortDropdown in trailing actions
|
||||
- TaskListScreen has SortDropdown before edit/delete buttons in AppBar
|
||||
- Sort selection updates task list order immediately (reactive via provider)
|
||||
- Sort preference persists (set in one screen, visible in another after navigation)
|
||||
- All tests pass, analyze clean
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/07-task-sorting/07-02-SUMMARY.md`
|
||||
</output>
|
||||
@@ -0,0 +1,125 @@
|
||||
---
|
||||
phase: 07-task-sorting
|
||||
plan: 02
|
||||
subsystem: ui
|
||||
tags: [flutter, riverpod, material3, popup-menu, sort-ui, localization]
|
||||
|
||||
# Dependency graph
|
||||
requires:
|
||||
- phase: 07-task-sorting
|
||||
plan: 01
|
||||
provides: sortPreferenceProvider, TaskSortOption enum, German sort l10n strings
|
||||
|
||||
provides:
|
||||
- SortDropdown ConsumerWidget (PopupMenuButton<TaskSortOption> with check marks)
|
||||
- HomeScreen with AppBar (title: Übersicht, actions: SortDropdown)
|
||||
- TaskListScreen AppBar with SortDropdown before edit/delete buttons
|
||||
|
||||
affects: [home_screen_test.dart, app_shell_test.dart, any screen showing HomeScreen]
|
||||
|
||||
# Tech tracking
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "PopupMenuButton<TaskSortOption> with Opacity check mark — avoids layout shift vs conditional Icon"
|
||||
- "Nested Scaffold inside AppShell tab body — standard pattern for per-tab AppBars in StatefulShellRoute"
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- lib/features/tasks/presentation/sort_dropdown.dart
|
||||
modified:
|
||||
- lib/features/home/presentation/home_screen.dart
|
||||
- lib/features/tasks/presentation/task_list_screen.dart
|
||||
- test/features/home/presentation/home_screen_test.dart
|
||||
- test/shell/app_shell_test.dart
|
||||
|
||||
key-decisions:
|
||||
- "Used PopupMenuButton instead of DropdownButton for AppBar — menu overlay vs inline expansion, consistent with Material 3 AppBar action patterns"
|
||||
- "Opacity(opacity: isSelected ? 1 : 0) for check mark — preserves item width alignment vs conditional show/hide"
|
||||
- "HomeScreen Scaffold is nested inside AppShell Scaffold — standard StatefulShellRoute pattern for per-tab AppBars"
|
||||
|
||||
# Metrics
|
||||
duration: 4min
|
||||
completed: 2026-03-16
|
||||
---
|
||||
|
||||
# Phase 07 Plan 02: Sort Dropdown UI Summary
|
||||
|
||||
**SortDropdown ConsumerWidget using PopupMenuButton wired into HomeScreen AppBar (title: Übersicht) and TaskListScreen AppBar before edit/delete actions**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** 4 min
|
||||
- **Started:** 2026-03-16T21:35:56Z
|
||||
- **Completed:** 2026-03-16T21:39:24Z
|
||||
- **Tasks:** 2
|
||||
- **Files modified:** 5 (1 created, 4 modified)
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- SortDropdown ConsumerWidget: PopupMenuButton<TaskSortOption> with sort icon, current label, and check mark on active option
|
||||
- HomeScreen wrapped in Scaffold with AppBar titled "Übersicht" and SortDropdown in trailing actions
|
||||
- TaskListScreen AppBar has SortDropdown before the existing edit/delete IconButtons
|
||||
- 2 new tests in HomeScreen test suite: verifies PopupMenuButton and AppBar title presence
|
||||
- Auto-fixed app_shell_test regression caused by "Übersicht" now appearing twice (AppBar + bottom nav)
|
||||
|
||||
## Task Commits
|
||||
|
||||
Each task was committed atomically:
|
||||
|
||||
1. **Task 1: SortDropdown widget and HomeScreen/TaskListScreen integration** - `e5eccb7` (feat)
|
||||
2. **Task 2: Sort dropdown tests and AppShell test fix** - `a3e4d02` (feat)
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `lib/features/tasks/presentation/sort_dropdown.dart` - Reusable SortDropdown ConsumerWidget with PopupMenuButton<TaskSortOption>
|
||||
- `lib/features/home/presentation/home_screen.dart` - Added Scaffold with AppBar (Übersicht title + SortDropdown)
|
||||
- `lib/features/tasks/presentation/task_list_screen.dart` - Added SortDropdown before edit/delete in AppBar actions
|
||||
- `test/features/home/presentation/home_screen_test.dart` - Added sortPreferenceProvider override + 2 new sort dropdown tests
|
||||
- `test/shell/app_shell_test.dart` - Fixed findsOneWidget -> findsWidgets for 'Übersicht' (now in AppBar + bottom nav)
|
||||
|
||||
## Decisions Made
|
||||
|
||||
- Used PopupMenuButton instead of DropdownButton for AppBar actions — menu overlay is cleaner in AppBar context (Material 3)
|
||||
- Opacity trick for check mark: `Opacity(opacity: isSelected ? 1 : 0)` preserves item width so labels align regardless of selection
|
||||
- HomeScreen uses nested Scaffold for AppBar — standard pattern in StatefulShellRoute.indexedStack; AppShell provides bottom nav, HomeScreen provides AppBar
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 1 - Bug] Fixed AppShell test regression from 'Übersicht' duplicate**
|
||||
- **Found during:** Task 2 test run
|
||||
- **Issue:** `app_shell_test.dart` expected `findsOneWidget` for 'Übersicht'. Adding the HomeScreen AppBar title caused the string to appear twice (AppBar + bottom nav label).
|
||||
- **Fix:** Changed `findsOneWidget` to `findsWidgets` in `app_shell_test.dart` line 67. Applied same fix to new `home_screen_test.dart` AppBar title test.
|
||||
- **Files modified:** `test/shell/app_shell_test.dart`, `test/features/home/presentation/home_screen_test.dart`
|
||||
- **Commit:** `a3e4d02`
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
None beyond the auto-fixed regression.
|
||||
|
||||
## User Setup Required
|
||||
|
||||
None - no external service configuration required.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
- Phase 07 (task sorting) is now complete: data layer (07-01) + UI layer (07-02)
|
||||
- Sort dropdown is live in both HomeScreen and TaskListScreen AppBars
|
||||
- Selecting a sort option reactively reorders task lists via sortPreferenceProvider
|
||||
- Preference persists across app restarts via SharedPreferences
|
||||
|
||||
---
|
||||
*Phase: 07-task-sorting*
|
||||
*Completed: 2026-03-16*
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
- FOUND: lib/features/tasks/presentation/sort_dropdown.dart
|
||||
- FOUND: lib/features/home/presentation/home_screen.dart (modified)
|
||||
- FOUND: lib/features/tasks/presentation/task_list_screen.dart (modified)
|
||||
- FOUND: test/features/home/presentation/home_screen_test.dart (modified)
|
||||
- FOUND: test/shell/app_shell_test.dart (modified)
|
||||
- Commits e5eccb7, a3e4d02 verified in git log
|
||||
- All 115 tests pass, dart analyze clean
|
||||
@@ -0,0 +1,91 @@
|
||||
# Phase 7: Task Sorting - Context
|
||||
|
||||
**Gathered:** 2026-03-16
|
||||
**Status:** Ready for planning
|
||||
|
||||
<domain>
|
||||
## Phase Boundary
|
||||
|
||||
Add sort controls to task list screens so users can reorder tasks by name (alphabetical), frequency interval, or effort level. The sort preference persists across app restarts. Requirements: SORT-01, SORT-02, SORT-03.
|
||||
|
||||
</domain>
|
||||
|
||||
<decisions>
|
||||
## Implementation Decisions
|
||||
|
||||
### Sort control widget
|
||||
- Dropdown button in the AppBar, right side (trailing actions position)
|
||||
- When collapsed, shows the current sort name as text (e.g., "A-Z", "Intervall", "Aufwand")
|
||||
- Expands to show the 3 sort options as a standard dropdown menu
|
||||
|
||||
### Sort option labels
|
||||
- Claude's discretion — pick German labels that fit the app's existing localization style (concise but clear)
|
||||
|
||||
### Sort scope
|
||||
- One global sort preference applies to all task list screens
|
||||
- Same dropdown appears in both the home screen (CalendarDayList) and per-room (TaskListScreen) AppBars
|
||||
|
||||
### Persistence
|
||||
- Store the sort preference in SharedPreferences (simple key-value for a single enum)
|
||||
- No database schema change needed
|
||||
- Persists across app restarts per success criteria
|
||||
|
||||
### Default sort
|
||||
- Claude's discretion — pick the least disruptive default (likely alphabetical to match current CalendarDayList behavior)
|
||||
|
||||
### Claude's Discretion
|
||||
- Sort option label text (German, concise)
|
||||
- Default sort order (recommend alphabetical for continuity)
|
||||
- Whether TaskListScreen also gets the dropdown (recommend yes for consistency with global setting, since the success criteria says "task list screens" plural)
|
||||
- Sort direction (always ascending — A-Z, daily→yearly, low→high — no toggle needed for MVP)
|
||||
- Dropdown styling (Material 3 DropdownButton or PopupMenuButton variant)
|
||||
- Sort icon or visual indicator in the dropdown
|
||||
- How overdue section interacts with sorting (recommend: overdue section stays pinned at top regardless of sort, only day tasks are sorted)
|
||||
|
||||
</decisions>
|
||||
|
||||
<specifics>
|
||||
## Specific Ideas
|
||||
|
||||
No specific requirements — open to standard approaches that match existing app patterns.
|
||||
|
||||
</specifics>
|
||||
|
||||
<code_context>
|
||||
## Existing Code Insights
|
||||
|
||||
### Reusable Assets
|
||||
- `EffortLevel` enum (low/medium/high) with `.index` for ordering — directly usable for effort sort
|
||||
- `IntervalType` enum with `.index` ordered roughly by frequency (daily=0 through yearly=7) — usable for interval sort
|
||||
- `FrequencyInterval.presets` list ordered most-frequent to least — reference for sort order
|
||||
- `Task.name` field — direct alphabetical sort target
|
||||
- `CalendarDayList` and `TaskListScreen` — the two list widgets that need sort integration
|
||||
- `AppLocalizations` + `.arb` files — existing German localization pipeline
|
||||
|
||||
### Established Patterns
|
||||
- Manual `StreamProvider.autoDispose` for drift types (riverpod_generator issue) — sort provider follows same pattern
|
||||
- `calendarDayProvider` watches `selectedDateProvider` — can also watch a sort preference provider
|
||||
- `tasksInRoomProvider` family provider — can be extended with sort parameter or read global sort
|
||||
- Feature folder structure: `features/home/`, `features/tasks/` — sort logic may live in a shared location or in each feature
|
||||
|
||||
### Integration Points
|
||||
- `HomeScreen` AppBar — add dropdown to trailing actions
|
||||
- `TaskListScreen` AppBar — already has edit/delete actions; add dropdown alongside
|
||||
- `CalendarDao.watchTasksForDate()` — currently sorts alphabetically; needs sort-aware query or in-memory sort
|
||||
- `TasksDao.watchTasksInRoom()` — currently sorts by nextDueDate; needs sort-aware query or in-memory sort
|
||||
- `SharedPreferences` — not yet used in the app; needs package addition and provider setup
|
||||
- `app_de.arb` — add localization strings for sort labels
|
||||
|
||||
</code_context>
|
||||
|
||||
<deferred>
|
||||
## Deferred Ideas
|
||||
|
||||
None — discussion stayed within phase scope
|
||||
|
||||
</deferred>
|
||||
|
||||
---
|
||||
|
||||
*Phase: 07-task-sorting*
|
||||
*Context gathered: 2026-03-16*
|
||||
@@ -0,0 +1,135 @@
|
||||
---
|
||||
phase: 07-task-sorting
|
||||
verified: 2026-03-16T22:00:00Z
|
||||
status: passed
|
||||
score: 9/9 must-haves verified
|
||||
re_verification: false
|
||||
---
|
||||
|
||||
# Phase 7: Task Sorting Verification Report
|
||||
|
||||
**Phase Goal:** Users can reorder task lists by the dimension most useful to them — name, how often the task recurs, or how much effort it requires
|
||||
**Verified:** 2026-03-16T22:00:00Z
|
||||
**Status:** passed
|
||||
**Re-verification:** No — initial verification
|
||||
|
||||
---
|
||||
|
||||
## Goal Achievement
|
||||
|
||||
### Observable Truths
|
||||
|
||||
| # | Truth | Status | Evidence |
|
||||
|----|--------------------------------------------------------------------------------|------------|------------------------------------------------------------------------------------------------------|
|
||||
| 1 | Sort preference persists across app restarts | VERIFIED | `SortPreferenceNotifier._loadPersisted()` reads `SharedPreferences.getString('task_sort_option')` on build; 2 restart-recovery tests pass |
|
||||
| 2 | CalendarDayList tasks are sorted according to the active sort preference | VERIFIED | `calendarDayProvider` calls `ref.watch(sortPreferenceProvider)` and applies `_sortTasks(dayTasks, sortOption)` before returning `CalendarDayState` |
|
||||
| 3 | TaskListScreen tasks are sorted according to the active sort preference | VERIFIED | `tasksInRoomProvider` calls `ref.watch(sortPreferenceProvider)` and applies `stream.map((tasks) => _sortTasksRaw(tasks, sortOption))` |
|
||||
| 4 | Default sort is alphabetical (matches current CalendarDayList behavior) | VERIFIED | `SortPreferenceNotifier.build()` returns `TaskSortOption.alphabetical` synchronously; test "build() returns default state of alphabetical" confirms |
|
||||
| 5 | A sort dropdown is visible in the HomeScreen AppBar showing the current label | VERIFIED | `HomeScreen.build()` returns `Scaffold(appBar: AppBar(actions: const [SortDropdown()]))` — wired and rendered |
|
||||
| 6 | A sort dropdown is visible in the TaskListScreen AppBar | VERIFIED | `TaskListScreen.build()` AppBar actions list: `[const SortDropdown(), edit IconButton, delete IconButton]` |
|
||||
| 7 | Tapping the dropdown shows three options: A-Z, Intervall, Aufwand | VERIFIED | `SortDropdown` builds `PopupMenuButton` from `TaskSortOption.values` (3 items), labels map to `l10n.sortAlphabetical/sortInterval/sortEffort` |
|
||||
| 8 | Selecting a sort option updates the task list order immediately | VERIFIED | `onSelected` calls `ref.read(sortPreferenceProvider.notifier).setSortOption(value)`; providers watch `sortPreferenceProvider` and rebuild reactively |
|
||||
| 9 | The sort preference persists across screen navigations and app restarts | VERIFIED | `@Riverpod(keepAlive: true)` prevents disposal during navigation; SharedPreferences stores and reloads value |
|
||||
|
||||
**Score:** 9/9 truths verified
|
||||
|
||||
---
|
||||
|
||||
## Required Artifacts
|
||||
|
||||
### Plan 07-01 Artifacts
|
||||
|
||||
| Artifact | Expected | Status | Details |
|
||||
|----------|----------|--------|---------|
|
||||
| `lib/features/tasks/domain/task_sort_option.dart` | `TaskSortOption` enum with alphabetical, interval, effort | VERIFIED | Exactly 3 values, comments match intent. No stubs. |
|
||||
| `lib/features/tasks/presentation/sort_preference_notifier.dart` | `SortPreferenceNotifier` with SharedPreferences persistence | VERIFIED | `build()` returns `alphabetical` synchronously, `_loadPersisted()` async, `setSortOption()` sets state + persists. Pattern matches `ThemeNotifier`. |
|
||||
| `lib/features/tasks/presentation/sort_preference_notifier.g.dart` | Generated Riverpod provider file | VERIFIED | Generated correctly; `sortPreferenceProvider` declared as `SortPreferenceNotifierProvider._()` with `isAutoDispose: false` (keepAlive). |
|
||||
| `lib/features/home/presentation/calendar_providers.dart` | `calendarDayProvider` sorts `dayTasks` by active sort preference | VERIFIED | `ref.watch(sortPreferenceProvider)` present. `_sortTasks()` helper implements all 3 sort modes. `overdueTasks` intentionally unsorted. |
|
||||
| `lib/features/tasks/presentation/task_providers.dart` | `tasksInRoomProvider` sorts tasks by active sort preference | VERIFIED | `ref.watch(sortPreferenceProvider)` present. `_sortTasksRaw()` helper + `stream.map()` applied correctly. |
|
||||
| `test/features/tasks/presentation/sort_preference_notifier_test.dart` | Unit tests for sort preference persistence and default | VERIFIED | 7 tests: default alphabetical, setSortOption interval, setSortOption effort, persist to SharedPreferences, restart recovery (effort), restart recovery (interval), unknown value fallback. |
|
||||
|
||||
### Plan 07-02 Artifacts
|
||||
|
||||
| Artifact | Expected | Status | Details |
|
||||
|----------|----------|--------|---------|
|
||||
| `lib/features/tasks/presentation/sort_dropdown.dart` | Reusable `SortDropdown` `ConsumerWidget` | VERIFIED | `ConsumerWidget`, `PopupMenuButton<TaskSortOption>`, Opacity check mark pattern, `ref.watch` for display, `ref.read` for mutation, `_label()` helper. |
|
||||
| `lib/features/home/presentation/home_screen.dart` | HomeScreen with AppBar containing `SortDropdown` | VERIFIED | `Scaffold(appBar: AppBar(title: Text(l10n.tabHome), actions: const [SortDropdown()]))`. Existing Stack body preserved. |
|
||||
| `lib/features/tasks/presentation/task_list_screen.dart` | TaskListScreen AppBar with `SortDropdown` before edit/delete | VERIFIED | `actions: [const SortDropdown(), IconButton(edit), IconButton(delete)]`. Correct order. |
|
||||
|
||||
---
|
||||
|
||||
## Key Link Verification
|
||||
|
||||
### Plan 07-01 Key Links
|
||||
|
||||
| From | To | Via | Status | Details |
|
||||
|------|----|-----|--------|---------|
|
||||
| `calendar_providers.dart` | `sortPreferenceProvider` | `ref.watch(sortPreferenceProvider)` in `calendarDayProvider` | WIRED | Line 77: `final sortOption = ref.watch(sortPreferenceProvider);`. Applied at line 101: `dayTasks: _sortTasks(dayTasks, sortOption)`. |
|
||||
| `task_providers.dart` | `sortPreferenceProvider` | `ref.watch(sortPreferenceProvider)` in `tasksInRoomProvider` | WIRED | Line 43: `final sortOption = ref.watch(sortPreferenceProvider);`. Applied at lines 44-46 via `stream.map`. |
|
||||
|
||||
### Plan 07-02 Key Links
|
||||
|
||||
| From | To | Via | Status | Details |
|
||||
|------|----|-----|--------|---------|
|
||||
| `sort_dropdown.dart` | `sortPreferenceProvider` | `ref.watch` for display, `ref.read` for mutation | WIRED | Line 21: `ref.watch(sortPreferenceProvider)`. Line 27: `ref.read(sortPreferenceProvider.notifier).setSortOption(value)`. |
|
||||
| `home_screen.dart` | `sort_dropdown.dart` | `SortDropdown` widget in AppBar actions | WIRED | Import on line 7. Used in `AppBar(actions: const [SortDropdown()])` on line 37. |
|
||||
| `task_list_screen.dart` | `sort_dropdown.dart` | `SortDropdown` widget in AppBar actions | WIRED | Import on line 7. Used in `actions: [const SortDropdown(), ...]` on line 31. |
|
||||
|
||||
---
|
||||
|
||||
## Requirements Coverage
|
||||
|
||||
| Requirement | Source Plan | Description | Status | Evidence |
|
||||
|-------------|-------------|-------------|--------|----------|
|
||||
| SORT-01 | 07-01, 07-02 | User can sort tasks alphabetically | SATISFIED | `TaskSortOption.alphabetical` is the default. `_sortTasks()` and `_sortTasksRaw()` implement case-insensitive A-Z sort. `SortDropdown` displays "A–Z" label (l10n). |
|
||||
| SORT-02 | 07-01, 07-02 | User can sort tasks by frequency interval | SATISFIED | `TaskSortOption.interval` sort implemented: `intervalType.index` ascending with `intervalDays` tiebreaker. Displayed as "Intervall" in `SortDropdown`. |
|
||||
| SORT-03 | 07-01, 07-02 | User can sort tasks by effort level | SATISFIED | `TaskSortOption.effort` sort implemented: `effortLevel.index` ascending (low=0, medium=1, high=2). Displayed as "Aufwand" in `SortDropdown`. |
|
||||
|
||||
No orphaned requirements. All three SORT requirements are claimed by both plans and fully implemented.
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns Found
|
||||
|
||||
None. All seven implementation files scanned — no TODO, FIXME, XXX, HACK, PLACEHOLDER, return null, return {}, return [], or empty arrow functions found.
|
||||
|
||||
---
|
||||
|
||||
## Human Verification Required
|
||||
|
||||
### 1. Visual check: Sort dropdown appearance in AppBar
|
||||
|
||||
**Test:** Launch the app, navigate to HomeScreen. Verify the AppBar shows a sort icon (Icons.sort) followed by the current sort label text "A–Z".
|
||||
**Expected:** Sort icon and "A–Z" text visible in the top-right AppBar area.
|
||||
**Why human:** Widget rendering and visual layout cannot be verified programmatically.
|
||||
|
||||
### 2. Popup menu interaction: Check marks on active option
|
||||
|
||||
**Test:** Tap the sort dropdown, verify three items appear with a check mark next to the currently selected option and no check mark on the other two.
|
||||
**Expected:** Check mark visible on "A–Z" (default), invisible (but space-preserving) on "Intervall" and "Aufwand".
|
||||
**Why human:** Opacity(0) vs Opacity(1) rendering and visual alignment cannot be verified with grep.
|
||||
|
||||
### 3. Reactive reorder on selection
|
||||
|
||||
**Test:** With tasks loaded in HomeScreen, tap the sort dropdown and select "Aufwand". Verify the task list reorders immediately without a page reload.
|
||||
**Expected:** Task list updates instantly, sorted low-effort first.
|
||||
**Why human:** Real-time Riverpod reactive rebuild requires a running app to observe.
|
||||
|
||||
### 4. Cross-screen persistence of sort preference
|
||||
|
||||
**Test:** Select "Intervall" in HomeScreen, then navigate to a room's TaskListScreen. Verify the sort dropdown there also shows "Intervall".
|
||||
**Expected:** Sort preference is shared across screens (same `sortPreferenceProvider`, `keepAlive: true`).
|
||||
**Why human:** Cross-screen navigation state cannot be verified statically.
|
||||
|
||||
---
|
||||
|
||||
## Gaps Summary
|
||||
|
||||
None. All 9 observable truths verified. All artifacts exist, are substantive, and are correctly wired. All 3 requirements (SORT-01, SORT-02, SORT-03) are fully satisfied. All 5 commits (a9f2983, 13c7d62, 3697e4e, e5eccb7, a3e4d02) confirmed present in git log. No anti-patterns detected in implementation files.
|
||||
|
||||
The phase delivers its stated goal: users can reorder task lists by name (A–Z), frequency interval, or effort level via a persistent, reactive sort preference accessible from both HomeScreen and TaskListScreen AppBars.
|
||||
|
||||
---
|
||||
|
||||
_Verified: 2026-03-16T22:00:00Z_
|
||||
_Verifier: Claude (gsd-verifier)_
|
||||
54
CHANGELOG.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to HouseHoldKeeper are documented in this file.
|
||||
|
||||
## [1.1.4] - 2026-03-17
|
||||
|
||||
### Added
|
||||
- CI workflow for branch pushes and pull requests with static analysis, tests, security audit, and debug build
|
||||
- Security gate in release workflow — CI checks must pass before release build proceeds
|
||||
- F-Droid store icon (512x512) for en-US and de-DE metadata
|
||||
|
||||
## [1.1.3] - 2026-03-17
|
||||
|
||||
### Added
|
||||
- Custom app launcher icon — white house on sage green background
|
||||
- Adaptive icon support for Android 8+ (API 26)
|
||||
- Native splash screen with themed colors (beige light / brown dark)
|
||||
- Android 12+ splash screen with icon background
|
||||
- F-Droid metadata (de-DE, en-US) with screenshots and descriptions
|
||||
- F-Droid metadata copy step in release workflow
|
||||
|
||||
## [1.1.2] - 2026-03-17
|
||||
|
||||
### Changed
|
||||
- Release workflow now sets Flutter app version from Git tag automatically
|
||||
|
||||
## [1.1.1] - 2026-03-17
|
||||
|
||||
### Added
|
||||
- Integration tests for filtered and overdue task states in TaskListScreen
|
||||
|
||||
## [1.1.0] - 2026-03-17
|
||||
|
||||
### Added
|
||||
- Calendar strip on home screen with day-by-day task overview
|
||||
- Floating "Today" button for quick navigation
|
||||
- Task history sheet showing past completions per task
|
||||
- Task sorting by name, due date, or room with persistent preference
|
||||
- Sort dropdown in HomeScreen and TaskListScreen
|
||||
|
||||
### Changed
|
||||
- HomeScreen replaced with calendar-based composition
|
||||
|
||||
## [1.0.0] - 2026-03-16
|
||||
|
||||
### Added
|
||||
- Initial MVP release
|
||||
- Room management with drag-and-drop reordering
|
||||
- Task creation with templates and custom tasks
|
||||
- Recurring task scheduling (daily, weekly, monthly, yearly)
|
||||
- Local notifications for due tasks
|
||||
- German and English localization
|
||||
- Light and dark theme support
|
||||
- Local-only SQLite database (drift)
|
||||
1
CLAUDE.md
Normal file
@@ -0,0 +1 @@
|
||||
When asked to tag the current commit, your task is to ask the user whether they want it to be a patch, minor, or major release. Based on their response, you will create a git tag with the appropriate version number. Also update the CHANGELOG.md file with the new Version and the changes made since the last tag.
|
||||
86
LICENSE
@@ -1,73 +1,21 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
MIT License
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
Copyright (c) 2026 Jean-Luc Makiola
|
||||
|
||||
1. Definitions.
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
|
||||
|
||||
Copyright 2026 makiolaj
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
65
README.md
@@ -1,2 +1,65 @@
|
||||
# HouseHoldKeaper
|
||||
# Household Keeper
|
||||
|
||||
Your household, effortlessly organized.
|
||||
|
||||
Household Keeper helps you organize and manage your household tasks. Create rooms, assign tasks, set recurring reminders, and keep your home running smoothly.
|
||||
|
||||
## Features
|
||||
|
||||
- **Room Management** — Create and organize rooms with drag-and-drop reordering
|
||||
- **Task Templates** — Quickly add common household tasks or create your own
|
||||
- **Recurring Scheduling** — Daily, weekly, monthly, or yearly task recurrence
|
||||
- **Calendar View** — Day-by-day task overview with a floating "Today" button
|
||||
- **Task History** — View past completions for each task
|
||||
- **Task Sorting** — Sort by name, due date, or room with persistent preferences
|
||||
- **Notifications** — Local reminders for due tasks
|
||||
- **Light & Dark Theme** — Follows your system preference
|
||||
- **Localization** — German and English
|
||||
|
||||
## Screenshots
|
||||
|
||||
<p float="left">
|
||||
<img src="fdroid-metadata/de.jeanlucmakiola.household_keeper/en-US/phoneScreenshots/1_overview.png" width="200" />
|
||||
<img src="fdroid-metadata/de.jeanlucmakiola.household_keeper/en-US/phoneScreenshots/2_create_room.png" width="200" />
|
||||
<img src="fdroid-metadata/de.jeanlucmakiola.household_keeper/en-US/phoneScreenshots/3_task_templates.png" width="200" />
|
||||
<img src="fdroid-metadata/de.jeanlucmakiola.household_keeper/en-US/phoneScreenshots/4_room_tasks.png" width="200" />
|
||||
</p>
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Flutter** (SDK ^3.11.0)
|
||||
- **Riverpod** — State management
|
||||
- **Drift** — Local SQLite database
|
||||
- **GoRouter** — Navigation
|
||||
- **flutter_local_notifications** — Scheduled reminders
|
||||
|
||||
## Getting Started
|
||||
|
||||
```bash
|
||||
# Clone the repo
|
||||
git clone https://gitea.jeanlucmakiola.de/makiolaj/HouseHoldKeaper.git
|
||||
cd HouseHoldKeaper
|
||||
|
||||
# Install dependencies
|
||||
flutter pub get
|
||||
|
||||
# Generate code (drift, riverpod, l10n)
|
||||
dart run build_runner build --delete-conflicting-outputs
|
||||
|
||||
# Run the app
|
||||
flutter run
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
# Debug APK
|
||||
flutter build apk --debug
|
||||
|
||||
# Release APK (requires signing config)
|
||||
flutter build apk --release
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE) — Jean-Luc Makiola, 2026
|
||||
|
||||
BIN
android/app/src/main/res/drawable-hdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
BIN
android/app/src/main/res/drawable-hdpi/splash.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
android/app/src/main/res/drawable-mdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
BIN
android/app/src/main/res/drawable-mdpi/splash.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
android/app/src/main/res/drawable-night-hdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
android/app/src/main/res/drawable-night-hdpi/splash.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
android/app/src/main/res/drawable-night-mdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
android/app/src/main/res/drawable-night-mdpi/splash.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
android/app/src/main/res/drawable-night-v21/background.png
Normal file
|
After Width: | Height: | Size: 69 B |
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splash"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
BIN
android/app/src/main/res/drawable-night-xhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
BIN
android/app/src/main/res/drawable-night-xxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 11 KiB |
BIN
android/app/src/main/res/drawable-night-xxxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
android/app/src/main/res/drawable-night/background.png
Normal file
|
After Width: | Height: | Size: 69 B |
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splash"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
BIN
android/app/src/main/res/drawable-v21/background.png
Normal file
|
After Width: | Height: | Size: 69 B |
@@ -1,12 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splash"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
||||
BIN
android/app/src/main/res/drawable-xhdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
BIN
android/app/src/main/res/drawable-xhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
android/app/src/main/res/drawable-xxhdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
BIN
android/app/src/main/res/drawable-xxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
android/app/src/main/res/drawable-xxxhdpi/android12splash.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
BIN
android/app/src/main/res/drawable-xxxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
android/app/src/main/res/drawable/background.png
Normal file
|
After Width: | Height: | Size: 69 B |
@@ -1,12 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
<item>
|
||||
<bitmap android:gravity="center" android:src="@drawable/splash"/>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground>
|
||||
<inset
|
||||
android:drawable="@drawable/ic_launcher_foreground"
|
||||
android:inset="16%" />
|
||||
</foreground>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 896 B |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 537 B |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.7 KiB |
22
android/app/src/main/res/values-night-v31/styles.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
<item name="android:windowSplashScreenBackground">#2A2520</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
|
||||
<item name="android:windowSplashScreenIconBackgroundColor">#7A9A6D</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
@@ -5,6 +5,10 @@
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
|
||||
22
android/app/src/main/res/values-v31/styles.xml
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
<item name="android:windowSplashScreenBackground">#F5F0E8</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
|
||||
<item name="android:windowSplashScreenIconBackgroundColor">#7A9A6D</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
4
android/app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#7A9A6D</color>
|
||||
</resources>
|
||||
@@ -5,6 +5,10 @@
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
|
||||
BIN
assets/icon/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
assets/icon/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
10
fdroid-metadata/de.jeanlucmakiola.household_keeper.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
AuthorName: Jean-Luc Makiola
|
||||
License: MIT
|
||||
Name: Household Keeper
|
||||
|
||||
Categories:
|
||||
- System
|
||||
|
||||
#WebSite: https://git.jlmak.dev/jlmak/HouseHoldKeaper
|
||||
SourceCode: https://gitea.jeanlucmakiola.de/makiolaj/HouseHoldKeaper
|
||||
IssueTracker: https://gitea.jeanlucmakiola.de/makiolaj/HouseHoldKeaper/issues
|
||||
@@ -0,0 +1 @@
|
||||
Household Keeper hilft dir, deine Haushaltsaufgaben mühelos zu organisieren und zu verwalten. Erstelle Aufgaben, setze Erinnerungen und sorge dafür, dass dein Zuhause reibungslos läuft.
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 114 KiB |
@@ -0,0 +1 @@
|
||||
Dein Haushalt, entspannt organisiert.
|
||||
@@ -0,0 +1 @@
|
||||
Household Keeper helps you organize and manage your household tasks effortlessly. Create tasks, set reminders, and keep your home running smoothly.
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 114 KiB |
@@ -0,0 +1 @@
|
||||
Your household, effortlessly organized.
|
||||
@@ -55,6 +55,33 @@ class CalendarDao extends DatabaseAccessor<AppDatabase>
|
||||
return result.read(countExp) ?? 0;
|
||||
}
|
||||
|
||||
/// Watch tasks due on [date] within a specific [roomId].
|
||||
///
|
||||
/// Same as [watchTasksForDate] but filtered to a single room.
|
||||
Stream<List<TaskWithRoom>> watchTasksForDateInRoom(
|
||||
DateTime date, int roomId) {
|
||||
final startOfDay = DateTime(date.year, date.month, date.day);
|
||||
final endOfDay = startOfDay.add(const Duration(days: 1));
|
||||
|
||||
final query = select(tasks).join([
|
||||
innerJoin(rooms, rooms.id.equalsExp(tasks.roomId)),
|
||||
]);
|
||||
query.where(
|
||||
tasks.nextDueDate.isBiggerOrEqualValue(startOfDay) &
|
||||
tasks.nextDueDate.isSmallerThanValue(endOfDay) &
|
||||
tasks.roomId.equals(roomId),
|
||||
);
|
||||
query.orderBy([OrderingTerm.asc(tasks.name)]);
|
||||
|
||||
return query.watch().map((rows) {
|
||||
return rows.map((row) {
|
||||
final task = row.readTable(tasks);
|
||||
final room = row.readTable(rooms);
|
||||
return TaskWithRoom(task: task, roomName: room.name, roomId: room.id);
|
||||
}).toList();
|
||||
});
|
||||
}
|
||||
|
||||
/// Watch tasks whose [nextDueDate] is strictly before [referenceDate].
|
||||
///
|
||||
/// Returns tasks sorted by [nextDueDate] ascending (oldest first).
|
||||
@@ -84,4 +111,50 @@ class CalendarDao extends DatabaseAccessor<AppDatabase>
|
||||
}).toList();
|
||||
});
|
||||
}
|
||||
|
||||
/// Watch overdue tasks (before [referenceDate]) within a specific [roomId].
|
||||
///
|
||||
/// Same as [watchOverdueTasks] but filtered to a single room.
|
||||
Stream<List<TaskWithRoom>> watchOverdueTasksInRoom(
|
||||
DateTime referenceDate, int roomId) {
|
||||
final startOfReferenceDay = DateTime(
|
||||
referenceDate.year,
|
||||
referenceDate.month,
|
||||
referenceDate.day,
|
||||
);
|
||||
|
||||
final query = select(tasks).join([
|
||||
innerJoin(rooms, rooms.id.equalsExp(tasks.roomId)),
|
||||
]);
|
||||
query.where(
|
||||
tasks.nextDueDate.isSmallerThanValue(startOfReferenceDay) &
|
||||
tasks.roomId.equals(roomId),
|
||||
);
|
||||
query.orderBy([OrderingTerm.asc(tasks.nextDueDate)]);
|
||||
|
||||
return query.watch().map((rows) {
|
||||
return rows.map((row) {
|
||||
final task = row.readTable(tasks);
|
||||
final room = row.readTable(rooms);
|
||||
return TaskWithRoom(
|
||||
task: task,
|
||||
roomName: room.name,
|
||||
roomId: room.id,
|
||||
);
|
||||
}).toList();
|
||||
});
|
||||
}
|
||||
|
||||
/// Total task count within a specific room.
|
||||
///
|
||||
/// Used to distinguish first-run empty state from celebration state
|
||||
/// in the room calendar view.
|
||||
Future<int> getTaskCountInRoom(int roomId) async {
|
||||
final countExp = tasks.id.count();
|
||||
final query = selectOnly(tasks)
|
||||
..addColumns([countExp])
|
||||
..where(tasks.roomId.equals(roomId));
|
||||
final result = await query.getSingle();
|
||||
return result.read(countExp) ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ const _overdueColor = Color(0xFFE07A5F);
|
||||
|
||||
/// Shows the task list for the selected calendar day.
|
||||
///
|
||||
/// Watches [calendarDayProvider] and renders one of several states:
|
||||
/// Watches [calendarDayProvider] (or [roomCalendarDayProvider] when [roomId]
|
||||
/// is provided) and renders one of several states:
|
||||
/// - Loading spinner while data loads
|
||||
/// - Error text on failure
|
||||
/// - First-run empty state (no rooms/tasks at all) — prompts to create a room
|
||||
@@ -22,7 +23,10 @@ const _overdueColor = Color(0xFFE07A5F);
|
||||
/// - Celebration state (today is selected and all tasks are done)
|
||||
/// - Has-tasks state with optional overdue section (today only) and checkboxes
|
||||
class CalendarDayList extends ConsumerStatefulWidget {
|
||||
const CalendarDayList({super.key});
|
||||
const CalendarDayList({super.key, this.roomId});
|
||||
|
||||
/// When non-null, filters tasks to this room only.
|
||||
final int? roomId;
|
||||
|
||||
@override
|
||||
ConsumerState<CalendarDayList> createState() => _CalendarDayListState();
|
||||
@@ -43,7 +47,9 @@ class _CalendarDayListState extends ConsumerState<CalendarDayList> {
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final dayState = ref.watch(calendarDayProvider);
|
||||
final dayState = widget.roomId != null
|
||||
? ref.watch(roomCalendarDayProvider(widget.roomId!))
|
||||
: ref.watch(calendarDayProvider);
|
||||
|
||||
return dayState.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
@@ -96,6 +102,46 @@ class _CalendarDayListState extends ConsumerState<CalendarDayList> {
|
||||
AppLocalizations l10n,
|
||||
ThemeData theme,
|
||||
) {
|
||||
// Room-scoped: prompt to create a task in this room.
|
||||
if (widget.roomId != null) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.task_alt,
|
||||
size: 80,
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.4),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
l10n.taskEmptyTitle,
|
||||
style: theme.textTheme.headlineSmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.taskEmptyMessage,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FilledButton.tonal(
|
||||
onPressed: () =>
|
||||
context.go('/rooms/${widget.roomId}/tasks/new'),
|
||||
child: Text(l10n.taskEmptyAction),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Home-screen: prompt to create a room.
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
@@ -194,20 +240,36 @@ class _CalendarDayListState extends ConsumerState<CalendarDayList> {
|
||||
AppLocalizations l10n,
|
||||
ThemeData theme,
|
||||
) {
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
final isFuture = state.selectedDate.isAfter(today);
|
||||
final showRoomTag = widget.roomId == null;
|
||||
|
||||
final items = <Widget>[];
|
||||
|
||||
// Overdue section (today only, when overdue tasks exist).
|
||||
// Overdue tasks are always completable (they're past due, only shown on today).
|
||||
if (state.overdueTasks.isNotEmpty) {
|
||||
items.add(_buildSectionHeader(l10n.dailyPlanSectionOverdue, theme,
|
||||
color: _overdueColor));
|
||||
for (final tw in state.overdueTasks) {
|
||||
items.add(_buildAnimatedTaskRow(tw, isOverdue: true));
|
||||
items.add(_buildAnimatedTaskRow(
|
||||
tw,
|
||||
isOverdue: true,
|
||||
showRoomTag: showRoomTag,
|
||||
canComplete: true,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Day tasks section.
|
||||
for (final tw in state.dayTasks) {
|
||||
items.add(_buildAnimatedTaskRow(tw, isOverdue: false));
|
||||
items.add(_buildAnimatedTaskRow(
|
||||
tw,
|
||||
isOverdue: false,
|
||||
showRoomTag: showRoomTag,
|
||||
canComplete: !isFuture,
|
||||
));
|
||||
}
|
||||
|
||||
return ListView(children: items);
|
||||
@@ -227,7 +289,12 @@ class _CalendarDayListState extends ConsumerState<CalendarDayList> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAnimatedTaskRow(TaskWithRoom tw, {required bool isOverdue}) {
|
||||
Widget _buildAnimatedTaskRow(
|
||||
TaskWithRoom tw, {
|
||||
required bool isOverdue,
|
||||
required bool showRoomTag,
|
||||
required bool canComplete,
|
||||
}) {
|
||||
final isCompleting = _completingTaskIds.contains(tw.task.id);
|
||||
|
||||
if (isCompleting) {
|
||||
@@ -235,6 +302,7 @@ class _CalendarDayListState extends ConsumerState<CalendarDayList> {
|
||||
key: ValueKey('completing-${tw.task.id}'),
|
||||
taskWithRoom: tw,
|
||||
isOverdue: isOverdue,
|
||||
showRoomTag: showRoomTag,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -242,6 +310,8 @@ class _CalendarDayListState extends ConsumerState<CalendarDayList> {
|
||||
key: ValueKey('task-${tw.task.id}'),
|
||||
taskWithRoom: tw,
|
||||
isOverdue: isOverdue,
|
||||
showRoomTag: showRoomTag,
|
||||
canComplete: canComplete,
|
||||
onCompleted: () => _onTaskCompleted(tw.task.id),
|
||||
);
|
||||
}
|
||||
@@ -253,10 +323,12 @@ class _CompletingTaskRow extends StatefulWidget {
|
||||
super.key,
|
||||
required this.taskWithRoom,
|
||||
required this.isOverdue,
|
||||
required this.showRoomTag,
|
||||
});
|
||||
|
||||
final TaskWithRoom taskWithRoom;
|
||||
final bool isOverdue;
|
||||
final bool showRoomTag;
|
||||
|
||||
@override
|
||||
State<_CompletingTaskRow> createState() => _CompletingTaskRowState();
|
||||
@@ -302,6 +374,7 @@ class _CompletingTaskRowState extends State<_CompletingTaskRow>
|
||||
child: CalendarTaskRow(
|
||||
taskWithRoom: widget.taskWithRoom,
|
||||
isOverdue: widget.isOverdue,
|
||||
showRoomTag: widget.showRoomTag,
|
||||
onCompleted: () {}, // Already completing — ignore repeat taps.
|
||||
),
|
||||
),
|
||||
|
||||
@@ -3,6 +3,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:household_keeper/core/providers/database_provider.dart';
|
||||
import 'package:household_keeper/features/home/domain/calendar_models.dart';
|
||||
import 'package:household_keeper/features/home/domain/daily_plan_models.dart';
|
||||
import 'package:household_keeper/features/tasks/domain/task_sort_option.dart';
|
||||
import 'package:household_keeper/features/tasks/presentation/sort_preference_notifier.dart';
|
||||
|
||||
/// Notifier that manages the currently selected date in the calendar strip.
|
||||
///
|
||||
@@ -27,17 +29,52 @@ final selectedDateProvider =
|
||||
SelectedDateNotifier.new,
|
||||
);
|
||||
|
||||
/// Sort a list of [TaskWithRoom] by the given [sortOption].
|
||||
///
|
||||
/// Returns a new sorted list; never mutates the original.
|
||||
/// Only [dayTasks] are sorted — the overdue section stays in its existing
|
||||
/// order per user decision.
|
||||
List<TaskWithRoom> _sortTasks(
|
||||
List<TaskWithRoom> tasks,
|
||||
TaskSortOption sortOption,
|
||||
) {
|
||||
final sorted = List<TaskWithRoom>.from(tasks);
|
||||
switch (sortOption) {
|
||||
case TaskSortOption.alphabetical:
|
||||
sorted.sort((a, b) => a.task.name.toLowerCase().compareTo(
|
||||
b.task.name.toLowerCase(),
|
||||
));
|
||||
case TaskSortOption.interval:
|
||||
sorted.sort((a, b) {
|
||||
final cmp = a.task.intervalType.index.compareTo(
|
||||
b.task.intervalType.index,
|
||||
);
|
||||
if (cmp != 0) return cmp;
|
||||
return a.task.intervalDays.compareTo(b.task.intervalDays);
|
||||
});
|
||||
case TaskSortOption.effort:
|
||||
sorted.sort((a, b) => a.task.effortLevel.index.compareTo(
|
||||
b.task.effortLevel.index,
|
||||
));
|
||||
}
|
||||
return sorted;
|
||||
}
|
||||
|
||||
/// Reactive calendar day state: tasks for the selected date + overdue tasks.
|
||||
///
|
||||
/// Overdue tasks are only included when the selected date is today.
|
||||
/// Past and future dates show only tasks originally due on that day.
|
||||
///
|
||||
/// dayTasks are sorted in-memory according to the active [sortPreferenceProvider].
|
||||
/// overdueTasks retain their existing order (pinned at top, unsorted per design).
|
||||
///
|
||||
/// Defined manually (not @riverpod) because riverpod_generator has trouble
|
||||
/// with drift's generated [Task] type. Same pattern as [dailyPlanProvider].
|
||||
final calendarDayProvider =
|
||||
StreamProvider.autoDispose<CalendarDayState>((ref) {
|
||||
final db = ref.watch(appDatabaseProvider);
|
||||
final selectedDate = ref.watch(selectedDateProvider);
|
||||
final sortOption = ref.watch(sortPreferenceProvider);
|
||||
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
@@ -61,7 +98,47 @@ final calendarDayProvider =
|
||||
|
||||
return CalendarDayState(
|
||||
selectedDate: selectedDate,
|
||||
dayTasks: dayTasks,
|
||||
dayTasks: _sortTasks(dayTasks, sortOption),
|
||||
overdueTasks: overdueTasks,
|
||||
totalTaskCount: totalTaskCount,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
/// Room-scoped calendar day state: tasks for the selected date within a room.
|
||||
///
|
||||
/// Mirrors [calendarDayProvider] but filters by [roomId].
|
||||
/// Uses the shared [selectedDateProvider] so date selection is consistent
|
||||
/// across HomeScreen and room views.
|
||||
final roomCalendarDayProvider =
|
||||
StreamProvider.autoDispose.family<CalendarDayState, int>((ref, roomId) {
|
||||
final db = ref.watch(appDatabaseProvider);
|
||||
final selectedDate = ref.watch(selectedDateProvider);
|
||||
final sortOption = ref.watch(sortPreferenceProvider);
|
||||
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
final isToday = selectedDate == today;
|
||||
|
||||
final dayTasksStream =
|
||||
db.calendarDao.watchTasksForDateInRoom(selectedDate, roomId);
|
||||
|
||||
return dayTasksStream.asyncMap((dayTasks) async {
|
||||
final List<TaskWithRoom> overdueTasks;
|
||||
|
||||
if (isToday) {
|
||||
overdueTasks = await db.calendarDao
|
||||
.watchOverdueTasksInRoom(selectedDate, roomId)
|
||||
.first;
|
||||
} else {
|
||||
overdueTasks = const [];
|
||||
}
|
||||
|
||||
final totalTaskCount = await db.calendarDao.getTaskCountInRoom(roomId);
|
||||
|
||||
return CalendarDayState(
|
||||
selectedDate: selectedDate,
|
||||
dayTasks: _sortTasks(dayTasks, sortOption),
|
||||
overdueTasks: overdueTasks,
|
||||
totalTaskCount: totalTaskCount,
|
||||
);
|
||||
|
||||
@@ -20,6 +20,8 @@ class CalendarTaskRow extends StatelessWidget {
|
||||
required this.taskWithRoom,
|
||||
required this.onCompleted,
|
||||
this.isOverdue = false,
|
||||
this.showRoomTag = true,
|
||||
this.canComplete = true,
|
||||
});
|
||||
|
||||
final TaskWithRoom taskWithRoom;
|
||||
@@ -30,6 +32,12 @@ class CalendarTaskRow extends StatelessWidget {
|
||||
/// When true, task name is rendered in coral color.
|
||||
final bool isOverdue;
|
||||
|
||||
/// When false, the room tag subtitle is hidden (e.g. in room-scoped view).
|
||||
final bool showRoomTag;
|
||||
|
||||
/// When false, the checkbox is disabled (e.g. for future tasks).
|
||||
final bool canComplete;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
@@ -41,7 +49,7 @@ class CalendarTaskRow extends StatelessWidget {
|
||||
),
|
||||
leading: Checkbox(
|
||||
value: false,
|
||||
onChanged: (_) => onCompleted(),
|
||||
onChanged: canComplete ? (_) => onCompleted() : null,
|
||||
),
|
||||
title: Text(
|
||||
task.name,
|
||||
@@ -51,22 +59,25 @@ class CalendarTaskRow extends StatelessWidget {
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: GestureDetector(
|
||||
onTap: () => context.go('/rooms/${taskWithRoom.roomId}'),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
taskWithRoom.roomName,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: theme.colorScheme.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
subtitle: showRoomTag
|
||||
? GestureDetector(
|
||||
onTap: () => context.go('/rooms/${taskWithRoom.roomId}'),
|
||||
child: Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
taskWithRoom.roomName,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: theme.colorScheme.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:household_keeper/features/home/presentation/calendar_day_list.dart';
|
||||
import 'package:household_keeper/features/home/presentation/calendar_providers.dart';
|
||||
import 'package:household_keeper/features/home/presentation/calendar_strip.dart';
|
||||
import 'package:household_keeper/features/tasks/presentation/sort_dropdown.dart';
|
||||
import 'package:household_keeper/l10n/app_localizations.dart';
|
||||
|
||||
/// The app's primary screen: a horizontal calendar strip at the top with a
|
||||
@@ -30,40 +31,46 @@ class _HomeScreenState extends ConsumerState<HomeScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
CalendarStrip(
|
||||
controller: _stripController,
|
||||
onTodayVisibilityChanged: (visible) {
|
||||
setState(() => _showTodayButton = !visible);
|
||||
},
|
||||
),
|
||||
const Expanded(child: CalendarDayList()),
|
||||
],
|
||||
),
|
||||
if (_showTodayButton)
|
||||
Positioned(
|
||||
bottom: 16,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
ref
|
||||
.read(selectedDateProvider.notifier)
|
||||
.selectDate(today);
|
||||
_stripController.scrollToToday();
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(l10n.tabHome),
|
||||
actions: const [SortDropdown()],
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
CalendarStrip(
|
||||
controller: _stripController,
|
||||
onTodayVisibilityChanged: (visible) {
|
||||
setState(() => _showTodayButton = !visible);
|
||||
},
|
||||
icon: const Icon(Icons.today),
|
||||
label: Text(l10n.calendarTodayButton),
|
||||
),
|
||||
const Expanded(child: CalendarDayList()),
|
||||
],
|
||||
),
|
||||
if (_showTodayButton)
|
||||
Positioned(
|
||||
bottom: 16,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
ref
|
||||
.read(selectedDateProvider.notifier)
|
||||
.selectDate(today);
|
||||
_stripController.scrollToToday();
|
||||
},
|
||||
icon: const Icon(Icons.today),
|
||||
label: Text(l10n.calendarTodayButton),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||