diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml new file mode 100644 index 0000000..f21a3ff --- /dev/null +++ b/.gitea/workflows/release.yaml @@ -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/ diff --git a/.gitignore b/.gitignore index 8b26114..5eb6816 100644 --- a/.gitignore +++ b/.gitignore @@ -118,3 +118,5 @@ app.*.symbols !**/ios/**/default.perspectivev3 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages !/dev/ci/**/Gemfile.lock + +.idea \ No newline at end of file diff --git a/.planning/config.json b/.planning/config.json index 8286c6c..bdd5e98 100644 --- a/.planning/config.json +++ b/.planning/config.json @@ -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" } -} +} \ No newline at end of file diff --git a/.planning/phases/02-rooms-and-tasks/2-CONTEXT.md b/.planning/phases/02-rooms-and-tasks/2-CONTEXT.md new file mode 100644 index 0000000..d40724f --- /dev/null +++ b/.planning/phases/02-rooms-and-tasks/2-CONTEXT.md @@ -0,0 +1,117 @@ +# Phase 2: Rooms and Tasks - Context + +**Gathered:** 2026-03-15 +**Status:** Ready for planning + + +## 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 + + + + +## 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) + + + + +## 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 + + + + +## 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 + + + + +## Deferred Ideas + +None — discussion stayed within phase scope + + + +--- + +*Phase: 02-rooms-and-tasks* +*Context gathered: 2026-03-15* diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 839716f..63ede81 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -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 } } } diff --git a/household_keeper.iml b/household_keeper.iml new file mode 100644 index 0000000..4d723b3 --- /dev/null +++ b/household_keeper.iml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 4685db9..fec7090 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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())); } diff --git a/pubspec.yaml b/pubspec.yaml index 9b4d372..315391e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: diff --git a/test/drift/household_keeper/generated/schema.dart b/test/drift/household_keeper/generated/schema.dart new file mode 100644 index 0000000..f5e870e --- /dev/null +++ b/test/drift/household_keeper/generated/schema.dart @@ -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]; +} diff --git a/test/drift/household_keeper/generated/schema_v1.dart b/test/drift/household_keeper/generated/schema_v1.dart new file mode 100644 index 0000000..b3dc862 --- /dev/null +++ b/test/drift/household_keeper/generated/schema_v1.dart @@ -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> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => []; + @override + int get schemaVersion => 1; +} diff --git a/test/drift/household_keeper/generated/schema_v2.dart b/test/drift/household_keeper/generated/schema_v2.dart new file mode 100644 index 0000000..c65fd8d --- /dev/null +++ b/test/drift/household_keeper/generated/schema_v2.dart @@ -0,0 +1,1034 @@ +// dart format width=80 +// GENERATED BY drift_dev, DO NOT MODIFY. +// ignore_for_file: type=lint,unused_import +// +import 'package:drift/drift.dart'; + +class Rooms extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Rooms(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT', + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn iconName = GeneratedColumn( + 'icon_name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn sortOrder = GeneratedColumn( + 'sort_order', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 0', + defaultValue: const CustomExpression('0'), + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [ + id, + name, + iconName, + sortOrder, + createdAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'rooms'; + @override + Set get $primaryKey => {id}; + @override + RoomsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return RoomsData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + iconName: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}icon_name'], + )!, + sortOrder: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}sort_order'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + ); + } + + @override + Rooms createAlias(String alias) { + return Rooms(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class RoomsData extends DataClass implements Insertable { + final int id; + final String name; + final String iconName; + final int sortOrder; + final int createdAt; + const RoomsData({ + required this.id, + required this.name, + required this.iconName, + required this.sortOrder, + required this.createdAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['icon_name'] = Variable(iconName); + map['sort_order'] = Variable(sortOrder); + map['created_at'] = Variable(createdAt); + return map; + } + + RoomsCompanion toCompanion(bool nullToAbsent) { + return RoomsCompanion( + id: Value(id), + name: Value(name), + iconName: Value(iconName), + sortOrder: Value(sortOrder), + createdAt: Value(createdAt), + ); + } + + factory RoomsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return RoomsData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + iconName: serializer.fromJson(json['iconName']), + sortOrder: serializer.fromJson(json['sortOrder']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'iconName': serializer.toJson(iconName), + 'sortOrder': serializer.toJson(sortOrder), + 'createdAt': serializer.toJson(createdAt), + }; + } + + RoomsData copyWith({ + int? id, + String? name, + String? iconName, + int? sortOrder, + int? createdAt, + }) => RoomsData( + id: id ?? this.id, + name: name ?? this.name, + iconName: iconName ?? this.iconName, + sortOrder: sortOrder ?? this.sortOrder, + createdAt: createdAt ?? this.createdAt, + ); + RoomsData copyWithCompanion(RoomsCompanion data) { + return RoomsData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + iconName: data.iconName.present ? data.iconName.value : this.iconName, + sortOrder: data.sortOrder.present ? data.sortOrder.value : this.sortOrder, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('RoomsData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('iconName: $iconName, ') + ..write('sortOrder: $sortOrder, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, name, iconName, sortOrder, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is RoomsData && + other.id == this.id && + other.name == this.name && + other.iconName == this.iconName && + other.sortOrder == this.sortOrder && + other.createdAt == this.createdAt); +} + +class RoomsCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value iconName; + final Value sortOrder; + final Value createdAt; + const RoomsCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.iconName = const Value.absent(), + this.sortOrder = const Value.absent(), + this.createdAt = const Value.absent(), + }); + RoomsCompanion.insert({ + this.id = const Value.absent(), + required String name, + required String iconName, + this.sortOrder = const Value.absent(), + required int createdAt, + }) : name = Value(name), + iconName = Value(iconName), + createdAt = Value(createdAt); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? iconName, + Expression? sortOrder, + Expression? createdAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (iconName != null) 'icon_name': iconName, + if (sortOrder != null) 'sort_order': sortOrder, + if (createdAt != null) 'created_at': createdAt, + }); + } + + RoomsCompanion copyWith({ + Value? id, + Value? name, + Value? iconName, + Value? sortOrder, + Value? createdAt, + }) { + return RoomsCompanion( + id: id ?? this.id, + name: name ?? this.name, + iconName: iconName ?? this.iconName, + sortOrder: sortOrder ?? this.sortOrder, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (iconName.present) { + map['icon_name'] = Variable(iconName.value); + } + if (sortOrder.present) { + map['sort_order'] = Variable(sortOrder.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RoomsCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('iconName: $iconName, ') + ..write('sortOrder: $sortOrder, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + +class Tasks extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + Tasks(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT', + ); + late final GeneratedColumn roomId = GeneratedColumn( + 'room_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL REFERENCES rooms(id)', + ); + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn description = GeneratedColumn( + 'description', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn intervalType = GeneratedColumn( + 'interval_type', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn intervalDays = GeneratedColumn( + 'interval_days', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL DEFAULT 1', + defaultValue: const CustomExpression('1'), + ); + late final GeneratedColumn anchorDay = GeneratedColumn( + 'anchor_day', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NULL', + ); + late final GeneratedColumn effortLevel = GeneratedColumn( + 'effort_level', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn nextDueDate = GeneratedColumn( + 'next_due_date', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [ + id, + roomId, + name, + description, + intervalType, + intervalDays, + anchorDay, + effortLevel, + nextDueDate, + createdAt, + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'tasks'; + @override + Set get $primaryKey => {id}; + @override + TasksData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TasksData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + roomId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}room_id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + description: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}description'], + ), + intervalType: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}interval_type'], + )!, + intervalDays: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}interval_days'], + )!, + anchorDay: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}anchor_day'], + ), + effortLevel: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}effort_level'], + )!, + nextDueDate: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}next_due_date'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}created_at'], + )!, + ); + } + + @override + Tasks createAlias(String alias) { + return Tasks(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class TasksData extends DataClass implements Insertable { + final int id; + final int roomId; + final String name; + final String? description; + final int intervalType; + final int intervalDays; + final int? anchorDay; + final int effortLevel; + final int nextDueDate; + final int createdAt; + const TasksData({ + required this.id, + required this.roomId, + required this.name, + this.description, + required this.intervalType, + required this.intervalDays, + this.anchorDay, + required this.effortLevel, + required this.nextDueDate, + required this.createdAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['room_id'] = Variable(roomId); + map['name'] = Variable(name); + if (!nullToAbsent || description != null) { + map['description'] = Variable(description); + } + map['interval_type'] = Variable(intervalType); + map['interval_days'] = Variable(intervalDays); + if (!nullToAbsent || anchorDay != null) { + map['anchor_day'] = Variable(anchorDay); + } + map['effort_level'] = Variable(effortLevel); + map['next_due_date'] = Variable(nextDueDate); + map['created_at'] = Variable(createdAt); + return map; + } + + TasksCompanion toCompanion(bool nullToAbsent) { + return TasksCompanion( + id: Value(id), + roomId: Value(roomId), + name: Value(name), + description: description == null && nullToAbsent + ? const Value.absent() + : Value(description), + intervalType: Value(intervalType), + intervalDays: Value(intervalDays), + anchorDay: anchorDay == null && nullToAbsent + ? const Value.absent() + : Value(anchorDay), + effortLevel: Value(effortLevel), + nextDueDate: Value(nextDueDate), + createdAt: Value(createdAt), + ); + } + + factory TasksData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TasksData( + id: serializer.fromJson(json['id']), + roomId: serializer.fromJson(json['roomId']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + intervalType: serializer.fromJson(json['intervalType']), + intervalDays: serializer.fromJson(json['intervalDays']), + anchorDay: serializer.fromJson(json['anchorDay']), + effortLevel: serializer.fromJson(json['effortLevel']), + nextDueDate: serializer.fromJson(json['nextDueDate']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'roomId': serializer.toJson(roomId), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'intervalType': serializer.toJson(intervalType), + 'intervalDays': serializer.toJson(intervalDays), + 'anchorDay': serializer.toJson(anchorDay), + 'effortLevel': serializer.toJson(effortLevel), + 'nextDueDate': serializer.toJson(nextDueDate), + 'createdAt': serializer.toJson(createdAt), + }; + } + + TasksData copyWith({ + int? id, + int? roomId, + String? name, + Value description = const Value.absent(), + int? intervalType, + int? intervalDays, + Value anchorDay = const Value.absent(), + int? effortLevel, + int? nextDueDate, + int? createdAt, + }) => TasksData( + id: id ?? this.id, + roomId: roomId ?? this.roomId, + name: name ?? this.name, + description: description.present ? description.value : this.description, + intervalType: intervalType ?? this.intervalType, + intervalDays: intervalDays ?? this.intervalDays, + anchorDay: anchorDay.present ? anchorDay.value : this.anchorDay, + effortLevel: effortLevel ?? this.effortLevel, + nextDueDate: nextDueDate ?? this.nextDueDate, + createdAt: createdAt ?? this.createdAt, + ); + TasksData copyWithCompanion(TasksCompanion data) { + return TasksData( + id: data.id.present ? data.id.value : this.id, + roomId: data.roomId.present ? data.roomId.value : this.roomId, + name: data.name.present ? data.name.value : this.name, + description: data.description.present + ? data.description.value + : this.description, + intervalType: data.intervalType.present + ? data.intervalType.value + : this.intervalType, + intervalDays: data.intervalDays.present + ? data.intervalDays.value + : this.intervalDays, + anchorDay: data.anchorDay.present ? data.anchorDay.value : this.anchorDay, + effortLevel: data.effortLevel.present + ? data.effortLevel.value + : this.effortLevel, + nextDueDate: data.nextDueDate.present + ? data.nextDueDate.value + : this.nextDueDate, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('TasksData(') + ..write('id: $id, ') + ..write('roomId: $roomId, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('intervalType: $intervalType, ') + ..write('intervalDays: $intervalDays, ') + ..write('anchorDay: $anchorDay, ') + ..write('effortLevel: $effortLevel, ') + ..write('nextDueDate: $nextDueDate, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + id, + roomId, + name, + description, + intervalType, + intervalDays, + anchorDay, + effortLevel, + nextDueDate, + createdAt, + ); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TasksData && + other.id == this.id && + other.roomId == this.roomId && + other.name == this.name && + other.description == this.description && + other.intervalType == this.intervalType && + other.intervalDays == this.intervalDays && + other.anchorDay == this.anchorDay && + other.effortLevel == this.effortLevel && + other.nextDueDate == this.nextDueDate && + other.createdAt == this.createdAt); +} + +class TasksCompanion extends UpdateCompanion { + final Value id; + final Value roomId; + final Value name; + final Value description; + final Value intervalType; + final Value intervalDays; + final Value anchorDay; + final Value effortLevel; + final Value nextDueDate; + final Value createdAt; + const TasksCompanion({ + this.id = const Value.absent(), + this.roomId = const Value.absent(), + this.name = const Value.absent(), + this.description = const Value.absent(), + this.intervalType = const Value.absent(), + this.intervalDays = const Value.absent(), + this.anchorDay = const Value.absent(), + this.effortLevel = const Value.absent(), + this.nextDueDate = const Value.absent(), + this.createdAt = const Value.absent(), + }); + TasksCompanion.insert({ + this.id = const Value.absent(), + required int roomId, + required String name, + this.description = const Value.absent(), + required int intervalType, + this.intervalDays = const Value.absent(), + this.anchorDay = const Value.absent(), + required int effortLevel, + required int nextDueDate, + required int createdAt, + }) : roomId = Value(roomId), + name = Value(name), + intervalType = Value(intervalType), + effortLevel = Value(effortLevel), + nextDueDate = Value(nextDueDate), + createdAt = Value(createdAt); + static Insertable custom({ + Expression? id, + Expression? roomId, + Expression? name, + Expression? description, + Expression? intervalType, + Expression? intervalDays, + Expression? anchorDay, + Expression? effortLevel, + Expression? nextDueDate, + Expression? createdAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (roomId != null) 'room_id': roomId, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (intervalType != null) 'interval_type': intervalType, + if (intervalDays != null) 'interval_days': intervalDays, + if (anchorDay != null) 'anchor_day': anchorDay, + if (effortLevel != null) 'effort_level': effortLevel, + if (nextDueDate != null) 'next_due_date': nextDueDate, + if (createdAt != null) 'created_at': createdAt, + }); + } + + TasksCompanion copyWith({ + Value? id, + Value? roomId, + Value? name, + Value? description, + Value? intervalType, + Value? intervalDays, + Value? anchorDay, + Value? effortLevel, + Value? nextDueDate, + Value? createdAt, + }) { + return TasksCompanion( + id: id ?? this.id, + roomId: roomId ?? this.roomId, + name: name ?? this.name, + description: description ?? this.description, + intervalType: intervalType ?? this.intervalType, + intervalDays: intervalDays ?? this.intervalDays, + anchorDay: anchorDay ?? this.anchorDay, + effortLevel: effortLevel ?? this.effortLevel, + nextDueDate: nextDueDate ?? this.nextDueDate, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (roomId.present) { + map['room_id'] = Variable(roomId.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + if (intervalType.present) { + map['interval_type'] = Variable(intervalType.value); + } + if (intervalDays.present) { + map['interval_days'] = Variable(intervalDays.value); + } + if (anchorDay.present) { + map['anchor_day'] = Variable(anchorDay.value); + } + if (effortLevel.present) { + map['effort_level'] = Variable(effortLevel.value); + } + if (nextDueDate.present) { + map['next_due_date'] = Variable(nextDueDate.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TasksCompanion(') + ..write('id: $id, ') + ..write('roomId: $roomId, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('intervalType: $intervalType, ') + ..write('intervalDays: $intervalDays, ') + ..write('anchorDay: $anchorDay, ') + ..write('effortLevel: $effortLevel, ') + ..write('nextDueDate: $nextDueDate, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } +} + +class TaskCompletions extends Table + with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + TaskCompletions(this.attachedDatabase, [this._alias]); + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'NOT NULL PRIMARY KEY AUTOINCREMENT', + ); + late final GeneratedColumn taskId = GeneratedColumn( + 'task_id', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL REFERENCES tasks(id)', + ); + late final GeneratedColumn completedAt = GeneratedColumn( + 'completed_at', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL', + ); + @override + List get $columns => [id, taskId, completedAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'task_completions'; + @override + Set get $primaryKey => {id}; + @override + TaskCompletionsData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TaskCompletionsData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}id'], + )!, + taskId: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}task_id'], + )!, + completedAt: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}completed_at'], + )!, + ); + } + + @override + TaskCompletions createAlias(String alias) { + return TaskCompletions(attachedDatabase, alias); + } + + @override + bool get dontWriteConstraints => true; +} + +class TaskCompletionsData extends DataClass + implements Insertable { + final int id; + final int taskId; + final int completedAt; + const TaskCompletionsData({ + required this.id, + required this.taskId, + required this.completedAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['task_id'] = Variable(taskId); + map['completed_at'] = Variable(completedAt); + return map; + } + + TaskCompletionsCompanion toCompanion(bool nullToAbsent) { + return TaskCompletionsCompanion( + id: Value(id), + taskId: Value(taskId), + completedAt: Value(completedAt), + ); + } + + factory TaskCompletionsData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TaskCompletionsData( + id: serializer.fromJson(json['id']), + taskId: serializer.fromJson(json['taskId']), + completedAt: serializer.fromJson(json['completedAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'taskId': serializer.toJson(taskId), + 'completedAt': serializer.toJson(completedAt), + }; + } + + TaskCompletionsData copyWith({int? id, int? taskId, int? completedAt}) => + TaskCompletionsData( + id: id ?? this.id, + taskId: taskId ?? this.taskId, + completedAt: completedAt ?? this.completedAt, + ); + TaskCompletionsData copyWithCompanion(TaskCompletionsCompanion data) { + return TaskCompletionsData( + id: data.id.present ? data.id.value : this.id, + taskId: data.taskId.present ? data.taskId.value : this.taskId, + completedAt: data.completedAt.present + ? data.completedAt.value + : this.completedAt, + ); + } + + @override + String toString() { + return (StringBuffer('TaskCompletionsData(') + ..write('id: $id, ') + ..write('taskId: $taskId, ') + ..write('completedAt: $completedAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, taskId, completedAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TaskCompletionsData && + other.id == this.id && + other.taskId == this.taskId && + other.completedAt == this.completedAt); +} + +class TaskCompletionsCompanion extends UpdateCompanion { + final Value id; + final Value taskId; + final Value completedAt; + const TaskCompletionsCompanion({ + this.id = const Value.absent(), + this.taskId = const Value.absent(), + this.completedAt = const Value.absent(), + }); + TaskCompletionsCompanion.insert({ + this.id = const Value.absent(), + required int taskId, + required int completedAt, + }) : taskId = Value(taskId), + completedAt = Value(completedAt); + static Insertable custom({ + Expression? id, + Expression? taskId, + Expression? completedAt, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (taskId != null) 'task_id': taskId, + if (completedAt != null) 'completed_at': completedAt, + }); + } + + TaskCompletionsCompanion copyWith({ + Value? id, + Value? taskId, + Value? completedAt, + }) { + return TaskCompletionsCompanion( + id: id ?? this.id, + taskId: taskId ?? this.taskId, + completedAt: completedAt ?? this.completedAt, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (taskId.present) { + map['task_id'] = Variable(taskId.value); + } + if (completedAt.present) { + map['completed_at'] = Variable(completedAt.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TaskCompletionsCompanion(') + ..write('id: $id, ') + ..write('taskId: $taskId, ') + ..write('completedAt: $completedAt') + ..write(')')) + .toString(); + } +} + +class DatabaseAtV2 extends GeneratedDatabase { + DatabaseAtV2(QueryExecutor e) : super(e); + late final Rooms rooms = Rooms(this); + late final Tasks tasks = Tasks(this); + late final TaskCompletions taskCompletions = TaskCompletions(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + rooms, + tasks, + taskCompletions, + ]; + @override + int get schemaVersion => 2; +}