Compare commits
1 Commits
88519f2de8
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 998f2be87f |
86
.gitea/workflows/release.yaml
Normal file
86
.gitea/workflows/release.yaml
Normal file
@@ -0,0 +1,86 @@
|
||||
name: Build and Release to F-Droid
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
|
||||
- name: Setup Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: '3.11.0'
|
||||
channel: 'stable'
|
||||
|
||||
- name: Install dependencies
|
||||
run: flutter pub get
|
||||
|
||||
# ADD THIS NEW STEP
|
||||
- name: Setup Android Keystore
|
||||
env:
|
||||
KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
|
||||
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
||||
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
||||
run: |
|
||||
# Decode the base64 string back into the binary .jks file
|
||||
echo "$KEYSTORE_BASE64" | base64 --decode > android/app/upload-keystore.jks
|
||||
|
||||
# Create the key.properties file that build.gradle expects
|
||||
echo "storePassword=$KEY_PASSWORD" > android/key.properties
|
||||
echo "keyPassword=$KEY_PASSWORD" >> android/key.properties
|
||||
echo "keyAlias=$KEY_ALIAS" >> android/key.properties
|
||||
echo "storeFile=upload-keystore.jks" >> android/key.properties
|
||||
|
||||
- name: Build APK
|
||||
run: flutter build apk --release
|
||||
|
||||
- name: Setup F-Droid Server Tools
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y fdroidserver sshpass
|
||||
|
||||
- name: Initialize or fetch F-Droid Repository
|
||||
env:
|
||||
HOST: ${{ secrets.HETZNER_HOST }}
|
||||
USER: ${{ secrets.HETZNER_USER }}
|
||||
PASS: ${{ secrets.HETZNER_PASS }}
|
||||
run: |
|
||||
mkdir -p fdroid
|
||||
cd fdroid
|
||||
|
||||
# Try to download the existing repo/ folder from Hetzner to keep older versions and the keystore
|
||||
# If it fails (first time), we just initialize a new one
|
||||
sshpass -p "$PASS" scp -o StrictHostKeyChecking=no -r $USER@$HOST:dev/fdroid/repo . || fdroid init
|
||||
|
||||
- name: Copy new APK to repo
|
||||
run: |
|
||||
# The app-release.apk name should ideally include the version number
|
||||
# so it doesn't overwrite older versions in the repo.
|
||||
VERSION_TAG=${GITHUB_REF#refs/tags/} # gets 'v1.0.0'
|
||||
cp build/app/outputs/flutter-apk/app-release.apk fdroid/repo/my_flutter_app_${VERSION_TAG}.apk
|
||||
|
||||
- name: Generate F-Droid Index
|
||||
run: |
|
||||
cd fdroid
|
||||
fdroid update -c
|
||||
|
||||
- name: Upload Repo to Hetzner
|
||||
env:
|
||||
HOST: ${{ secrets.HETZNER_HOST }}
|
||||
USER: ${{ secrets.HETZNER_USER }}
|
||||
PASS: ${{ secrets.HETZNER_PASS }}
|
||||
run: |
|
||||
# Use rsync to efficiently upload only the changed files (the new APK and updated index files)
|
||||
sshpass -p "$PASS" rsync -avz -e "ssh -o StrictHostKeyChecking=no" fdroid/repo/ $USER@$HOST:dev/fdroid/repo/
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -118,3 +118,5 @@ app.*.symbols
|
||||
!**/ios/**/default.perspectivev3
|
||||
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
|
||||
!/dev/ci/**/Gemfile.lock
|
||||
|
||||
.idea
|
||||
@@ -3,11 +3,16 @@
|
||||
"granularity": "coarse",
|
||||
"parallelization": true,
|
||||
"commit_docs": true,
|
||||
"model_profile": "quality",
|
||||
"model_profile": "balanced",
|
||||
"workflow": {
|
||||
"research": true,
|
||||
"plan_check": true,
|
||||
"verifier": true,
|
||||
"nyquist_validation": true
|
||||
"nyquist_validation": true,
|
||||
"auto_advance": true,
|
||||
"_auto_chain_active": true
|
||||
},
|
||||
"git": {
|
||||
"branching_strategy": "none"
|
||||
}
|
||||
}
|
||||
117
.planning/phases/02-rooms-and-tasks/2-CONTEXT.md
Normal file
117
.planning/phases/02-rooms-and-tasks/2-CONTEXT.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# Phase 2: Rooms and Tasks - Context
|
||||
|
||||
**Gathered:** 2026-03-15
|
||||
**Status:** Ready for planning
|
||||
|
||||
<domain>
|
||||
## Phase Boundary
|
||||
|
||||
Users can create and manage rooms and tasks, mark tasks done, and trust the app to schedule the next occurrence automatically. Delivers: room CRUD with icons and reorder, task CRUD with frequency intervals and effort levels, task completion with auto-scheduling, bundled German-language task templates for 14 room types, overdue highlighting, and room cards with cleanliness indicators.
|
||||
|
||||
Requirements: ROOM-01, ROOM-02, ROOM-03, ROOM-04, ROOM-05, TASK-01, TASK-02, TASK-03, TASK-04, TASK-05, TASK-06, TASK-07, TASK-08, TMPL-01, TMPL-02
|
||||
|
||||
</domain>
|
||||
|
||||
<decisions>
|
||||
## Implementation Decisions
|
||||
|
||||
### Room cards & layout
|
||||
- **2-column grid** layout on the Rooms screen — compact cards, shows more rooms at once
|
||||
- Each card shows: **room icon, room name, count of due/overdue tasks, thin cleanliness progress bar**
|
||||
- No next-task preview or total task count on cards — keep them clean
|
||||
- **Cleanliness indicator**: thin horizontal progress bar at bottom of card, fill color shifts green→yellow→red based on ratio of on-time to overdue tasks
|
||||
- **Icon picker**: curated grid of ~20-30 hand-picked household Material Icons in a bottom sheet. No full icon search — focused and simple
|
||||
- Cards support drag-and-drop reorder (ROOM-04)
|
||||
- Delete room with confirmation dialog that warns about cascade deletion of all tasks (ROOM-03)
|
||||
|
||||
### Task completion & overdue
|
||||
- **Leading checkbox** on each task row to mark done — tap to toggle. No swipe gesture.
|
||||
- Tapping the task row (not the checkbox) opens task detail/edit
|
||||
- **Overdue visual**: due date text turns warm red/coral color. Rest of row stays normal — subtle but clear
|
||||
- **No undo** on completion — immediate and final. Records timestamp, auto-calculates next due date
|
||||
- **Task row info**: task name, relative due date (e.g. "Heute", "in 3 Tagen", "Überfällig"), and frequency label (e.g. "Wöchentlich", "Alle 3 Tage"). No effort indicator or description preview on list view
|
||||
- Tasks within a room sorted by due date (default sort order, TASK-06)
|
||||
|
||||
### Template selection flow
|
||||
- **Post-creation prompt**: user creates a room first (name + icon), then gets prompted "Aufgaben aus Vorlagen hinzufügen?" with template selection
|
||||
- **Room type is optional** — used only to determine which templates to suggest. Not stored as a permanent field. If no matching room type is detected, no template prompt appears
|
||||
- **All templates unchecked** by default — user explicitly checks what they want. No pre-selection
|
||||
- Users can create fully custom rooms (name + icon only) with no template prompt if no room type matches
|
||||
- Templates cover all 14 room types from TMPL-02: Küche, Badezimmer, Schlafzimmer, Wohnzimmer, Flur, Büro, Garage, Balkon, Waschküche, Keller, Kinderzimmer, Gästezimmer, Esszimmer, Garten/Außenbereich
|
||||
- Templates are bundled in the app as static data (German language)
|
||||
|
||||
### Scheduling & recurrence
|
||||
- **Two interval categories** with different behavior:
|
||||
- **Day-count intervals** (daily, every N days, weekly, biweekly): add N days from due date. Pure arithmetic, no clamping.
|
||||
- **Calendar-anchored intervals** (monthly, quarterly, every N months, yearly): anchor to original day-of-month. If the month is shorter, clamp to last day of month but remember the anchor (e.g. task set for the 31st: Jan 31 → Feb 28 → Mar 31)
|
||||
- **Next due calculated from original due date**, not completion date — keeps rhythm stable even when completed late
|
||||
- **Catch-up on very late completion**: if calculated next due is in the past, keep adding intervals until next due is today or in the future. No stacking of missed occurrences
|
||||
- **Custom intervals**: user picks a number + unit (Tage/Wochen/Monate). E.g. "Alle 10 Tage" or "Alle 3 Monate"
|
||||
- **Preset intervals** from TASK-04: daily, every 2 days, every 3 days, weekly, biweekly, monthly, every 2 months, quarterly, every 6 months, yearly, custom
|
||||
- All due dates stored as date-only (calendar day) — established in Phase 1 pre-decision
|
||||
|
||||
### Claude's Discretion
|
||||
- Room creation form layout (full screen vs bottom sheet vs dialog)
|
||||
- Task creation/edit form layout and field ordering
|
||||
- Exact Material Icons chosen for the curated icon picker set
|
||||
- Drag-and-drop reorder implementation approach (ReorderableListView vs custom)
|
||||
- Delete confirmation dialog design
|
||||
- Animation on task completion (checkbox fill, row transition)
|
||||
- Template data structure and storage format (Dart constants vs JSON asset)
|
||||
- Exact color values for overdue red/coral (within the sage & stone palette)
|
||||
- Empty state design for rooms with no tasks (following Phase 1 playful tone)
|
||||
|
||||
</decisions>
|
||||
|
||||
<specifics>
|
||||
## Specific Ideas
|
||||
|
||||
- Room type detection for templates should be lightweight — match room name against known types, don't force a classification step
|
||||
- The template prompt after room creation should feel like a helpful suggestion, not a required step — easy to dismiss
|
||||
- Overdue text color should be warm (coral/terracotta) not harsh alarm-red — fits the calm sage & stone palette
|
||||
- Relative due date labels in German: "Heute", "Morgen", "in X Tagen", "Überfällig seit X Tagen"
|
||||
- The cleanliness bar should be subtle — thin, at the bottom edge of the card, not a dominant visual element
|
||||
- Checkbox interaction should feel instant — no loading spinners, optimistic UI
|
||||
|
||||
</specifics>
|
||||
|
||||
<code_context>
|
||||
## Existing Code Insights
|
||||
|
||||
### Reusable Assets
|
||||
- `AppDatabase` (`lib/core/database/database.dart`): Drift database with schema v1, currently no tables — Phase 2 adds Room, Task, and TaskCompletion tables
|
||||
- `appDatabaseProvider` (`lib/core/providers/database_provider.dart`): Riverpod provider with `keepAlive: true` and `ref.onDispose(db.close)` — all DAOs will reference this
|
||||
- `ThemeNotifier` pattern (`lib/core/theme/theme_provider.dart`): AsyncNotifier with SharedPreferences persistence — template for room/task notifiers
|
||||
- `SettingsScreen` (`lib/features/settings/presentation/settings_screen.dart`): ConsumerWidget with `ref.watch` + `ref.read(...notifier)` pattern — template for reactive screens
|
||||
- `RoomsScreen` placeholder (`lib/features/rooms/presentation/rooms_screen.dart`): Ready to replace with actual room grid
|
||||
- `app_de.arb` (`lib/l10n/app_de.arb`): Localization file with 18 existing keys — Phase 2 adds room/task/frequency/effort strings
|
||||
|
||||
### Established Patterns
|
||||
- **Riverpod 3 code generation**: `@riverpod` annotation + `.g.dart` files via build_runner. Functional providers for reads, class-based AsyncNotifier for mutations
|
||||
- **Clean architecture**: `features/X/data/domain/presentation` layer structure. Presentation never imports directly from data layer
|
||||
- **GoRouter StatefulShellRoute**: `/rooms` branch exists, ready for nested routes (`/rooms/:roomId`, `/rooms/:roomId/tasks/new`)
|
||||
- **Material 3 theming**: `ColorScheme.fromSeed` with sage green seed (0xFF7A9A6D), warm stone surfaces. All color via `Theme.of(context).colorScheme`
|
||||
- **Localization**: ARB-based, German-only, strongly typed `AppLocalizations.of(context).keyName`
|
||||
- **Database testing**: `NativeDatabase.memory()` for in-memory tests, `setUp/tearDown` pattern
|
||||
- **Widget testing**: `ProviderScope` + `MaterialApp.router` with German locale
|
||||
|
||||
### Integration Points
|
||||
- Phase 2 replaces the `RoomsScreen` placeholder with the actual room grid
|
||||
- Room cards link to room detail screens via GoRouter nested routes under `/rooms`
|
||||
- Task completion data feeds Phase 3's daily plan view (overdue/today/upcoming grouping)
|
||||
- Cleanliness indicator logic established here is reused by Phase 3 room cards on the Home screen
|
||||
- Phase 4 notifications query task due dates established in this phase's schema
|
||||
|
||||
</code_context>
|
||||
|
||||
<deferred>
|
||||
## Deferred Ideas
|
||||
|
||||
None — discussion stayed within phase scope
|
||||
|
||||
</deferred>
|
||||
|
||||
---
|
||||
|
||||
*Phase: 02-rooms-and-tasks*
|
||||
*Context gathered: 2026-03-15*
|
||||
@@ -6,8 +6,8 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.jlmak.household_keeper"
|
||||
compileSdk = 35
|
||||
namespace = "de.jeanlucmakiola.household_keeper"
|
||||
compileSdk = 36
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
@@ -22,7 +22,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId = "com.jlmak.household_keeper"
|
||||
applicationId = "de.jeanlucmakiola.household_keeper"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
@@ -31,11 +31,25 @@ android {
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
def keystorePropertiesFile = rootProject.file("key.properties")
|
||||
def keystoreProperties = new Properties()
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
keyAlias = keystoreProperties['keyAlias']
|
||||
keyPassword = keystoreProperties['keyPassword']
|
||||
storeFile = keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
|
||||
storePassword = keystoreProperties['storePassword']
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
signingConfig = signingConfigs.release
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
18
household_keeper.iml
Normal file
18
household_keeper.iml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/lib" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.idea" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.pub" />
|
||||
</content>
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="Dart SDK" level="project" />
|
||||
<orderEntry type="library" name="Flutter Plugins" level="project" />
|
||||
<orderEntry type="library" name="Dart Packages" level="project" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -10,8 +10,8 @@ import 'package:household_keeper/core/notifications/notification_service.dart';
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
tz.initializeTimeZones();
|
||||
final timeZoneName = await FlutterTimezone.getLocalTimezone();
|
||||
tz.setLocalLocation(tz.getLocation(timeZoneName));
|
||||
final timeZone = await FlutterTimezone.getLocalTimezone();
|
||||
tz.setLocalLocation(tz.getLocation(timeZone.identifier));
|
||||
await NotificationService().initialize();
|
||||
runApp(const ProviderScope(child: App()));
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ dependencies:
|
||||
flutter_reorderable_grid_view: ^5.6.0
|
||||
flutter_local_notifications: ^21.0.0
|
||||
timezone: ^0.11.0
|
||||
flutter_timezone: ^1.0.8
|
||||
flutter_timezone: ^5.0.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
24
test/drift/household_keeper/generated/schema.dart
Normal file
24
test/drift/household_keeper/generated/schema.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
// dart format width=80
|
||||
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||
// ignore_for_file: type=lint,unused_import
|
||||
//
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/internal/migrations.dart';
|
||||
import 'schema_v1.dart' as v1;
|
||||
import 'schema_v2.dart' as v2;
|
||||
|
||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
@override
|
||||
GeneratedDatabase databaseForVersion(QueryExecutor db, int version) {
|
||||
switch (version) {
|
||||
case 1:
|
||||
return v1.DatabaseAtV1(db);
|
||||
case 2:
|
||||
return v2.DatabaseAtV2(db);
|
||||
default:
|
||||
throw MissingSchemaException(version, versions);
|
||||
}
|
||||
}
|
||||
|
||||
static const versions = const [1, 2];
|
||||
}
|
||||
16
test/drift/household_keeper/generated/schema_v1.dart
Normal file
16
test/drift/household_keeper/generated/schema_v1.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
// dart format width=80
|
||||
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||
// ignore_for_file: type=lint,unused_import
|
||||
//
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
class DatabaseAtV1 extends GeneratedDatabase {
|
||||
DatabaseAtV1(QueryExecutor e) : super(e);
|
||||
@override
|
||||
Iterable<TableInfo<Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||
@override
|
||||
List<DatabaseSchemaEntity> get allSchemaEntities => [];
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
}
|
||||
1034
test/drift/household_keeper/generated/schema_v2.dart
Normal file
1034
test/drift/household_keeper/generated/schema_v2.dart
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user