initial commit

This commit is contained in:
dp 2026-08-03 21:51:48 +02:00
commit 641b90e321
222 changed files with 17715 additions and 0 deletions

46
.ai/architecture.md Normal file
View file

@ -0,0 +1,46 @@
# Architecture — Decisions & Rationale
## ADR-001: Flutter as framework
**Decision:** Flutter (not React Native or native).
**Reason:** Single codebase for Android + iOS + Web; Dart type safety;
strong Material 3 support; flet-managed Flutter SDK already present.
## ADR-002: Riverpod for state management
**Decision:** Riverpod with code generation (`riverpod_annotation`).
**Reason:** Compile-time safety; no BuildContext required for providers;
excellent async support (AsyncNotifier); easy testing via ProviderContainer overrides.
Rejected: Bloc (too verbose), Provider (deprecated patterns), GetX (opinionated anti-patterns).
## ADR-003: Drift (formerly Moor) for local database
**Decision:** Drift + sqlite3_flutter_libs.
**Reason:** Type-safe SQL in Dart; reactive streams out of the box; strong
migration support; works on all Flutter platforms incl. Web (via WASM).
Rejected: Hive (no relations), Isar (less mature migration story).
## ADR-004: Offline-first, no backend
**Decision:** All data stored locally on device.
**Reason:** Privacy, no auth complexity, works without internet.
Future: optional cloud sync (Supabase/Firebase) can be added as a separate
`sync` feature without touching existing data layer.
## ADR-005: go_router for navigation
**Decision:** go_router with StatefulShellRoute.
**Reason:** Official Flutter navigation package; deep link support; URL-based
routing works for Web target; StatefulShellRoute preserves bottom-nav state.
## ADR-006: Feature-first directory structure
**Decision:** `lib/features/<name>/{data,domain,presentation}/`
**Reason:** Features can be developed and reviewed in isolation; clear
ownership; easier to add/remove features without touching unrelated code.
Each feature's `data/` holds repositories and DAOs, `domain/` holds models
and providers, `presentation/` holds screens and widgets.
## ADR-007: freezed for domain models
**Decision:** All domain models use `freezed`.
**Reason:** Immutability enforced at compile time; `copyWith` generated;
`==` and `hashCode` correct; union types for state (e.g. TimerState).
## ADR-008: Multilingual from day one
**Decision:** `flutter_localizations` + ARB files, no hardcoded strings.
**Reason:** Avoids costly retro-fitting; supports en + de initially;
easily extensible by adding new ARB files.

81
.ai/database.md Normal file
View file

@ -0,0 +1,81 @@
# Database — Drift Schema & Conventions
## Tables
### projects
| Column | Type | Notes |
|--------------|-----------|------------------------------|
| id | INTEGER | PK, autoincrement |
| name | TEXT | NOT NULL, unique |
| color_value | INTEGER | NOT NULL (ARGB int) |
| description | TEXT | nullable |
| archived_at | DATETIME | nullable; null = active |
| created_at | DATETIME | NOT NULL, default now |
### time_entries
| Column | Type | Notes |
|-------------|----------|--------------------------------------|
| id | INTEGER | PK, autoincrement |
| project_id | INTEGER | FK → projects.id, NOT NULL |
| start_time | DATETIME | NOT NULL |
| end_time | DATETIME | nullable; null = timer still running |
| duration_s | INTEGER | seconds; computed on stop |
| note | TEXT | nullable |
| created_at | DATETIME | NOT NULL, default now |
### tags
| Column | Type | Notes |
|--------|---------|-------------------|
| id | INTEGER | PK, autoincrement |
| name | TEXT | NOT NULL, unique |
### time_entry_tags (junction)
| Column | Type | Notes |
|---------------|---------|--------------------|
| time_entry_id | INTEGER | FK → time_entries |
| tag_id | INTEGER | FK → tags |
| PRIMARY KEY (time_entry_id, tag_id) | | |
## Drift Setup
```dart
// lib/core/database/app_database.dart
@DriftDatabase(tables: [Projects, TimeEntries, Tags, TimeEntryTags])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
}
LazyDatabase _openConnection() {
return LazyDatabase(() async {
final dir = await getApplicationDocumentsDirectory();
final file = File(p.join(dir.path, 'timetrack.db'));
return NativeDatabase.createInBackground(file);
});
}
```
## DAOs
- `ProjectsDao` — CRUD for projects, watchAll(), watchActive()
- `TimeEntriesDao` — CRUD, watchByProject(), watchByDateRange(), getActiveEntry()
- `TagsDao` — CRUD, watchAll()
## Migration Strategy
- Schema version starts at `1`
- Increment `schemaVersion` for every breaking change
- Add a `MigrationStrategy` with `onUpgrade` steps
- Never drop columns — use nullable columns for additions
- Test migrations with `drift_dev` schema tests
## Riverpod Provider
```dart
@Riverpod(keepAlive: true)
AppDatabase appDatabase(AppDatabaseRef ref) {
final db = AppDatabase();
ref.onDispose(db.close);
return db;
}
```

55
.ai/features/export.md Normal file
View file

@ -0,0 +1,55 @@
# Feature: Export
## Supported Formats
| Format | Package | Use case |
|--------|--------------|----------------------------------|
| CSV | `csv` | Spreadsheet import (Excel, etc.) |
| PDF | `pdf` | Printable report |
| JSON | `dart:convert` | Backup / data portability |
## CSV Schema
```
id,project,start_time,end_time,duration_seconds,note,tags
42,My Project,2026-07-01T08:00:00,2026-07-01T10:30:00,9000,"Standup",planning|backend
```
- Date format: ISO 8601
- Tags: pipe-separated within the cell
- Encoding: UTF-8 with BOM for Excel compatibility
## PDF Layout
1. Header: App name + export date + selected period
2. Summary table: Project | Total hours | % of total
3. Entries table: Date | Project | Duration | Note | Tags
4. Footer: Generated by Timetrack
Use `pw.Document` from the `pdf` package.
Render with `pw.Table`, `pw.Text`, `pw.Chart` (optional).
## JSON Schema
```json
{
"exportedAt": "2026-07-12T10:00:00Z",
"version": 1,
"projects": [ { "id": 1, "name": "...", "color": -16711936 } ],
"tags": [ { "id": 1, "name": "backend" } ],
"entries": [
{
"id": 42, "projectId": 1, "startTime": "...", "endTime": "...",
"durationSeconds": 9000, "note": "...", "tagIds": [1]
}
]
}
```
JSON export can be used as a full backup and re-imported in a future version.
## Share Flow
```dart
final file = await _buildExportFile(format); // write to temp dir
await Share.shareXFiles([XFile(file.path)], // share_plus
subject: 'Timetrack Export ${DateFormat.yMd().format(DateTime.now())}');
```
Always write to `getTemporaryDirectory()` — never to arbitrary paths.
## Export Scope
- User selects: date range + format + optional project filter
- Available from Settings screen and Reports screen (share button)

52
.ai/features/reports.md Normal file
View file

@ -0,0 +1,52 @@
# Feature: Reports
## Views
| View | Period | Grouping |
|---------|---------------|-----------------------|
| Daily | Selected day | Per entry (list) |
| Weekly | MonSun | Per day (bar chart) |
| Monthly | Calendar month| Per week (bar chart) |
## Chart Data Format (fl_chart BarChart)
```dart
// Weekly: 7 bars (Mon=0 … Sun=6)
List<BarChartGroupData> weeklyBars(Map<int, Duration> durationByWeekday) {
return List.generate(7, (i) => BarChartGroupData(
x: i,
barRods: [BarChartRodData(toY: durationByWeekday[i]?.inMinutes.toDouble() ?? 0)],
));
}
```
## Key Queries (Drift)
```dart
// Total duration per project in date range
SELECT project_id, SUM(duration_s) AS total
FROM time_entries
WHERE start_time >= :from AND start_time < :to
AND end_time IS NOT NULL
GROUP BY project_id;
// Duration per weekday in week
SELECT strftime('%w', start_time) AS weekday, SUM(duration_s) AS total
FROM time_entries
WHERE start_time >= :weekStart AND start_time < :weekEnd
AND end_time IS NOT NULL
GROUP BY weekday;
```
## ReportsNotifier
- Exposes `selectedPeriod` (day/week/month) and `selectedDate`
- Provides `AsyncValue<ReportData>` via Drift watch stream
- `ReportData` contains: total duration, per-project breakdown, chart bars
## Filters
- Filter by project (optional, default: all projects)
- Filter by tag (optional)
- Date navigation: previous/next period buttons
## Summary Cards
Each report view shows:
- Total tracked time for period
- Most tracked project
- Average daily hours (weekly/monthly view)

49
.ai/features/timer.md Normal file
View file

@ -0,0 +1,49 @@
# Feature: Timer
## Behaviour
- Only **one** active timer at a time.
- Starting a new timer while one is running automatically stops the previous one
and saves the completed `TimeEntry`.
- Timer state persists across app restarts: on launch, read `time_entries`
where `end_time IS NULL` — if found, restore the running timer.
## State Machine
```
IDLE ──start()──► RUNNING ──stop()──► IDLE
discard()
IDLE
```
## TimerState (freezed union)
```dart
@freezed
class TimerState with _$TimerState {
const factory TimerState.idle() = TimerIdle;
const factory TimerState.running({
required int entryId,
required DateTime startTime,
required Project project,
String? note,
}) = TimerRunning;
}
```
## TimerNotifier responsibilities
1. `start(Project project)` — create TimeEntry (end_time = null), emit running
2. `stop()` — update TimeEntry (set end_time, compute duration_s), emit idle
3. `discard()` — delete the active TimeEntry, emit idle
4. `updateNote(String note)` — update note on active entry
5. `restoreFromDb()` — called on app init; check for open entry
## Elapsed Time Display
- Use a `Timer.periodic(1 second)` inside the notifier while running.
- Dispose the periodic timer on `stop()` / `discard()`.
- Widget reads `DateTime.now().difference(startTime)` for display.
## No background service (v1)
- Timer only ticks while app is in foreground.
- If app is killed mid-session, the entry remains open (end_time = null).
- On next launch, `restoreFromDb()` re-attaches to the open entry.
- Future v2: consider `flutter_foreground_task` for background timer.

81
.ai/testing.md Normal file
View file

@ -0,0 +1,81 @@
# Testing — Conventions & Patterns
## Structure
Test files mirror `lib/` under `test/`:
```
test/
features/
timer/
timer_repository_test.dart
timer_notifier_test.dart
entries/
entries_repository_test.dart
projects/
projects_repository_test.dart
reports/
reports_repository_test.dart
core/
database/
app_database_test.dart
```
## What to Test
| Layer | Test type | Tool |
|--------------|---------------|-------------------|
| Repository | Unit | mocktail + drift in-memory |
| Notifier | Unit | ProviderContainer |
| Screen | Widget | flutter_test |
| Navigation | Widget | GoRouter test helpers |
## Repository Tests — Pattern
Use an **in-memory** Drift database:
```dart
AppDatabase createTestDb() => AppDatabase(NativeDatabase.memory());
```
Test every public DAO method: insert, update, delete, watch (stream).
## Notifier Tests — Pattern
Use `ProviderContainer` with overridden repository:
```dart
final container = ProviderContainer(overrides: [
projectsRepositoryProvider.overrideWithValue(MockProjectsRepository()),
]);
addTearDown(container.dispose);
```
## Widget Tests — Pattern
```dart
await tester.pumpWidget(
ProviderScope(
overrides: [/* mock providers */],
child: const MaterialApp(home: TimerScreen()),
),
);
```
- Verify key widgets are present
- Tap interactions and verify state changes
- Use `mocktail` `when()` / `verify()` for interactions
## Mocktail Conventions
```dart
class MockTimerRepository extends Mock implements TimerRepository {}
// Setup
when(() => mock.getActiveEntry()).thenAnswer((_) async => null);
// Verify
verify(() => mock.stopTimer(any())).called(1);
```
## Coverage Goal
- Repository methods: 100%
- Notifiers/providers: >80%
- Widget tests: at least smoke test per screen
## Running Tests
```bash
flutter test # all tests
flutter test test/features/timer/ # single feature
flutter test --coverage # with coverage report
```

105
.ai/tickets.md Normal file
View file

@ -0,0 +1,105 @@
# Ticket Workflow
Jede Aufgabe aus einem Implementierungsplan wird als eigenständiges Ticket unter `.tasks/` abgelegt.
Tickets folgen dem Jira-Stil (Story, Task, Bug, Spike) und durchlaufen einen festen Lebenszyklus.
## Verzeichnisstruktur
```
.tasks/
todo/ ← Backlog: fertig beschrieben, noch nicht begonnen
processing/ ← Aktiv in Bearbeitung (max. 12 gleichzeitig)
done/ ← Abgeschlossen (zur Referenz aufbewahren)
```
## Ticket-Lebenszyklus
```
todo/ → processing/ → done/ → nächstes Ticket
```
Die `.md`-Datei wird beim Statuswechsel physisch in den entsprechenden Ordner verschoben.
Niemals den Status nur im Dateiinhalt ändern — der Ordner **ist** der Status.
## Ticket-ID-Konvention
```
TT-<dreistellige Nummer>-<kurzer-slug>.md
```
Beispiel: `TT-018-quick-access-grid.md`
Die nächste freie Nummer ermitteln:
```bash
ls .tasks/done/ .tasks/processing/ .tasks/todo/ | grep -oP 'TT-\d+' | sort -t- -k2 -n | tail -1
```
## Ticket-Template
```markdown
# TT-XXX — Titel
**Type:** Story | Task | Bug | Spike
**Priority:** Highest | High | Medium | Low
**Labels:** komma, getrennt
**Depends on:** TT-XXX, …
**Blocks:** TT-XXX, …
---
## Summary
Ein Absatz: Was wird gemacht und warum.
## Background
Kontext, der zum Verständnis nötig ist (optional).
## Acceptance Criteria
- [ ] …
## Steps
1. …
## Files to create / modify
- `lib/…`
## Notes
Hinweise, Fallstricke, verwandte Tickets (optional).
```
## Regeln für den Agenten
1. **Implementierungsplan → Tickets**: Jeder Schritt eines Plans wird zu einem eigenen Ticket in `.tasks/todo/`.
2. **Ein Ticket auf einmal**: Immer nur ein Ticket nach `processing/` verschieben — erst abschließen, dann das nächste beginnen.
3. **Ticket schließen**: Datei nach `done/` verschieben, bevor das nächste Ticket geöffnet wird.
4. **Keine impliziten Aufgaben**: Alles, was getan wird, muss einem Ticket zugeordnet sein. Entsteht Arbeit ad-hoc, zuerst ein Ticket anlegen.
5. **Abhängigkeiten respektieren**: `Depends on`-Felder beachten — blockierte Tickets nicht vor ihren Voraussetzungen beginnen.
6. **`.tasks/README.md` aktualisieren**: Nach jedem neuen Ticket die Übersichtstabelle dort ergänzen.
## Beispiel-Workflow
```
# 1. Implementierungsplan analysieren → Tickets anlegen
touch .tasks/todo/TT-018-quick-access-grid.md
touch .tasks/todo/TT-019-frequent-projects-provider.md
# 2. Erstes Ticket in Bearbeitung nehmen
mv .tasks/todo/TT-018-quick-access-grid.md .tasks/processing/
# 3. Implementierung durchführen …
# 4. Ticket abschließen
mv .tasks/processing/TT-018-quick-access-grid.md .tasks/done/
# 5. Nächstes Ticket beginnen
mv .tasks/todo/TT-019-frequent-projects-provider.md .tasks/processing/
```
## Verwandte Dateien
| Datei | Inhalt |
|---|---|
| `.tasks/README.md` | Übersichtstabelle aller Tickets mit aktuellem Status |
| `.tasks/todo/` | Offene Tickets |
| `.tasks/processing/` | Tickets in Bearbeitung |
| `.tasks/done/` | Abgeschlossene Tickets (Referenz) |

48
.gitignore vendored Normal file
View file

@ -0,0 +1,48 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
#lock files
*.lock

30
.metadata Normal file
View file

@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "cc0734ac716fbb8b90f3f9db8020958b1553afa7"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: cc0734ac716fbb8b90f3f9db8020958b1553afa7
base_revision: cc0734ac716fbb8b90f3f9db8020958b1553afa7
- platform: linux
create_revision: cc0734ac716fbb8b90f3f9db8020958b1553afa7
base_revision: cc0734ac716fbb8b90f3f9db8020958b1553afa7
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

77
.tasks/README.md Normal file
View file

@ -0,0 +1,77 @@
# .tasks — Ticket System
Lightweight ticket system for the Timetrack project.
Tickets follow the same conventions as Jira stories/tasks.
## Folder Structure
```
.tasks/
todo/ ← backlog: ready to be picked up
processing/ ← in active development (max. 12 at a time)
done/ ← completed tickets (keep for reference)
```
## Ticket Lifecycle
```
todo/ → processing/ → done/
```
Move the `.md` file to the corresponding folder when status changes.
## Ticket ID Convention
`TT-<number>-<short-slug>.md`
Example: `TT-006-timer-feature-ui.md`
## Ticket Template
```markdown
# TT-XXX — Title
**Type:** Story | Task | Bug | Spike
**Priority:** Highest | High | Medium | Low
**Labels:** comma, separated
**Depends on:** TT-XXX, …
**Blocks:** TT-XXX, …
---
## Summary
One paragraph describing what and why.
## Acceptance Criteria
- [ ] …
## Files to create / modify
- `lib/…`
```
## Current Tickets
| ID | Title | Status |
|----|-------|--------|
| TT-001 | Dependencies Setup & Build Runner | ✅ done |
| TT-002 | Drift Database Schema | ✅ done |
| TT-003 | Domain Models (freezed) | ✅ done |
| TT-004 | Repository Implementations | ✅ done |
| TT-005 | Riverpod Providers & Notifiers | ✅ done |
| TT-006 | Timer Feature UI | ✅ done |
| TT-007 | Entries Feature UI | ✅ done |
| TT-008 | Projects Feature UI | ✅ done |
| TT-009 | Reports Feature UI | ✅ done |
| TT-010 | Settings & Export Feature | ✅ done |
| TT-011 | Localisation (i18n) | ✅ done |
| TT-012 | Unit Tests | ✅ done |
| TT-014 | Go APK-Serve-Binary | ✅ done |
| TT-015 | APK-Retention-Script | ✅ done |
| TT-016 | Makefile erstellen | ✅ done |
| TT-017 | Integration-Test | ✅ done |
| TT-018 | Quick-Access Grid: Fallback auf erste N Projekte | ✅ done |
| TT-019 | Quick-Access Grid: Auffüllen mit ältesten Projekten | ✅ done |
| TT-020 | Bug: RangeError in frequentProjects beim Sortieren | ✅ done |
| TT-018 | Go-Server: --web Flag für Static-File-Serving | ✅ done |
| TT-019 | Makefile: serve_web Target + WEB_PORT Variable | ✅ done |
| TT-020 | Makefile: build_and_serve_apk + build_and_serve_web | ✅ done |

View file

@ -0,0 +1,36 @@
# TT-001 — Dependencies Setup & Build Runner
**Type:** Task
**Priority:** Highest
**Labels:** setup, infrastructure
**Depends on:** —
**Blocks:** TT-002, TT-003
---
## Summary
Run `flutter pub get` to install all declared dependencies and verify the
project compiles. Set up `build_runner` as the code-generation pipeline for
`drift`, `freezed`, and `riverpod_generator`.
## Background
`pubspec.yaml` has been populated with all required packages but `pub get`
has not been run yet. Generated files (`*.g.dart`, `*.freezed.dart`) do not
exist and will cause compile errors until `build_runner` has run at least once.
## Acceptance Criteria
- [ ] `flutter pub get` exits with code 0, no resolution conflicts
- [ ] `dart run build_runner build --delete-conflicting-outputs` exits with code 0
- [ ] `flutter analyze` reports zero errors (warnings acceptable for stubs)
- [ ] `flutter build apk --debug` (or `flutter build web`) succeeds
## Steps
1. `flutter pub get`
2. `dart run build_runner build --delete-conflicting-outputs`
3. `flutter analyze`
4. Fix any immediate compile errors in generated stubs
## Notes
- If version conflicts arise, prefer the newer package and update `pubspec.yaml`
- `app_router.g.dart` will fail until `@riverpod` annotation resolves —
this is expected and will be fixed as part of this ticket

View file

@ -0,0 +1,50 @@
# TT-002 — Drift Database Schema
**Type:** Story
**Priority:** Highest
**Labels:** database, infrastructure
**Depends on:** TT-001
**Blocks:** TT-004, TT-005
---
## Summary
Implement the full Drift database schema in `lib/core/database/app_database.dart`
including all table definitions, DAOs, and the database connection provider.
## Background
The Drift schema is documented in `.ai/database.md`. The placeholder file
`lib/core/database/app_database.dart` exists but contains no implementation.
## Data Model
### Tables
| Table | Key columns |
|-------------------|----------------------------------------------------------|
| `projects` | id, name, color_value, description, archived_at, created_at |
| `time_entries` | id, project_id (FK), start_time, end_time, duration_s, note, created_at |
| `tags` | id, name |
| `time_entry_tags` | time_entry_id (FK), tag_id (FK) — composite PK |
## Acceptance Criteria
- [ ] All 4 tables defined as Drift `Table` classes
- [ ] Foreign key constraints declared and enforced
- [ ] `AppDatabase` annotated with `@DriftDatabase(tables: [...])`
- [ ] `_openConnection()` uses `getApplicationDocumentsDirectory()` + `NativeDatabase.createInBackground`
- [ ] `schemaVersion` = 1, empty `MigrationStrategy` scaffold in place
- [ ] Three DAOs created: `ProjectsDao`, `TimeEntriesDao`, `TagsDao`
- [ ] Each DAO has at minimum: `watchAll()`, `insert()`, `update()`, `delete()`
- [ ] `TimeEntriesDao.getActiveEntry()` returns entry where `end_time IS NULL`
- [ ] Riverpod `@Riverpod(keepAlive: true)` provider for `AppDatabase`
- [ ] `dart run build_runner build` generates schema without errors
- [ ] Unit test: insert + read a `Project` using in-memory DB
## Files to create / modify
- `lib/core/database/app_database.dart` — full implementation
- `lib/core/database/daos/projects_dao.dart`
- `lib/core/database/daos/time_entries_dao.dart`
- `lib/core/database/daos/tags_dao.dart`
- `test/core/database/app_database_test.dart`
## Notes
- See `.ai/database.md` for full schema details
- Use `NativeDatabase.memory()` in tests — never the real file DB

View file

@ -0,0 +1,100 @@
# TT-003 — Domain Models (freezed)
**Type:** Story
**Priority:** High
**Labels:** domain, models
**Depends on:** TT-001
**Blocks:** TT-004, TT-005, TT-006, TT-007, TT-008
---
## Summary
Define all domain-level models using `freezed`. These are the immutable data
classes used throughout the app's business logic and UI layer — distinct from
the Drift table companion classes.
## Models to implement
### `Project`
```dart
// lib/features/projects/domain/project.dart
@freezed
class Project with _$Project {
const factory Project({
required int id,
required String name,
required int colorValue,
String? description,
DateTime? archivedAt,
required DateTime createdAt,
}) = _Project;
factory Project.fromJson(Map<String, dynamic> json) => _$ProjectFromJson(json);
}
```
### `TimeEntry`
```dart
// lib/features/entries/domain/time_entry.dart
@freezed
class TimeEntry with _$TimeEntry {
const factory TimeEntry({
required int id,
required int projectId,
required DateTime startTime,
DateTime? endTime,
int? durationSeconds,
String? note,
@Default([]) List<String> tags,
required DateTime createdAt,
}) = _TimeEntry;
factory TimeEntry.fromJson(Map<String, dynamic> json) => _$TimeEntryFromJson(json);
}
```
### `Tag`
```dart
// lib/features/entries/domain/tag.dart
@freezed
class Tag with _$Tag {
const factory Tag({
required int id,
required String name,
}) = _Tag;
factory Tag.fromJson(Map<String, dynamic> json) => _$TagFromJson(json);
}
```
### `TimerState`
```dart
// lib/features/timer/domain/timer_state.dart
@freezed
class TimerState with _$TimerState {
const factory TimerState.idle() = TimerIdle;
const factory TimerState.running({
required int entryId,
required DateTime startTime,
required Project project,
String? note,
}) = TimerRunning;
}
```
## Acceptance Criteria
- [ ] All models defined with `@freezed`
- [ ] `fromJson` / `toJson` generated via `json_serializable`
- [ ] `dart run build_runner build` generates `*.freezed.dart` and `*.g.dart` without errors
- [ ] Models have no mutable fields
- [ ] `TimerState` is a union type (sealed), not a single class
## Files to create
- `lib/features/projects/domain/project.dart`
- `lib/features/entries/domain/time_entry.dart`
- `lib/features/entries/domain/tag.dart`
- `lib/features/timer/domain/timer_state.dart`
## Notes
- Drift table rows ≠ domain models. Use mapper extension methods to convert between them.
- Add `extension ProjectMapper on ProjectData { Project toDomain() {...} }` in the DAO files.

View file

@ -0,0 +1,92 @@
# TT-004 — Repository Implementations
**Type:** Story
**Priority:** High
**Labels:** data-layer, repositories
**Depends on:** TT-002, TT-003
**Blocks:** TT-005
---
## Summary
Implement the concrete repository classes for each feature. Repositories act
as the single access point for the data layer; they delegate to Drift DAOs
and map DB rows to domain models.
## Repositories to implement
### `ProjectsRepository`
File: `lib/features/projects/data/projects_repository.dart`
```dart
abstract class ProjectsRepository {
Stream<List<Project>> watchAll();
Stream<List<Project>> watchActive(); // archivedAt IS NULL
Future<Project> getById(int id);
Future<int> create({required String name, required int colorValue, String? description});
Future<void> update(Project project);
Future<void> archive(int id); // sets archivedAt = now
Future<void> delete(int id);
}
```
### `TimeEntriesRepository`
File: `lib/features/entries/data/entries_repository.dart`
```dart
abstract class TimeEntriesRepository {
Stream<List<TimeEntry>> watchAll();
Stream<List<TimeEntry>> watchByProject(int projectId);
Stream<List<TimeEntry>> watchByDateRange(DateTime from, DateTime to);
Future<TimeEntry?> getActiveEntry(); // end_time IS NULL
Future<int> create(TimeEntry entry);
Future<void> update(TimeEntry entry);
Future<void> delete(int id);
}
```
### `TagsRepository`
File: `lib/features/entries/data/tags_repository.dart`
```dart
abstract class TagsRepository {
Stream<List<Tag>> watchAll();
Future<Tag> findOrCreate(String name);
Future<void> setTagsForEntry(int entryId, List<String> tagNames);
Future<List<Tag>> getTagsForEntry(int entryId);
}
```
### `ReportsRepository`
File: `lib/features/reports/data/reports_repository.dart`
```dart
abstract class ReportsRepository {
Future<Duration> getTotalDuration(DateTime from, DateTime to, {int? projectId});
Future<Map<int, Duration>> getDurationByProject(DateTime from, DateTime to);
Future<Map<int, Duration>> getDurationByWeekday(DateTime weekStart);
Future<Map<int, Duration>> getDurationByDay(DateTime monthStart);
}
```
## Acceptance Criteria
- [ ] Abstract interface + concrete `DriftXxxRepository` implementation for each
- [ ] Riverpod provider for each concrete implementation
- [ ] All DB access goes through DAOs — no raw SQL in repositories
- [ ] Mapper extension methods convert `Drift row → domain model`
- [ ] Unit tests for each repository method using in-memory Drift DB
## Files to create / modify
- `lib/features/projects/data/projects_repository.dart` (replace stub)
- `lib/features/projects/data/drift_projects_repository.dart`
- `lib/features/entries/data/entries_repository.dart` (replace stub)
- `lib/features/entries/data/drift_entries_repository.dart`
- `lib/features/entries/data/tags_repository.dart`
- `lib/features/entries/data/drift_tags_repository.dart`
- `lib/features/reports/data/reports_repository.dart` (replace stub)
- `lib/features/reports/data/drift_reports_repository.dart`
- `test/features/*/` — unit tests for each
## Notes
- Keep repositories free of Flutter/UI imports
- Streams from Drift are reactive — repositories expose them directly to Riverpod providers

View file

@ -0,0 +1,114 @@
# TT-005 — Riverpod Providers & Notifiers
**Type:** Story
**Priority:** High
**Labels:** state-management, riverpod
**Depends on:** TT-003, TT-004
**Blocks:** TT-006, TT-007, TT-008, TT-009
---
## Summary
Implement all Riverpod providers and notifiers for each feature. This is the
domain/application layer that connects the data layer (repositories) to the
presentation layer (screens/widgets).
## Providers to implement
### Timer Feature
File: `lib/features/timer/domain/timer_notifier.dart`
```dart
@riverpod
class TimerNotifier extends _$TimerNotifier {
@override
TimerState build(); // reads DB for open entry on init
Future<void> start(Project project, {String? note});
Future<void> stop();
Future<void> discard();
void updateNote(String note);
}
```
- Uses a `dart:async Timer.periodic(1s)` while running
- On `build()`: calls `timeEntriesRepository.getActiveEntry()` to restore state
- See `.ai/features/timer.md` for full state-machine spec
### Projects Feature
File: `lib/features/projects/domain/projects_provider.dart`
```dart
@riverpod
Stream<List<Project>> activeProjects(ActiveProjectsRef ref);
@riverpod
Stream<List<Project>> allProjects(AllProjectsRef ref);
@riverpod
class ProjectsNotifier extends _$ProjectsNotifier {
Future<void> create({required String name, required int colorValue, String? description});
Future<void> update(Project project);
Future<void> archive(int id);
Future<void> delete(int id);
}
```
### Entries Feature
File: `lib/features/entries/domain/entries_provider.dart`
```dart
@riverpod
Stream<List<TimeEntry>> entriesByDateRange(
EntriesByDateRangeRef ref, {required DateTime from, required DateTime to});
@riverpod
class EntriesNotifier extends _$EntriesNotifier {
Future<void> create(TimeEntry entry);
Future<void> update(TimeEntry entry);
Future<void> delete(int id);
}
```
### Reports Feature
File: `lib/features/reports/domain/reports_provider.dart`
```dart
@riverpod
class ReportsNotifier extends _$ReportsNotifier {
@override
ReportData build();
void selectPeriod(ReportPeriod period); // day | week | month
void goToPrevious();
void goToNext();
void filterByProject(int? projectId);
}
@freezed
class ReportData with _$ReportData {
const factory ReportData({
required Duration totalDuration,
required Map<String, Duration> durationByProject,
required List<double> chartValues, // bars for fl_chart
required DateTime periodStart,
required DateTime periodEnd,
}) = _ReportData;
}
```
## Acceptance Criteria
- [ ] All providers use `@riverpod` annotation (code-gen)
- [ ] `TimerNotifier.build()` restores running timer from DB on app start
- [ ] `TimerNotifier` disposes its `dart:async Timer` via `ref.onDispose`
- [ ] All providers inject repositories via `ref.watch(xxxRepositoryProvider)`
- [ ] Unit tests for `TimerNotifier` (start/stop/discard/restore)
- [ ] Unit tests for `ProjectsNotifier` (CRUD operations)
## Files to create
- `lib/features/timer/domain/timer_notifier.dart`
- `lib/features/projects/domain/projects_provider.dart`
- `lib/features/entries/domain/entries_provider.dart`
- `lib/features/reports/domain/reports_provider.dart`
- `lib/features/reports/domain/report_data.dart`
- `test/features/timer/timer_notifier_test.dart`
- `test/features/projects/projects_notifier_test.dart`

View file

@ -0,0 +1,76 @@
# TT-006 — Timer Feature UI
**Type:** Story
**Priority:** High
**Labels:** ui, feature, timer
**Depends on:** TT-005
**Blocks:** —
---
## Summary
Implement the fully functional Timer screen. This is the primary screen of the
app — users will spend most of their time here.
## UI Specification
### TimerScreen layout
```
┌──────────────────────────────┐
│ AppBar: "Timer" │
├──────────────────────────────┤
│ │
│ [Project Picker Chip] │
│ │
│ 00:00:00 ← elapsed │
│ │
│ [Note TextField] │
│ │
│ ● START / ■ STOP │
│ │
│ [Discard] (only running) │
│ │
│ ── Recent entries ──────── │
│ Today: 2h 30m │
│ [ Entry 1 ] │
│ [ Entry 2 ] │
└──────────────────────────────┘
```
### Widgets to build
| Widget | File | Description |
|--------|------|-------------|
| `TimerScreen` | `timer_screen.dart` | Root screen |
| `TimerDisplay` | `widgets/timer_display.dart` | Large elapsed time counter `HH:MM:SS` |
| `TimerControls` | `widgets/timer_controls.dart` | Start/Stop/Discard FAB area |
| `ProjectPickerSheet` | `widgets/project_picker_sheet.dart` | BottomSheet to select project |
| `ProjectPickerChip` | `widgets/project_picker_chip.dart` | Tappable chip showing selected project |
| `TodaySummaryCard` | `widgets/today_summary_card.dart` | Total hours today |
## Behaviour
- On mount: `TimerNotifier.build()` already restores running state
- Elapsed display updates every second via `ref.watch(timerNotifierProvider)`
- Tapping project chip → opens `ProjectPickerSheet`
- START disabled if no project selected
- STOP saves entry; DISCARD deletes it
- "Recent entries" shows today's completed entries (last 5)
## Acceptance Criteria
- [ ] `TimerDisplay` shows `00:00:00` when idle, live elapsed when running
- [ ] Project chip is required before START is enabled
- [ ] Note field is editable while timer is running
- [ ] STOP → entry appears in recent list immediately (reactive stream)
- [ ] DISCARD shows confirmation dialog before deleting
- [ ] `TodaySummaryCard` updates when entries change
- [ ] Widget test: idle state renders START button
- [ ] Widget test: running state renders STOP + DISCARD buttons
- [ ] Widget test: tapping STOP calls `timerNotifier.stop()`
## Files to create / modify
- `lib/features/timer/presentation/timer_screen.dart` (replace stub)
- `lib/features/timer/presentation/widgets/timer_display.dart`
- `lib/features/timer/presentation/widgets/timer_controls.dart`
- `lib/features/timer/presentation/widgets/project_picker_chip.dart`
- `lib/features/timer/presentation/widgets/project_picker_sheet.dart`
- `lib/features/timer/presentation/widgets/today_summary_card.dart`
- `test/features/timer/timer_screen_test.dart`

View file

@ -0,0 +1,73 @@
# TT-007 — Entries Feature UI
**Type:** Story
**Priority:** High
**Labels:** ui, feature, entries
**Depends on:** TT-005
**Blocks:** —
---
## Summary
Implement the Entries screen — a chronological list of all time entries with
the ability to manually add, edit, and delete entries.
## UI Specification
### EntriesScreen layout
```
┌──────────────────────────────┐
│ AppBar: "Entries" [+ Add] │
│ [Date range filter chips] │
├──────────────────────────────┤
│ Monday, 2026-07-12 │
│ ┌─────────────────────────┐ │
│ │ 🟦 My Project 2h 30m│ │
│ │ 09:00 11:30 "Standup"│ │
│ └─────────────────────────┘ │
│ ┌─────────────────────────┐ │
│ │ 🟩 Backend 1h 00m│ │
│ │ 13:00 14:00 │ │
│ └─────────────────────────┘ │
│ Total: 3h 30m │
│ │
│ Sunday, 2026-07-11 │
│ ... │
└──────────────────────────────┘
```
### Widgets to build
| Widget | File | Description |
|--------|------|-------------|
| `EntriesScreen` | `entries_screen.dart` | Root screen with date-grouped list |
| `EntryListTile` | `widgets/entry_list_tile.dart` | Single entry row with swipe-to-delete |
| `DayHeader` | `widgets/day_header.dart` | Section header with date + daily total |
| `EntryFormSheet` | `widgets/entry_form_sheet.dart` | BottomSheet for create/edit |
| `DateRangeFilter` | `widgets/date_range_filter.dart` | Chip row: Today / This week / This month |
### Entry Form Fields
- Project (required, dropdown/picker)
- Start date + time (DateTimePicker)
- End date + time (DateTimePicker)
- Note (optional, text field)
- Tags (optional, multi-select chip input)
- Duration is auto-computed from start/end
## Acceptance Criteria
- [ ] Entries grouped by date, newest first
- [ ] Each group shows daily total duration
- [ ] Swipe-to-delete with undo `SnackBar`
- [ ] Tap entry → opens edit form pre-filled
- [ ] "+ Add" button → opens empty create form
- [ ] Form validates: end time must be after start time
- [ ] DateRangeFilter chips change the displayed entries reactively
- [ ] Widget test: empty state shows placeholder text
- [ ] Widget test: list renders entries from provider
## Files to create / modify
- `lib/features/entries/presentation/entries_screen.dart` (replace stub)
- `lib/features/entries/presentation/widgets/entry_list_tile.dart`
- `lib/features/entries/presentation/widgets/day_header.dart`
- `lib/features/entries/presentation/widgets/entry_form_sheet.dart`
- `lib/features/entries/presentation/widgets/date_range_filter.dart`
- `test/features/entries/entries_screen_test.dart`

View file

@ -0,0 +1,69 @@
# TT-008 — Projects Feature UI
**Type:** Story
**Priority:** High
**Labels:** ui, feature, projects
**Depends on:** TT-005
**Blocks:** TT-006 (project picker)
---
## Summary
Implement the Projects screen where users manage their projects — create,
edit, archive, and delete. Projects are referenced by time entries and the timer.
## UI Specification
### ProjectsScreen layout
```
┌──────────────────────────────┐
│ AppBar: "Projects" [+ Add] │
│ [Active] [Archived] ← tabs │
├──────────────────────────────┤
│ ┌─────────────────────────┐ │
│ │ 🟦 My Project → │ │
│ │ 14h 30m total │ │
│ └─────────────────────────┘ │
│ ┌─────────────────────────┐ │
│ │ 🟩 Backend → │ │
│ │ 8h 00m total │ │
│ └─────────────────────────┘ │
└──────────────────────────────┘
```
### Widgets to build
| Widget | File | Description |
|--------|------|-------------|
| `ProjectsScreen` | `projects_screen.dart` | Tabbed screen: Active / Archived |
| `ProjectListTile` | `widgets/project_list_tile.dart` | Tile with color dot, name, total hours |
| `ProjectFormSheet` | `widgets/project_form_sheet.dart` | Create / edit bottom sheet |
| `ColorPickerRow` | `widgets/color_picker_row.dart` | Row of selectable color circles |
### ProjectFormSheet Fields
- Name (required, text field, unique validation)
- Color (required, `ColorPickerRow` — 12 preset colors)
- Description (optional, multiline text field)
### Actions per project (long-press or trailing menu)
- Edit
- Archive / Unarchive
- Delete (only if no time entries reference it; otherwise show warning)
## Acceptance Criteria
- [ ] Active tab shows non-archived projects
- [ ] Archived tab shows archived projects
- [ ] Each tile shows total tracked hours (all-time)
- [ ] "+" → create form; tap tile → edit form
- [ ] Archive action sets `archivedAt`; moves project to Archived tab immediately
- [ ] Delete blocked with dialog if project has entries
- [ ] Color picker shows ≥ 10 preset colors
- [ ] Name uniqueness validated before save
- [ ] Widget test: empty state renders "No projects yet"
- [ ] Widget test: create form saves and tile appears
## Files to create / modify
- `lib/features/projects/presentation/projects_screen.dart` (replace stub)
- `lib/features/projects/presentation/widgets/project_list_tile.dart`
- `lib/features/projects/presentation/widgets/project_form_sheet.dart`
- `lib/features/projects/presentation/widgets/color_picker_row.dart`
- `test/features/projects/projects_screen_test.dart`

View file

@ -0,0 +1,82 @@
# TT-009 — Reports Feature UI
**Type:** Story
**Priority:** High
**Labels:** ui, feature, reports, charts
**Depends on:** TT-005
**Blocks:** —
---
## Summary
Implement the Reports screen with day / week / month views and bar charts
powered by `fl_chart`. Users can navigate between periods and filter by project.
## UI Specification
### ReportsScreen layout
```
┌──────────────────────────────┐
│ AppBar: "Reports" [export] │
│ [Day] [Week] [Month] tabs │
├──────────────────────────────┤
│ ◀ Mon 7 Jul Sun 13 Jul ▶ │
│ │
│ Total: 22h 15m │
│ │
│ ┌────────────────────────┐ │
│ │ Bar chart (fl_chart) │ │
│ │ Mon Tue Wed Thu Fri │ │
│ └────────────────────────┘ │
│ │
│ By project: │
│ 🟦 My Project 14h (63%) │
│ 🟩 Backend 8h (36%) │
└──────────────────────────────┘
```
### Views
| Tab | X-axis | Bar value |
|-------|-----------------|-------------------|
| Day | Hours (023) | Minutes per hour |
| Week | Weekdays (MoSu)| Hours per day |
| Month | Weeks 15 | Hours per week |
### Widgets to build
| Widget | File | Description |
|--------|------|-------------|
| `ReportsScreen` | `reports_screen.dart` | Tabbed screen + period navigator |
| `PeriodNavigator` | `widgets/period_navigator.dart` | ◀ Period label ▶ |
| `DurationBarChart` | `widgets/duration_bar_chart.dart` | fl_chart BarChart wrapper |
| `ProjectBreakdownList` | `widgets/project_breakdown_list.dart` | Per-project totals + % |
| `SummaryHeader` | `widgets/summary_header.dart` | Total duration + avg daily |
### Period Navigation
- "Previous" / "Next" arrows call `reportsNotifier.goToPrevious()` / `.goToNext()`
- Tapping the period label opens a `DateRangePicker` (week/month snapped)
### Project Filter
- Optional `DropdownButton` in AppBar actions to filter by project
- Default: all projects
## Acceptance Criteria
- [ ] Three tabs: Day / Week / Month with distinct chart shapes
- [ ] Period navigator updates charts and summary reactively
- [ ] `DurationBarChart` renders correct number of bars per period
- [ ] Y-axis labeled in hours; bars show duration for that bucket
- [ ] Project breakdown list sorted by duration descending
- [ ] Export button in AppBar triggers export flow (see TT-010)
- [ ] Widget test: week view renders 7 bars
- [ ] Widget test: navigating period changes displayed label
## Files to create / modify
- `lib/features/reports/presentation/reports_screen.dart` (replace stub)
- `lib/features/reports/presentation/widgets/period_navigator.dart`
- `lib/features/reports/presentation/widgets/duration_bar_chart.dart`
- `lib/features/reports/presentation/widgets/project_breakdown_list.dart`
- `lib/features/reports/presentation/widgets/summary_header.dart`
- `test/features/reports/reports_screen_test.dart`
## Notes
- See `.ai/features/reports.md` for Drift query details and chart data format
- `fl_chart` docs: https://pub.dev/packages/fl_chart

View file

@ -0,0 +1,98 @@
# TT-010 — Settings & Export Feature
**Type:** Story
**Priority:** Medium
**Labels:** ui, feature, settings, export
**Depends on:** TT-005
**Blocks:** —
---
## Summary
Implement the Settings screen covering language selection, theme override,
and the full data export flow (CSV + PDF + JSON) via the system share sheet.
## UI Specification
### SettingsScreen sections
```
┌──────────────────────────────┐
│ AppBar: "Settings" │
├──────────────────────────────┤
│ ▶ APPEARANCE │
│ Theme [System ▼] │
│ Language [English ▼] │
│ │
│ ▶ DATA │
│ Export data → │
│ Import / Restore → │
│ │
│ ▶ ABOUT │
│ Version 0.1.0 │
│ Licenses │
└──────────────────────────────┘
```
### Export Flow (ExportSheet)
Triggered from Settings → "Export data" or Reports → AppBar export button.
```
1. Select format: [CSV] [PDF] [JSON]
2. Select range: [Today] [This week] [This month] [Custom]
3. Filter project: [All projects ▼]
4. → Share button → share_plus share sheet
```
## Export Implementation
### CSV
- Columns: `id, project, start_time, end_time, duration_seconds, note, tags`
- Tags: pipe-separated
- Encoding: UTF-8 with BOM
- File name: `timetrack_export_2026-07-12.csv`
### PDF
- Header: App name, export date, period
- Summary table: Project | Total hours | %
- Entries table: Date | Project | Duration | Note
- File name: `timetrack_report_2026-07-12.pdf`
### JSON
- Full backup schema — see `.ai/features/export.md`
- File name: `timetrack_backup_2026-07-12.json`
## Settings Persistence
Store user preferences in `shared_preferences`:
| Key | Type | Default |
|-----|------|---------|
| `theme_mode` | String | `system` |
| `locale` | String | device locale |
Expose as Riverpod `@riverpod` providers:
- `themeModeProvider` — reads / writes `ThemeMode`
- `localeProvider` — reads / writes `Locale`
Wire into `app.dart` `MaterialApp.router`.
## Acceptance Criteria
- [ ] Theme dropdown: System / Light / Dark — changes app theme immediately
- [ ] Language dropdown: English / Deutsch — changes app locale immediately
- [ ] Export sheet: all 3 formats functional, share sheet opens
- [ ] Export produces non-empty files for each format
- [ ] CSV UTF-8 BOM present (verify with hex check in test)
- [ ] PDF renders without exception for non-empty date range
- [ ] JSON is valid and matches backup schema
- [ ] Preferences persist across app restart (shared_preferences)
- [ ] Widget test: theme selection updates `themeModeProvider`
## Files to create / modify
- `lib/features/settings/presentation/settings_screen.dart` (replace stub)
- `lib/features/settings/presentation/widgets/export_sheet.dart`
- `lib/features/settings/domain/settings_provider.dart`
- `lib/features/settings/data/export_service.dart`
- `test/features/settings/export_service_test.dart`
## Dependencies to add
```yaml
shared_preferences: ^2.3.3
```
Add to `pubspec.yaml`.

View file

@ -0,0 +1,131 @@
# TT-011 — Localisation (i18n)
**Type:** Story
**Priority:** Medium
**Labels:** i18n, l10n, infrastructure
**Depends on:** TT-001
**Blocks:** TT-006, TT-007, TT-008, TT-009, TT-010
---
## Summary
Set up `flutter_localizations` with ARB files for English (`en`) and German
(`de`). All UI strings must be externalised — no hardcoded display text in
widget files.
## Setup
### l10n.yaml (project root)
```yaml
arb-dir: lib/core/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations
```
### File structure
```
lib/core/l10n/
app_en.arb ← template (English)
app_de.arb ← German translations
```
Generated: `lib/core/l10n/app_localizations.dart` (do not edit manually)
### Usage in widgets
```dart
import 'package:timetrack/core/l10n/app_localizations.dart';
// ...
Text(AppLocalizations.of(context)!.timerStart)
```
## Strings to externalise (minimum set)
### Navigation
```json
"navTimer": "Timer",
"navEntries": "Entries",
"navProjects": "Projects",
"navReports": "Reports",
"navSettings": "Settings"
```
### Timer
```json
"timerStart": "Start",
"timerStop": "Stop",
"timerDiscard": "Discard",
"timerDiscardConfirm": "Discard this entry?",
"timerSelectProject": "Select project",
"timerAddNote": "Add a note…",
"timerTodayTotal": "Today: {duration}"
```
### Entries
```json
"entriesTitle": "Entries",
"entriesEmpty": "No entries yet",
"entriesAdd": "Add entry",
"entriesDeleteUndo": "Entry deleted",
"entriesUndo": "Undo"
```
### Projects
```json
"projectsTitle": "Projects",
"projectsEmpty": "No projects yet",
"projectsAdd": "Add project",
"projectsActive": "Active",
"projectsArchived": "Archived",
"projectsName": "Name",
"projectsDescription": "Description",
"projectsArchive": "Archive",
"projectsDeleteBlocked": "Cannot delete — project has entries"
```
### Reports
```json
"reportsTitle": "Reports",
"reportsDay": "Day",
"reportsWeek": "Week",
"reportsMonth": "Month",
"reportsTotal": "Total",
"reportsNoData": "No data for this period"
```
### Settings / Export
```json
"settingsTitle": "Settings",
"settingsTheme": "Theme",
"settingsThemeSystem": "System",
"settingsThemeLight": "Light",
"settingsThemeDark": "Dark",
"settingsLanguage": "Language",
"settingsExport": "Export data",
"exportFormat": "Format",
"exportRange": "Date range",
"exportShare": "Share"
```
### Common
```json
"cancel": "Cancel",
"save": "Save",
"delete": "Delete",
"edit": "Edit",
"confirm": "Confirm"
```
## Acceptance Criteria
- [ ] `l10n.yaml` present at project root
- [ ] `app_en.arb` contains all strings listed above
- [ ] `app_de.arb` contains all German translations (no English fallback strings)
- [ ] `flutter gen-l10n` (or `flutter pub get`) generates `app_localizations.dart`
- [ ] Zero hardcoded display strings in any `presentation/` file
- [ ] Switching language in Settings updates all visible strings immediately
- [ ] Widget test: locale override renders German strings
## Files to create
- `l10n.yaml`
- `lib/core/l10n/app_en.arb`
- `lib/core/l10n/app_de.arb`

View file

@ -0,0 +1,96 @@
# TT-012 — Unit Tests
**Type:** Task
**Priority:** Medium
**Labels:** testing, unit-tests
**Depends on:** TT-004, TT-005
**Blocks:** —
---
## Summary
Write comprehensive unit tests for all repository methods and Riverpod
notifiers. See `.ai/testing.md` for full conventions.
## Scope
### Database / Repository tests
All tests use `NativeDatabase.memory()` — never the real file DB.
| Test file | What to test |
|-----------|-------------|
| `test/core/database/app_database_test.dart` | Schema creation, schemaVersion |
| `test/features/projects/projects_repository_test.dart` | CRUD, watchActive stream, archive |
| `test/features/entries/entries_repository_test.dart` | CRUD, watchByDateRange, getActiveEntry |
| `test/features/entries/tags_repository_test.dart` | findOrCreate, setTagsForEntry |
| `test/features/reports/reports_repository_test.dart` | getDurationByProject, getDurationByWeekday |
### Notifier tests
All notifier tests use `ProviderContainer` with mocked repositories.
| Test file | What to test |
|-----------|-------------|
| `test/features/timer/timer_notifier_test.dart` | start, stop, discard, restore from open entry |
| `test/features/projects/projects_notifier_test.dart` | create, update, archive, delete |
| `test/features/entries/entries_notifier_test.dart` | create, update, delete |
## Test Patterns
### Repository test pattern
```dart
late AppDatabase db;
late DriftProjectsRepository repository;
setUp(() {
db = AppDatabase(NativeDatabase.memory());
repository = DriftProjectsRepository(db);
});
tearDown(() => db.close());
test('insert and retrieve project', () async {
await repository.create(name: 'Test', colorValue: 0xFF0000FF);
final projects = await repository.watchActive().first;
expect(projects, hasLength(1));
expect(projects.first.name, equals('Test'));
});
```
### Notifier test pattern
```dart
late ProviderContainer container;
late MockProjectsRepository mockRepo;
setUp(() {
mockRepo = MockProjectsRepository();
container = ProviderContainer(overrides: [
projectsRepositoryProvider.overrideWithValue(mockRepo),
]);
});
tearDown(container.dispose);
```
## Coverage Goals
| Layer | Target |
|-------|--------|
| Repository (CRUD methods) | 100% |
| Notifiers | ≥ 80% |
| Export service | ≥ 70% |
## Acceptance Criteria
- [ ] All listed test files exist and pass
- [ ] `flutter test` exits with code 0
- [ ] No test uses the real file database
- [ ] Each repository test covers: insert, read, update, delete, stream emission
- [ ] `TimerNotifier` test covers: start → running state, stop → idle + entry saved,
discard → idle + entry deleted, build with open entry → running state restored
- [ ] `flutter test --coverage` generates coverage report
## Running tests
```bash
flutter test # all tests
flutter test test/features/timer/ # single feature
flutter test --coverage # with lcov coverage
genhtml coverage/lcov.info -o coverage/html # HTML report
```

View file

@ -0,0 +1,93 @@
# TT-013 — Widget Tests
**Type:** Task
**Priority:** Medium
**Labels:** testing, widget-tests
**Depends on:** TT-006, TT-007, TT-008, TT-009, TT-010, TT-011
**Blocks:** —
---
## Summary
Write widget tests for each feature screen. Tests verify correct rendering
of UI states, user interactions, and provider integration. All provider
dependencies are overridden with mocks via `mocktail`.
## Scope
| Test file | Screen | Key scenarios |
|-----------|--------|---------------|
| `test/features/timer/timer_screen_test.dart` | TimerScreen | idle state, running state, start tap, stop tap |
| `test/features/entries/entries_screen_test.dart` | EntriesScreen | empty state, list renders, swipe delete, add form |
| `test/features/projects/projects_screen_test.dart` | ProjectsScreen | empty state, tile renders, create form, archive |
| `test/features/reports/reports_screen_test.dart` | ReportsScreen | week tab, period navigation, chart renders |
| `test/features/settings/settings_screen_test.dart` | SettingsScreen | theme picker, language picker, export sheet opens |
## Widget Test Pattern
```dart
Widget buildTestApp({List<Override> overrides = const []}) {
return ProviderScope(
overrides: overrides,
child: MaterialApp(
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: const TimerScreen(),
),
);
}
testWidgets('shows START button when idle', (tester) async {
when(() => mockTimerNotifier.build()).thenReturn(const TimerState.idle());
await tester.pumpWidget(buildTestApp(overrides: [
timerNotifierProvider.overrideWith(() => mockTimerNotifier),
]));
expect(find.text('Start'), findsOneWidget);
expect(find.text('Stop'), findsNothing);
});
```
## Key Widget Test Cases per Screen
### TimerScreen
- [ ] Idle: START visible, STOP hidden, elapsed shows `00:00:00`
- [ ] Running: STOP visible, DISCARD visible, project name shown
- [ ] Tap START without project → button remains disabled
- [ ] Tap STOP → `timerNotifier.stop()` called once
- [ ] Tap DISCARD → confirmation dialog appears
### EntriesScreen
- [ ] Empty: placeholder text visible
- [ ] With entries: `EntryListTile` rendered for each
- [ ] Swipe left on entry → delete action visible
- [ ] Tap "+" → `EntryFormSheet` appears
- [ ] Date filter chip "Today" → filters visible entries
### ProjectsScreen
- [ ] Empty Active tab: "No projects yet" visible
- [ ] Tile shows project name and color dot
- [ ] Tap tile → edit form opens pre-filled
- [ ] Archive action → tile moves to Archived tab
### ReportsScreen
- [ ] Week tab shows 7 bar groups
- [ ] Tap ▶ → period label advances by one week
- [ ] Total duration card shows formatted duration
### SettingsScreen
- [ ] Theme dropdown shows current selection
- [ ] Changing theme → `themeModeProvider` updated
- [ ] Tap "Export data" → `ExportSheet` bottom sheet appears
## Acceptance Criteria
- [ ] All listed widget test files exist and pass with `flutter test`
- [ ] Each screen has ≥ 3 test cases
- [ ] No test relies on real DB or real providers
- [ ] Tests run in < 30 seconds total (no `sleep` / real async delays)
- [ ] `pumpAndSettle` used after async interactions
## Notes
- Use `find.byType()`, `find.text()`, `find.byKey()` — avoid pixel-exact finders
- Add semantic labels / keys to interactive widgets to simplify test selectors
- Golden tests (screenshot comparison) are optional for v1

View file

@ -0,0 +1,53 @@
# TT-014 — Go APK-Serve-Binary
**Type:** Story
**Priority:** High
**Labels:** go, tooling, server
**Depends on:** —
**Blocks:** TT-016 (serve_apk Make-Target)
---
## Summary
Implementiere einen portablen HTTP-Server in Go der eine APK per WLAN bereitstellt.
Kein Python, kein Flask, keine externen Abhängigkeiten — nur Go Standard-Library.
Das kompilierte Binary `scripts/serve/serve` wird von `make serve_apk` gestartet.
## Funktionsumfang
| Feature | Detail |
|---------|--------|
| Landingpage `/` | HTML-Template mit App-Name, Version, Größe, Build-Datum, SHA256 |
| Download `/download` | APK als Streaming-Response mit Content-Disposition |
| SHA256-Endpoint `/sha256` | JSON: `{"sha256":"…","filename":"…"}` |
| No-Cache-Header | `Cache-Control: no-store` auf allen Responses |
| QR-Code im Terminal | `qrencode -t UTF8 <url>` via `os/exec` |
| Lokale IP | UDP-Trick: Verbindung zu 8.8.8.8:80, eigene IP ablesen |
| CLI-Flags | `--dir build/apk` (default), `--port 8888`, `--debug` |
| Versioninfo | APK-Dateiname parsen: `timetrack-0.1.0+3.apk` → Version + Build-Nummer |
| SHA256 Sidecar | `.sha256`-Datei neben APK lesen (Format: `<hash> <pfad>`) |
## HTML-Template
- Design: modernes Card-Layout, dunkles Theme (Timetrack-Blau `#2563EB`)
- Felder: Version, Größe MB, Build-Datum, Plattform
- Verifizierungs-Sektion: SHA256 anzeigen + JS-Datei-Hash-Prüfung (Web Crypto API)
- Installationshinweis für Android (Sicherheitseinstellungen)
## Go-Modul
```
scripts/serve/
go.mod (module timetrack/serve, go 1.21, keine externen deps)
main.go (alles in einem File — überschaubar)
```
## Acceptance Criteria
- [ ] `go build -o serve ./scripts/serve/` kompiliert ohne Fehler
- [ ] `./serve --dir build/apk --port 8888` startet Server
- [ ] Landingpage zeigt korrekte Metadaten wenn APK vorhanden
- [ ] `/download` liefert APK mit korrektem MIME-Type
- [ ] QR-Code erscheint im Terminal (falls `qrencode` verfügbar)
- [ ] Server startet auch wenn kein `qrencode` vorhanden (graceful fallback)
- [ ] Klarer Fehler wenn kein `.apk` im `--dir` gefunden
## Files to create
- `scripts/serve/go.mod`
- `scripts/serve/main.go`

View file

@ -0,0 +1,57 @@
# TT-015 — APK-Retention-Script
**Type:** Task
**Priority:** High
**Labels:** tooling, shell
**Depends on:** —
**Blocks:** TT-016 (build_android Make-Target)
---
## Summary
Shell-Script `scripts/apk_retention.sh` das nach jedem APK-Build die Build-Historie
bereinigt. Wird vom Makefile als letzter Schritt von `build_android` aufgerufen.
## Retention-Strategie
### Regel 1 — Aktuelle Minor-Version
In der Minor-Version die gerade entwickelt wird (höchste Minor):
- Behalte die **letzten 15 APKs** nach Build-Nummer
- Lösche ältere
### Regel 2 — Abgeschlossene Minor-Versionen
Für ältere Minor-Versionen (z.B. 0.1.x wenn aktuell 0.2.x entwickelt wird):
- Behalte immer genau **eine APK pro Minor-Version** (die mit der höchsten Build-Nummer)
- Das ist der "Release-Marker" für diese Version
### Beispiel
```
Aktuell: 0.1.x (Build +1 bis +20 vorhanden)
→ Behalte +6 bis +20 (15 neueste)
→ Lösche +1 bis +5
Später: 0.2.x existiert, 0.1.x hat +1 bis +20
→ 0.1.x: Behalte nur +20 (höchste = Release-Marker)
→ 0.2.x: Behalte letzte 15
```
## APK-Dateiformat
`timetrack-{VERSION}+{BUILD}.apk`
Beispiele:
- `timetrack-0.1.0+1.apk`
- `timetrack-0.1.0+15.apk`
- `timetrack-0.2.0+3.apk`
## Acceptance Criteria
- [ ] Script nimmt `--dir` als Argument (default: `build/apk`)
- [ ] Erkennt Minor-Versionen korrekt (0.1, 0.2, etc.)
- [ ] Behält letzte 15 in aktueller Minor-Version
- [ ] Behält je eine (neueste) APK für ältere Minor-Versionen
- [ ] Löscht zugehörige `.sha256`-Dateien mitgleich
- [ ] Gibt Liste der gelöschten Dateien aus
- [ ] Gibt Liste der behaltenen Dateien aus
- [ ] Tut nichts wenn weniger als 16 APKs vorhanden (keine Löschung nötig)
- [ ] Löscht niemals den `timetrack.apk`-Symlink
## Files to create
- `scripts/apk_retention.sh` (ausführbar, `chmod +x`)

View file

@ -0,0 +1,106 @@
# TT-016 — Makefile erstellen
**Type:** Story
**Priority:** High
**Labels:** makefile, tooling, build
**Depends on:** TT-014 (Go-Binary), TT-015 (Retention-Script)
**Blocks:** TT-017 (Integration-Test)
---
## Summary
Erstelle das zentrale `Makefile` mit allen Build-, Test- und Deploy-Targets
für das Timetrack Flutter-Projekt.
## Variablen
```makefile
FLUTTER := $(HOME)/flutter/3.41.7/bin/flutter
JAVA_HOME ?= $(HOME)/java/17.0.13+11
ANDROID_HOME ?= $(HOME)/Android/sdk
ADB := $(ANDROID_HOME)/platform-tools/adb
BUILD_VERSION ?= 0.1.0
BUILD_NUMBER ?= 1
PORT ?= 8888
APK_NAME := timetrack-$(BUILD_VERSION)+$(BUILD_NUMBER).apk
APK_DIR := build/apk
APK_LATEST := $(APK_DIR)/timetrack.apk
SERVE_BIN := scripts/serve/serve
```
## Targets
### Dev-Targets
| Target | Befehl | Beschreibung |
|--------|--------|-------------|
| `help` | — | Alle Targets mit Beschreibung ausgeben |
| `install` | `flutter pub get` | Dependencies installieren |
| `generate` | `flutter pub run build_runner build --delete-conflicting-outputs` | Code-Generierung |
| `test` | `flutter test` | Alle Tests ausführen |
| `test_coverage` | `flutter test --coverage` + genhtml | Tests mit Coverage-Report |
| `analyze` | `flutter analyze` | Statische Analyse |
| `format` | `dart format lib/ test/` | Code formatieren |
| `clean` | `flutter clean` + `rm -rf coverage/` | Build-Artefakte löschen |
### Build-Targets
| Target | Beschreibung |
|--------|-------------|
| `build_serve` | Go-Binary `scripts/serve/serve` kompilieren |
| `build_android` | APK (release) + SHA256 + Symlink + Retention |
| `build_aab` | App Bundle für Play Store |
| `build_linux` | Linux Desktop-App |
| `build_web` | Web-App (debug) |
| `build_all` | APK + Linux + Web nacheinander |
### Deploy-Targets
| Target | Beschreibung |
|--------|-------------|
| `install_android` | APK via ADB auf verbundenes Gerät installieren |
| `deploy_android` | `build_android` + `install_android` |
| `serve_apk` | Go-Server starten (kompiliert Binary falls nötig) |
### Info-Targets
| Target | Beschreibung |
|--------|-------------|
| `db_schema` | Drift-Schema-Version und Tabellennamen anzeigen |
## build_android — Detail-Logik
```makefile
build_android:
# 1. JAVA_HOME und ANDROID_HOME prüfen
# 2. flutter build apk --release --build-name --build-number
# 3. mkdir -p $(APK_DIR)
# 4. cp build/app/outputs/flutter-apk/app-release.apk $(APK_DIR)/$(APK_NAME)
# 5. sha256sum $(APK_DIR)/$(APK_NAME) | tee $(APK_DIR)/$(APK_NAME).sha256
# 6. ln -sf $(APK_NAME) $(APK_LATEST)
# 7. bash scripts/apk_retention.sh --dir $(APK_DIR)
# 8. Ausgabe: Pfad, Größe, SHA256, serve_apk-Hinweis
```
## serve_apk — Dependency auf Binary
```makefile
serve_apk: $(SERVE_BIN) # Auto-kompilieren falls binary fehlt
$(SERVE_BIN) --dir $(APK_DIR) --port $(PORT)
$(SERVE_BIN):
cd scripts/serve && go build -o serve .
```
## Acceptance Criteria
- [ ] `make help` listet alle Targets mit Beschreibung
- [ ] `make install` führt `flutter pub get` aus
- [ ] `make test` führt alle Flutter-Tests aus
- [ ] `make analyze` führt `flutter analyze` aus
- [ ] `make format` formatiert Dart-Code
- [ ] `make clean` löscht Build-Artefakte
- [ ] `make build_serve` kompiliert das Go-Binary
- [ ] `make serve_apk` kompiliert Binary automatisch falls fehlend
- [ ] `make build_linux` prüft `ld.lld` und bricht mit Hinweis ab falls fehlend
- [ ] `make build_web` läuft durch (debug-Build)
- [ ] `make build_all` führt APK + Linux + Web nacheinander aus
- [ ] `make db_schema` zeigt Schema-Version und Tabellen an
- [ ] `BUILD_VERSION=0.2.0 BUILD_NUMBER=5 make build_android` nutzt die übergebenen Werte
## Files to create
- `Makefile`

View file

@ -0,0 +1,48 @@
# TT-017 — Integration-Test Makefile + Go-Binary
**Type:** Task
**Priority:** Medium
**Labels:** testing, tooling
**Depends on:** TT-014, TT-015, TT-016
**Blocks:** —
---
## Summary
Verifiziere alle Makefile-Targets und das Go-Binary manuell durch atomare
Smoke-Tests. Kein automatisiertes Test-Framework — Ausgabe wird visuell geprüft.
## Test-Schritte
### Go-Binary
- [ ] `cd scripts/serve && go build -o serve .` → kein Fehler
- [ ] `./scripts/serve/serve --help` → Flags werden angezeigt
- [ ] `./scripts/serve/serve --dir build/apk --port 9999` ohne APK → Fehlermeldung klar
### Retention-Script
- [ ] `bash scripts/apk_retention.sh --dir /tmp/test_apk` mit simulierten APK-Dummy-Dateien (touch)
- [ ] 20 APKs 0.1.x → nach Lauf: 15 verbleiben
- [ ] 5 APKs 0.1.x + 3 APKs 0.2.x → 0.1.x: 1 verbleibt (Release-Marker), 0.2.x: alle 3
### Makefile Dev-Targets
- [ ] `make help` → Ausgabe ohne Fehler
- [ ] `make install``flutter pub get` erfolgreich
- [ ] `make test` → 37 Tests grün
- [ ] `make analyze` → No issues
- [ ] `make format` → kein Fehler
- [ ] `make clean` → build/ und coverage/ entfernt
### Makefile Build-Targets
- [ ] `make build_serve` → Binary erstellt
- [ ] `make build_web``build/web` vorhanden
- [ ] `make build_linux``build/linux/x64/release/bundle/timetrack` vorhanden
- [ ] `make db_schema` → Schema-Version + Tabellen sichtbar
### serve_apk Auto-Build
- [ ] Binary löschen, dann `make serve_apk` → Binary wird transparent neu gebaut
- [ ] Server startet, QR-Code erscheint im Terminal
## Acceptance Criteria
- [ ] Alle Smoke-Tests grün
- [ ] Kein Target bricht mit unerwartetem Fehler ab
- [ ] `make help` enthält alle implementierten Targets

View file

@ -0,0 +1,31 @@
# TT-018 — Go-Server: --web Flag für Static-File-Serving
**Type:** Task
**Priority:** High
**Labels:** backend, go, serve
**Depends on:** —
**Blocks:** TT-019, TT-020
---
## Summary
Den bestehenden Go-Server (`scripts/serve/main.go`) um einen `--web` Flag erweitern.
Im Web-Modus wird das Verzeichnis `--dir` (Standard: `build/web/`) als Static-File-Server
ausgeliefert (`http.FileServer`). Die Flutter-Web-App läuft damit direkt im Browser des
Nutzers — kein Download-Button, kein APK-spezifisches UI.
Im Terminal soll — identisch zum APK-Modus — die lokale IP-Adresse sowie ein QR-Code
ausgegeben werden, der auf die Serve-URL zeigt.
## Acceptance Criteria
- [ ] Neuer CLI-Flag `--web` (bool) vorhanden
- [ ] Im Web-Modus: `http.FileServer` auf `--dir` (Standard: `build/web/`)
- [ ] Im Web-Modus: Terminal-Ausgabe mit lokaler IP + QR-Code (via `qrencode`)
- [ ] APK-Modus (ohne `--web`) verhält sich unverändert
- [ ] `go build` kompiliert fehlerfrei
## Files to create / modify
- `scripts/serve/main.go`

View file

@ -0,0 +1,22 @@
# TT-018 — Quick-Access Grid: Fallback auf erste N Projekte
**Type:** Task
**Priority:** Medium
**Labels:** timer, ux, quick-access
**Depends on:** TT-017 (Quick-Access Grid Implementierung)
**Blocks:** —
---
## Summary
Wenn noch keine TimeEntries vorhanden sind (kein Score berechenbar), sollen im
Quick-Access-Grid die ersten N angelegten Projekte (sortiert nach `createdAt` aufsteigend)
angezeigt werden. Das verbessert die UX direkt nach dem ersten Anlegen von Projekten.
## Acceptance Criteria
- [ ] Grid zeigt die ältesten N aktiven Projekte, wenn kein Projekt einen Score > 0 hat
- [ ] Sobald TimeEntries existieren, greift der normale Score-Algorithmus
- [ ] N entspricht dem konfigurierten `quickAccessCount`-Wert
## Files to modify
- `lib/features/timer/domain/frequent_projects_provider.dart`

View file

@ -0,0 +1,32 @@
# TT-019 — Makefile: serve_web Target + WEB_PORT Variable
**Type:** Task
**Priority:** High
**Labels:** makefile, devops, serve
**Depends on:** TT-018
**Blocks:** TT-020
---
## Summary
Im Makefile ein neues `serve_web` Target anlegen, das den Go-Server im Web-Modus startet.
Außerdem eine eigene `WEB_PORT`-Variable (Standard: `8080`) einführen, damit APK-Server
(Port `8888`) und Web-Server gleichzeitig betrieben werden können.
Das `$(SERVE_BIN)`-Target soll als dateibasiertes Makefile-Target umgebaut werden, sodass
das Go-Binary nur dann neu kompiliert wird, wenn `scripts/serve/main.go` neuer ist als das
Binary.
## Acceptance Criteria
- [ ] Variable `WEB_PORT ?= 8080` im Makefile vorhanden
- [ ] `$(SERVE_BIN): scripts/serve/main.go` als dateibasiertes Target (Timestamp-Check)
- [ ] `serve_apk` und `serve_web` hängen von `$(SERVE_BIN)` ab
- [ ] `serve_web` ruft `$(SERVE_BIN) --dir build/web --port $(WEB_PORT) --web` auf
- [ ] `make serve_web` startet den Server korrekt
- [ ] `make serve_web WEB_PORT=9090` überschreibt den Port
## Files to create / modify
- `Makefile`

View file

@ -0,0 +1,25 @@
# TT-019 — Quick-Access Grid: Auffüllen mit ältesten Projekten
**Type:** Task
**Priority:** Medium
**Labels:** timer, ux, quick-access
**Depends on:** TT-018
**Blocks:** —
---
## Summary
Das Quick-Access-Grid soll immer bis zu N Slots füllen. Gescorte Projekte stehen
links (höchster Score zuerst). Verbleibende Slots werden mit den ältesten aktiven
Projekten aufgefüllt, die noch nicht im scored-Set sind. Erst wenn es mehr als N
gescorte Projekte gibt, werden ausschließlich scored-Projekte angezeigt.
## Acceptance Criteria
- [ ] Gescorte Projekte erscheinen zuerst (links), sortiert nach Score
- [ ] Restliche Slots werden mit ältesten Projekten (nach createdAt) aufgefüllt
- [ ] Kein Projekt erscheint doppelt
- [ ] Grid zeigt maximal N Projekte (N = quickAccessCount)
- [ ] Gibt es >= N gescorte Projekte, werden nur gescorte angezeigt
## Files to modify
- `lib/features/timer/domain/frequent_projects_provider.dart`

View file

@ -0,0 +1,36 @@
# TT-020 — Bug: RangeError in frequentProjects beim Sortieren
**Type:** Bug
**Priority:** Highest
**Labels:** timer, quick-access, crash
**Depends on:** TT-019
**Blocks:** —
---
## Summary
`scoreOf` nutzt `scored.indexOf(p)` um den zugehörigen Index in `ages[]` zu ermitteln.
Während `List.sort()` die Liste umsortiert, liefert `indexOf` falsche oder `-1`-Indizes,
was zu einem `RangeError (length): Invalid value: Not in inclusive range 0..2: -1` führt.
## Root Cause
```dart
scored.sort((a, b) {
final ia = scored.indexOf(a); // ← indexOf auf der Liste die gerade sortiert wird
final ib = scored.indexOf(b);
...
});
```
`indexOf` sucht linear in der aktuell (halb-)sortierten Liste — der gefundene Index
stimmt nicht mehr mit dem ursprünglichen `ages`-Index überein.
## Fix
Score vorab in einer `Map<int, double>` (projectId → score) berechnen,
dann beim Sortieren direkt per ID nachschlagen.
## Acceptance Criteria
- [ ] Kein RangeError mehr beim Wechsel zwischen gescorten/ungescorten Projekten
- [ ] Sortierung bleibt korrekt (höchster Score zuerst)
## Files to modify
- `lib/features/timer/domain/frequent_projects_provider.dart`

View file

@ -0,0 +1,31 @@
# TT-020 — Makefile: build_and_serve_apk + build_and_serve_web Targets
**Type:** Task
**Priority:** High
**Labels:** makefile, devops
**Depends on:** TT-018, TT-019
**Blocks:** —
---
## Summary
Zwei neue Kombinations-Targets im Makefile anlegen, die Build und Serve in einem Schritt
zusammenfassen:
- `build_and_serve_apk`: baut das Android-APK und startet anschließend den APK-Server
- `build_and_serve_web`: baut die Flutter-Web-App und startet anschließend den Web-Server
Beide Targets sollen im `help`-Target dokumentiert sein.
## Acceptance Criteria
- [ ] `build_and_serve_apk` ruft `build_android` dann `serve_apk` auf
- [ ] `build_and_serve_web` ruft `build_web` dann `serve_web` auf
- [ ] Beide Targets in der `help`-Ausgabe sichtbar
- [ ] `make build_and_serve_apk` läuft durch (APK gebaut + Server gestartet)
- [ ] `make build_and_serve_web` läuft durch (Web gebaut + Server gestartet)
## Files to create / modify
- `Makefile`

123
AGENT.md Normal file
View file

@ -0,0 +1,123 @@
# AGENT.md — Timetrack
## Project Overview
Flutter time-tracking app for capturing and analysing working hours.
Targets: Android, iOS, Web. Data is stored locally (offline-first, no backend).
## Stack
| Concern | Choice |
|----------------|-------------------------------|
| Framework | Flutter 3.41 / Dart 3.11 |
| State | Riverpod (riverpod_annotation)|
| Database | Drift + SQLite |
| Navigation | go_router (StatefulShellRoute)|
| Charts | fl_chart |
| Models | freezed + json_serializable |
| Export | pdf + csv + share_plus |
| i18n | flutter_localizations (ARB) |
| Tests | flutter_test + mocktail |
## Architecture — Feature-first
```
lib/
main.dart # ProviderScope → App
app.dart # MaterialApp.router, theme, locales
core/
database/ # Drift AppDatabase, DAOs
router/ # GoRouter provider (app_router.dart)
theme/ # AppTheme.light / AppTheme.dark
l10n/ # ARB files + AppLocalizations
features/
timer/ # Active timer — start/stop, project select
entries/ # TimeEntry CRUD + manual input
projects/ # Project CRUD (name, color, description)
reports/ # Day/week/month views + fl_chart
settings/ # Theme toggle, language, export
```
Each feature follows: `data/``domain/``presentation/`
## Data Models
### Project
```dart
int id, String name, int colorValue, String? description,
DateTime? archivedAt
```
### TimeEntry
```dart
int id, int projectId, DateTime startTime, DateTime? endTime,
Duration? duration, String? note, List<String> tags
```
### Tag
```dart
int id, String name
```
Junction table `time_entry_tags` links entries ↔ tags.
Full schema: see `.ai/database.md`
## Riverpod Conventions
- Use `@riverpod` annotation (code-gen); run `dart run build_runner watch`
- Providers live in the feature's `data/` or `domain/` layer
- Never put providers in `presentation/` widgets directly — import from domain/data
- AsyncNotifier for async state, Notifier for sync state
## Navigation
- `AppRoutes` constants in `lib/core/router/app_router.dart`
- Bottom nav: `/timer` | `/entries` | `/projects` | `/reports` | `/settings`
- Deep links use GoRouter sub-routes inside each branch
## Coding Conventions
- **Language in code**: English (variables, comments, docs)
- **UI strings**: ARB files only — never hardcode display text
- **Models**: always `freezed` + `copyWith`; no mutable model classes
- **Async**: always `await`; no fire-and-forget without `unawaited()` annotation
- **Imports**: always `package:` imports, no relative `../` imports
- **Formatting**: `dart format` enforced; trailing commas required
- **Single quotes** throughout
## Testing
See `.ai/testing.md` for full conventions.
- Unit tests for all repository methods and domain logic
- Widget tests for each screen (golden tests optional)
- Use `mocktail` for mocking repositories in widget tests
- Test files mirror `lib/` structure under `test/`
## .ai/ Context Files
| File | Content |
|--------------------------|------------------------------------------|
| `.ai/database.md` | Full Drift schema, migration strategy |
| `.ai/architecture.md` | ADRs, dependency decisions |
| `.ai/testing.md` | Test conventions, patterns, examples |
| `.ai/tickets.md` | Ticket workflow, template, agent rules |
| `.ai/features/timer.md` | Timer state-machine, background rules |
| `.ai/features/reports.md`| Report queries, chart data format |
| `.ai/features/export.md` | CSV/PDF/JSON export logic |
## Ticket Workflow
Every step in an implementation plan becomes a ticket under `.tasks/`.
Full rules, the ticket template, and the agent workflow are in `.ai/tickets.md`.
**Short version:**
1. Break implementation plan into tickets → create in `.tasks/todo/`
2. Move one ticket to `.tasks/processing/` before starting work
3. Move to `.tasks/done/` when complete, then pick the next ticket
4. Update the overview table in `.tasks/README.md` for every new ticket
## Interacting with the User
When any decision, clarification, or preference is needed — no matter how small —
**always use the `question` tool** rather than asking in plain text.
This applies to architecture choices, naming, scope, priority, and ambiguous requirements.
Only skip the question tool when the answer can be unambiguously derived from the codebase or existing documentation.
## Do / Don't
| Do | Don't |
|-------------------------------------------------|----------------------------------------------|
| Use `ConsumerWidget` / `ConsumerStatefulWidget` | Don't use `StatefulWidget` + manual setState |
| Keep screens thin — delegate to providers | Don't put business logic in widgets |
| Use `drift` DAOs for all DB access | Don't use raw SQL strings outside DAOs |
| Write tests alongside new features | Don't commit untested repository methods |
| Export via `share_plus` share sheet | Don't write files to arbitrary paths |
| Use `freezed` for all domain models | Don't use plain Dart classes for models |
| Single active timer — stop before starting new | Don't allow concurrent timer instances |

289
Makefile Normal file
View file

@ -0,0 +1,289 @@
# Timetrack — Build & Run Commands
# Flutter cross-platform build automation
.PHONY: help install generate test test_coverage analyze format clean \
build_serve build_android build_aab build_linux build_web build_all \
install_android deploy_android serve_apk serve_web \
build_and_serve_apk build_and_serve_web \
db_schema
# ── Toolchain paths ────────────────────────────────────────────────────────────
FLUTTER := $(HOME)/flutter/3.41.7/bin/flutter
JAVA_HOME ?= $(HOME)/java/17.0.13+11
ANDROID_HOME ?= $(HOME)/Android/sdk
ADB := $(ANDROID_HOME)/platform-tools/adb
# ── Versionierung ──────────────────────────────────────────────────────────────
# Werte werden aus der VERSION-Datei gelesen und nach jedem Build automatisch
# hochgezählt. Manuell überschreibbar:
# make build_android BUILD_VERSION=0.2.0 BUILD_NUMBER=5
#
# TODO: Sobald das Projekt in Git liegt, BUILD_NUMBER durch git-Commits ersetzen:
# BUILD_NUMBER = $(shell git rev-list --count HEAD)
VERSION_FILE := VERSION
BUILD_VERSION ?= $(shell grep '^VERSION=' $(VERSION_FILE) | cut -d= -f2)
BUILD_NUMBER ?= $(shell grep '^BUILD_NUMBER=' $(VERSION_FILE) | cut -d= -f2)
# ── APK-Pfade ─────────────────────────────────────────────────────────────────
APK_NAME := timetrack-$(BUILD_VERSION)+$(BUILD_NUMBER).apk
APK_DIR := build/apk
APK := $(APK_DIR)/$(APK_NAME)
APK_LATEST := $(APK_DIR)/timetrack.apk
# ── Serve-Binary ──────────────────────────────────────────────────────────────
SERVE_BIN := scripts/serve/serve
PORT ?= 8888
WEB_PORT ?= 8080
# ── Default target ─────────────────────────────────────────────────────────────
help:
@echo "Timetrack — Build Commands"
@echo "=========================="
@echo ""
@echo "Dev-Targets:"
@echo " make install - flutter pub get"
@echo " make generate - build_runner (Codegenerierung)"
@echo " make test - Flutter Tests ausführen"
@echo " make test_coverage - Tests mit Coverage-Report"
@echo " make analyze - flutter analyze"
@echo " make format - Dart-Code formatieren"
@echo " make clean - Build-Artefakte löschen"
@echo ""
@echo "Build-Targets:"
@echo " make build_serve - Go APK-Server kompilieren"
@echo " make build_android - Android APK (release)"
@echo " make build_aab - Android App Bundle (Play Store)"
@echo " make build_linux - Linux Desktop-App"
@echo " make build_web - Web-App"
@echo " make build_all - APK + Linux + Web"
@echo ""
@echo "Deploy-Targets:"
@echo " make install_android - APK via ADB auf Gerät installieren"
@echo " make deploy_android - build_android + install_android"
@echo " make serve_apk - APK per WLAN bereitstellen + QR-Code"
@echo " make serve_web - Web-App per WLAN bereitstellen + QR-Code"
@echo " make build_and_serve_apk - build_android + serve_apk"
@echo " make build_and_serve_web - build_web + serve_web"
@echo ""
@echo "Info-Targets:"
@echo " make db_schema - Drift-Schema-Version und Tabellen anzeigen"
@echo ""
@echo "Variablen (Beispiel):"
@echo " BUILD_VERSION=0.2.0 BUILD_NUMBER=5 make build_android"
@echo " PORT=9090 make serve_apk"
@echo " WEB_PORT=9091 make serve_web"
@echo " DEVICE=emulator-5554 make install_android"
# ── Dev-Targets ───────────────────────────────────────────────────────────────
install:
@echo "Installing dependencies..."
$(FLUTTER) pub get
@echo "Done."
generate:
@echo "Running code generation..."
$(FLUTTER) pub run build_runner build --delete-conflicting-outputs
@echo "Code generation complete."
test:
@echo "Running tests..."
$(FLUTTER) test
@echo "Tests complete."
test_coverage:
@echo "Running tests with coverage..."
$(FLUTTER) test --coverage
@if command -v genhtml > /dev/null 2>&1; then \
genhtml coverage/lcov.info -o coverage/html --quiet; \
echo "Coverage report: coverage/html/index.html"; \
else \
echo "Hinweis: genhtml nicht gefunden. lcov installieren für HTML-Report:"; \
echo " sudo apt install lcov"; \
echo "Rohes lcov-Ergebnis: coverage/lcov.info"; \
fi
analyze:
@echo "Running static analysis..."
$(FLUTTER) analyze
@echo "Analysis complete."
format:
@echo "Formatting code..."
$(FLUTTER) pub run dart_style:format --fix lib/ test/
@echo "Format complete."
clean:
@echo "Cleaning build artifacts..."
$(FLUTTER) clean
rm -rf coverage/
@echo "Clean complete."
# ── Build: Go Server ──────────────────────────────────────────────────────────
build_serve: $(SERVE_BIN)
# Auto-Build: Binary wird nur neu gebaut wenn main.go neuer als Binary ist
$(SERVE_BIN): scripts/serve/main.go
@echo "Building serve binary..."
@command -v go > /dev/null 2>&1 || \
(echo "FEHLER: go nicht gefunden. Bitte installieren: https://go.dev/dl/" && exit 1)
cd scripts/serve && go build -o serve .
@echo "Server binary: scripts/serve/serve"
# ── Build: Android APK ────────────────────────────────────────────────────────
build_android:
@echo "Building Timetrack Android APK..."
@echo " Version : $(BUILD_VERSION)+$(BUILD_NUMBER)"
@echo ""
@# Voraussetzungen prüfen
@test -d "$(JAVA_HOME)" || \
(echo "FEHLER: JAVA_HOME='$(JAVA_HOME)' nicht gefunden." && \
echo " Überschreiben: JAVA_HOME=/pfad/zu/jdk make build_android" && exit 1)
@test -d "$(ANDROID_HOME)" || \
(echo "FEHLER: ANDROID_HOME='$(ANDROID_HOME)' nicht gefunden." && \
echo " Überschreiben: ANDROID_HOME=/pfad/zum/sdk make build_android" && exit 1)
@# Flutter APK bauen
JAVA_HOME="$(JAVA_HOME)" ANDROID_HOME="$(ANDROID_HOME)" \
$(FLUTTER) build apk --release \
--build-name=$(BUILD_VERSION) \
--build-number=$(BUILD_NUMBER)
@# APK in build/apk/ ablegen
@mkdir -p $(APK_DIR)
cp build/app/outputs/flutter-apk/app-release.apk $(APK)
@# SHA256-Prüfsumme
sha256sum $(APK) | tee $(APK).sha256
@# Symlink auf neueste Version aktualisieren
@cd $(APK_DIR) && ln -sf $(APK_NAME) timetrack.apk
@# Build-Number in VERSION-Datei hochzählen
@next=$$(( $(BUILD_NUMBER) + 1 )); \
sed -i "s/^BUILD_NUMBER=.*/BUILD_NUMBER=$$next/" $(VERSION_FILE); \
echo " Nächste Build-Number: $$next (in $(VERSION_FILE) gespeichert)"
@# Retention-Bereinigung
@bash scripts/apk_retention.sh --dir $(APK_DIR) --keep 15
@echo ""
@echo "APK fertig:"
@find $(APK_DIR) -name "$(APK_NAME)" -type f
@echo ""
@echo "Auf Gerät installieren: make install_android"
@echo "WLAN-Download starten: make serve_apk"
# ── Build: Android App Bundle (Play Store) ───────────────────────────────────
build_aab:
@echo "Building Timetrack Android App Bundle (AAB)..."
@echo " Version : $(BUILD_VERSION)+$(BUILD_NUMBER)"
@test -d "$(JAVA_HOME)" || \
(echo "FEHLER: JAVA_HOME='$(JAVA_HOME)' nicht gefunden." && exit 1)
@test -d "$(ANDROID_HOME)" || \
(echo "FEHLER: ANDROID_HOME='$(ANDROID_HOME)' nicht gefunden." && exit 1)
JAVA_HOME="$(JAVA_HOME)" ANDROID_HOME="$(ANDROID_HOME)" \
$(FLUTTER) build appbundle --release \
--build-name=$(BUILD_VERSION) \
--build-number=$(BUILD_NUMBER)
@echo ""
@echo "AAB fertig:"
@find build/app/outputs/bundle -name "*.aab" -type f
@echo ""
@echo "Nächste Schritte:"
@echo " 1. AAB signieren (Keystore konfigurieren)"
@echo " 2. In Play Console hochladen: Testen → Internes Testen → Upload"
# ── Build: Linux Desktop ──────────────────────────────────────────────────────
build_linux:
@which ld.lld > /dev/null 2>&1 || \
(echo "" && \
echo "FEHLER: ld.lld (LLVM-Linker) nicht gefunden." && \
echo "Bitte installieren mit:" && \
echo " sudo apt install lld" && \
echo "" && exit 1)
@echo "Building Timetrack Linux app..."
$(FLUTTER) build linux --release \
--build-name=$(BUILD_VERSION) \
--build-number=$(BUILD_NUMBER)
@echo ""
@echo "Linux build fertig:"
@find build/linux -maxdepth 4 -name "timetrack" -type f 2>/dev/null || \
find build/linux -maxdepth 4 -name "*.elf" -type f 2>/dev/null || true
@echo ""
@echo "Starten: ./build/linux/x64/release/bundle/timetrack"
# ── Build: Web ────────────────────────────────────────────────────────────────
build_web:
@echo "Building Timetrack Web app..."
$(FLUTTER) build web --release
@echo ""
@echo "Web build fertig: build/web/"
@echo "WLAN-Zugriff starten: make serve_web"
# ── Build: Alle Plattformen ───────────────────────────────────────────────────
build_all: build_android build_linux build_web
@echo ""
@echo "Alle Builds abgeschlossen:"
@echo " APK : $(APK)"
@echo " Linux : build/linux/x64/release/bundle/timetrack"
@echo " Web : build/web/"
# ── Deploy: Android ───────────────────────────────────────────────────────────
install_android:
@echo "Suche verbundene Android-Geräte..."
@$(ADB) devices | grep -v "List of" | grep "device$$" > /dev/null || \
(echo "FEHLER: Kein Gerät gefunden. USB-Kabel und USB-Debugging prüfen." && exit 1)
@test -f "$(APK_LATEST)" || test -f "$(APK)" || \
(echo "FEHLER: APK nicht gefunden. Zuerst 'make build_android' ausführen." && exit 1)
@APK_TO_INSTALL="$(APK_LATEST)"; \
test -f "$$APK_TO_INSTALL" || APK_TO_INSTALL="$(APK)"; \
echo "Installiere $$APK_TO_INSTALL..."; \
if [ -n "$(DEVICE)" ]; then \
$(ADB) -s $(DEVICE) install -r "$$APK_TO_INSTALL"; \
else \
$(ADB) install -r "$$APK_TO_INSTALL"; \
fi
@echo ""
@echo "Installation abgeschlossen."
deploy_android: build_android install_android
# ── Serve + Build: Kombinierte Targets ───────────────────────────────────────
build_and_serve_apk: build_android serve_apk
build_and_serve_web: build_web serve_web
# ── Serve: WLAN APK-Download ──────────────────────────────────────────────────
serve_apk: $(SERVE_BIN)
@test -d "$(APK_DIR)" || \
(echo "FEHLER: $(APK_DIR)/ nicht gefunden. Zuerst 'make build_android' ausführen." && exit 1)
$(SERVE_BIN) --dir $(APK_DIR) --port $(PORT)
# ── Serve: WLAN Web ───────────────────────────────────────────────────────────
serve_web: $(SERVE_BIN)
@test -d "build/web" || \
(echo "FEHLER: build/web/ nicht gefunden. Zuerst 'make build_web' ausführen." && exit 1)
$(SERVE_BIN) --web --dir build/web --port $(WEB_PORT)
# ── Info: Drift-Schema ────────────────────────────────────────────────────────
db_schema:
@echo "Timetrack — Drift Schema"
@echo "========================"
@echo ""
@echo "Schema-Version:"
@grep -h "schemaVersion" lib/core/database/app_database.dart 2>/dev/null | \
grep -v "//" | sed 's/^[[:space:]]*/ /'
@echo ""
@echo "Tabellen:"
@grep -h "class.*extends Table" lib/core/database/tables/*.dart 2>/dev/null | \
sed 's/class \([A-Za-z]*\) extends Table.*/ \1/' | sort
@echo ""
@echo "DAOs:"
@grep -h "class.*Dao" lib/core/database/daos/*.dart 2>/dev/null | \
grep "class.*DatabaseAccessor" | \
sed 's/class \([A-Za-z]*\) extends.*/ \1/' | sort

322
Makefile.example Normal file
View file

@ -0,0 +1,322 @@
# Gullrune - Build & Run Commands
# Cross-platform build automation for Flet app
.PHONY: help run install test clean build_win build_android build_aab install_android deploy_android serve_apk build_ios build_linux build_all
# ── Toolchain paths (auto-detected by flet, override here if needed) ──────────
JAVA_HOME ?= $(HOME)/java/17.0.13+11
ANDROID_HOME ?= $(HOME)/Android/sdk
# ── Versionierung ──────────────────────────────────────────────────────────────
# Überschreibbar: make build_aab BUILD_VERSION=1.1.0 BUILD_NUMBER=2
BUILD_VERSION ?= 1.0.0
BUILD_NUMBER ?= 1
# Default target
help:
@echo "Gullrune - Build Commands"
@echo "========================="
@echo ""
@echo "Available targets:"
@echo " make run - Run the app in development mode"
@echo " make install - Install all dependencies"
@echo " make test - Run security tests"
@echo " make clean - Clean build artifacts"
@echo ""
@echo "Build targets:"
@echo " make build_win - Build Windows desktop app"
@echo " make build_android - Build Android APK"
@echo " make install_android - Install APK auf verbundenem Android-Gerät (USB)"
@echo " make deploy_android - Build + Install in einem Schritt"
@echo " make serve_apk - APK per lokalem HTTP-Server im WLAN bereitstellen"
@echo " make build_ios - Build iOS IPA (macOS only)"
@echo " make build_linux - Build Linux app"
@echo " make build_all - Build for all platforms"
@echo ""
@echo "Migration targets:"
@echo " make migrate - Run Alembic migrations"
@echo " make migrate_create - Create new migration"
# Run the app in development mode
run:
@echo "Starting Gullrune..."
uv run python main.py
# Install dependencies
install:
@echo "Installing dependencies..."
pip install -r requirements.txt
@echo "Done! All dependencies installed."
# Run tests
test:
@echo "Running security tests..."
pytest tests/ -v
@echo "Tests completed."
# Run tests with coverage
test_coverage:
@echo "Running tests with coverage..."
pytest tests/ --cov=src --cov-report=html
@echo "Coverage report generated in htmlcov/"
# Clean build artifacts
clean:
@echo "Cleaning build artifacts..."
rm -rf build/
rm -rf dist/
rm -rf *.egg-info
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
find . -type f -name "*.pyc" -delete 2>/dev/null || true
@echo "Clean complete."
# Build Windows desktop app
build_win:
@echo "Building Windows desktop application..."
@echo "This will create a standalone .exe in build/windows/"
flet build windows --verbose
@echo "Windows build complete! Check build/windows/"
# Build Android APK
# ─────────────────────────────────────────────────────────────────────────────
# Voraussetzungen (werden beim ersten Build automatisch installiert falls nötig):
# - JDK 17 → $(HOME)/java/17.0.13+11 (auto-install by flet)
# - Android SDK 36 + Build-Tools 28.0.3 (auto-install by flet)
#
# Bekannte Fallstricke:
# - flet[all] in pyproject.toml zieht watchdog rein → kein Android-Wheel.
# Lösung: [project].dependencies nutzt nur "flet==...", flet[all] liegt
# unter [project.optional-dependencies].dev
# - Dart SDK-Cache nach Flutter-Update leeren:
# rm -rf $(HOME)/flutter/3.41.4/bin/cache
#
# Ausgabe: build/apk/gullrune.apk
# ─────────────────────────────────────────────────────────────────────────────
build_android:
@echo "Building Gullrune Android APK..."
@echo "Erster Build: 10-20 Minuten (Gradle + SDK-Download)"
@echo "Folge-Builds: ~3-5 Minuten (Cache)"
uv run python3 scripts/gen_version.py --version $(BUILD_VERSION) --build $(BUILD_NUMBER)
JAVA_HOME="$(JAVA_HOME)" ANDROID_HOME="$(ANDROID_HOME)" \
uv run flet build apk \
--project gullrune \
--product "Gullrune" \
--org com.keldamar \
--bundle-id com.keldamar.gullrune \
--build-version $(BUILD_VERSION) \
--build-number $(BUILD_NUMBER) \
--splash-color "#1A2A38" \
--splash-dark-color "#1A2A38" \
--android-adaptive-icon-background "#2A6F9E" \
--compile-app \
--compile-packages \
--exclude tests .git .venv user_data
@echo ""
@echo "APK fertig:"
@find build/apk -name "*.apk" -type f
@echo ""
@sha256sum build/apk/gullrune.apk | tee build/apk/gullrune.apk.sha256
@echo ""
@echo "Auf Gerät installieren: adb install build/apk/gullrune.apk"
@echo "WLAN-Download starten: make serve_apk"
# Build Android App Bundle (AAB) — für den Google Play Store
# ─────────────────────────────────────────────────────────────────────────────
# AAB ist das von Google empfohlene Format für den Play Store.
# Google Play optimiert die Auslieferung pro Gerät automatisch.
#
# Vor dem Upload: AAB mit Upload-Keystore signieren (siehe Signing-Anleitung).
# Ausgabe: build/aab/gullrune.aab
# ─────────────────────────────────────────────────────────────────────────────
build_aab:
@echo "Building Gullrune Android App Bundle (AAB) für Play Store..."
uv run python3 scripts/gen_version.py --version $(BUILD_VERSION) --build $(BUILD_NUMBER)
JAVA_HOME="$(JAVA_HOME)" ANDROID_HOME="$(ANDROID_HOME)" \
uv run flet build aab \
--project gullrune \
--product "Gullrune" \
--org com.keldamar \
--bundle-id com.keldamar.gullrune \
--build-version $(BUILD_VERSION) \
--build-number $(BUILD_NUMBER) \
--splash-color "#1A2A38" \
--splash-dark-color "#1A2A38" \
--android-adaptive-icon-background "#2A6F9E" \
--compile-app \
--compile-packages \
--exclude tests .git .venv user_data
@echo ""
@echo "AAB fertig:"
@find build/aab -name "*.aab" -type f
@echo ""
@sha256sum build/aab/gullrune.aab | tee build/aab/gullrune.aab.sha256
@echo ""
@echo "Nächste Schritte:"
@echo " 1. AAB mit Upload-Keystore signieren (falls noch nicht via flet signing konfiguriert)"
@echo " 2. In Play Console hochladen: Test and release → Internal testing → Upload"
# Install Android APK auf verbundenem Gerät
# ─────────────────────────────────────────────────────────────────────────────
# Voraussetzungen:
# - Android-Gerät per USB verbunden, USB-Debugging aktiviert
# - APK muss bereits gebaut sein (make build_android)
#
# Mehrere Geräte: DEVICE=<serial> make install_android
# Serials anzeigen mit: adb devices
# ─────────────────────────────────────────────────────────────────────────────
APK := build/apk/gullrune.apk
ADB := $(ANDROID_HOME)/platform-tools/adb
install_android:
@echo "Suche verbundene Android-Geräte..."
@$(ADB) devices | grep -v "List of" | grep "device$$" > /dev/null || \
(echo "FEHLER: Kein Gerät gefunden. USB-Kabel und USB-Debugging prüfen." && exit 1)
@test -f "$(APK)" || \
(echo "FEHLER: $(APK) nicht gefunden. Zuerst 'make build_android' ausführen." && exit 1)
@echo "Installiere $(APK)..."
$(if $(DEVICE), \
$(ADB) -s $(DEVICE) install -r "$(APK)", \
$(ADB) install -r "$(APK)" \
)
@echo ""
@echo "Installation abgeschlossen. App auf Gerät starten:"
$(if $(DEVICE), \
$(ADB) -s $(DEVICE) shell am start -n com.keldamar.gullrune/.MainActivity, \
$(ADB) shell am start -n com.keldamar.gullrune/.MainActivity \
)
# Shortcut: Build + Install in einem Schritt
deploy_android: build_android install_android
# APK per lokalem HTTP-Server im WLAN bereitstellen
# ─────────────────────────────────────────────────────────────────────────────
# Das Handy muss im selben WLAN wie dieser Rechner sein.
# Ablauf:
# 1. QR-Code im Terminal scannen
# 2. Im Android-Browser die angezeigte URL öffnen
# 3. APK herunterladen und "Installieren" tippen
# 4. Einmalig: Einstellungen → Sicherheit → Browser als Installationsquelle erlauben
#
# Port überschreiben: PORT=9090 make serve_apk
# ─────────────────────────────────────────────────────────────────────────────
PORT ?= 8888
serve_apk:
@test -f "$(APK)" || \
(echo "FEHLER: $(APK) nicht gefunden. Zuerst 'make build_android' ausführen." && exit 1)
uv run python3 scripts/serve_apk.py --dir build/apk --port $(PORT)
serve_apk_debug:
@test -f "$(APK)" || \
(echo "FEHLER: $(APK) nicht gefunden. Zuerst 'make build_android' ausführen." && exit 1)
uv run python3 scripts/serve_apk.py --dir build/apk --port $(PORT) --debug
# Build iOS IPA (macOS only)
build_ios:
@echo "Building iOS IPA..."
@echo "Requirements: macOS with Xcode installed"
@echo "This may take 10-20 minutes on first build..."
flet build ipa --verbose
@echo "iOS IPA build complete! Check build/ipa/"
# Build Linux desktop app
# ─────────────────────────────────────────────────────────────────────────────
# Voraussetzung: lld (LLVM-Linker) muss installiert sein.
# Falls nicht vorhanden:
# sudo apt install lld
#
# Das Target prüft dies automatisch und gibt einen klaren Hinweis.
#
# Ausgabe: build/linux/gullrune (ausführbare Datei + shared libs)
# ─────────────────────────────────────────────────────────────────────────────
build_linux:
@which ld.lld > /dev/null 2>&1 || \
(echo "" && \
echo "FEHLER: ld.lld (LLVM-Linker) nicht gefunden." && \
echo "Bitte installieren mit:" && \
echo " sudo apt install lld" && \
echo "" && exit 1)
@echo "Building Gullrune Linux app..."
uv run flet build linux \
--project gullrune \
--product "Gullrune" \
--build-version 1.0.0 \
--build-number 1 \
--compile-app \
--compile-packages \
--exclude tests .git .venv user_data
@echo ""
@echo "Linux build fertig:"
@find build/linux -maxdepth 2 -name "gullrune" -type f
@echo ""
@echo "Starten: ./build/linux/gullrune"
# Build for all platforms (use with caution)
build_all: build_win build_linux
@echo "All platform builds complete!"
@echo "Note: Android and iOS builds skipped (use build_android/build_ios separately)"
# Database migration targets
migrate:
@echo "Running database migrations..."
alembic upgrade head
@echo "Migrations complete."
migrate_create:
@echo "Creating new migration..."
@read -p "Migration message: " message; \
alembic revision --autogenerate -m "$$message"
@echo "Migration created in alembic/versions/"
migrate_downgrade:
@echo "Rolling back last migration..."
alembic downgrade -1
@echo "Rollback complete."
# Relay server
# ─────────────────────────────────────────────────────────────────────────────
# Startet den Gullrune Relay-Server lokal (für Entwicklung / Test).
# Für Produktion: systemd-Service oder Docker auf einem VPS.
#
# Beispiel Docker:
# docker run -d -p 8765:8765 \
# -v $(PWD)/server:/app \
# python:3.12-slim \
# sh -c "pip install websockets && python /app/relay.py"
# ─────────────────────────────────────────────────────────────────────────────
RELAY_PORT ?= 8765
relay:
@echo "Starting Gullrune relay server on ws://0.0.0.0:$(RELAY_PORT) ..."
uv run python server/relay.py --port $(RELAY_PORT)
# Development helpers
dev: install run
format:
@echo "Formatting code with black..."
black src/ tests/ main.py
@echo "Code formatted."
lint:
@echo "Linting code..."
pylint src/ main.py || true
@echo "Lint complete."
# Package for distribution
package_win: build_win
@echo "Creating Windows installer..."
@echo "TODO: Add NSIS or InnoSetup script"
package_android: build_android
@echo "Android APK ready for distribution at:"
@find build/apk -name "*.apk" -type f
# Security check
security_check:
@echo "Running security checks..."
@echo "Checking for .env in git..."
@git check-ignore .env && echo "✓ .env is ignored" || echo "✗ WARNING: .env not ignored!"
@echo "Checking database encryption..."
@grep -q "DB_ENCRYPTION_KEY" .env && echo "✓ Encryption key exists" || echo "✗ WARNING: No encryption key!"
@echo "Security check complete."

17
README.md Normal file
View file

@ -0,0 +1,17 @@
# timetrack
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

86
REPOMAP.md Normal file
View file

@ -0,0 +1,86 @@
# REPOMAP — Timetrack
> Machine-readable overview of the repository. Every important file and
> directory with a one-line description. Update this file when adding new
> features or renaming paths.
## Root
| Path | Description |
|--------------------------|--------------------------------------------------|
| `pubspec.yaml` | Dependencies and Flutter config |
| `analysis_options.yaml` | Dart linter rules |
| `AGENT.md` | AI agent context — project rules and conventions |
| `REPOMAP.md` | This file — repository map |
## lib/
| Path | Description |
|---------------------------------------------------|-----------------------------------------------------|
| `lib/main.dart` | Entry point — ProviderScope wraps App |
| `lib/app.dart` | MaterialApp.router — theme, locales, router |
### lib/core/
| Path | Description |
|---------------------------------------------------|-----------------------------------------------------|
| `lib/core/database/app_database.dart` | Drift AppDatabase — tables, DAOs, connection setup |
| `lib/core/router/app_router.dart` | GoRouter provider — routes, bottom nav shell |
| `lib/core/theme/app_theme.dart` | AppTheme.light / AppTheme.dark (Material 3) |
| `lib/core/l10n/` | ARB localisation files (en, de, …) |
### lib/features/timer/
| Path | Description |
|---------------------------------------------------|-----------------------------------------------------|
| `lib/features/timer/data/timer_repository.dart` | TimerRepository interface |
| `lib/features/timer/domain/` | TimerState (freezed), TimerNotifier (Riverpod) |
| `lib/features/timer/presentation/timer_screen.dart` | Timer UI — start/stop, project picker, elapsed |
### lib/features/entries/
| Path | Description |
|---------------------------------------------------|-----------------------------------------------------|
| `lib/features/entries/data/entries_repository.dart` | EntriesRepository — CRUD TimeEntry |
| `lib/features/entries/domain/` | TimeEntry model (freezed), EntriesNotifier |
| `lib/features/entries/presentation/entries_screen.dart` | List of time entries, manual add/edit |
### lib/features/projects/
| Path | Description |
|---------------------------------------------------|-----------------------------------------------------|
| `lib/features/projects/data/projects_repository.dart` | ProjectsRepository — CRUD Project |
| `lib/features/projects/domain/` | Project model (freezed), ProjectsNotifier |
| `lib/features/projects/presentation/projects_screen.dart` | Project list, create/edit/archive |
### lib/features/reports/
| Path | Description |
|---------------------------------------------------|-----------------------------------------------------|
| `lib/features/reports/data/reports_repository.dart` | Report queries — aggregation by day/week/month |
| `lib/features/reports/domain/` | ReportData model, ReportsNotifier |
| `lib/features/reports/presentation/reports_screen.dart` | Day/week/month charts (fl_chart) |
### lib/features/settings/
| Path | Description |
|---------------------------------------------------|-----------------------------------------------------|
| `lib/features/settings/presentation/settings_screen.dart` | Language, theme, export, about |
## test/
| Path | Description |
|---------------------------------------------------|-----------------------------------------------------|
| `test/features/timer/` | Unit + widget tests for timer feature |
| `test/features/entries/` | Unit + widget tests for entries feature |
| `test/features/projects/` | Unit + widget tests for projects feature |
| `test/features/reports/` | Unit + widget tests for reports feature |
| `test/core/` | Database migration tests, router tests |
## .ai/
| Path | Description |
|----------------------------|-------------------------------------------------------|
| `.ai/database.md` | Full Drift schema, DAO list, migration strategy |
| `.ai/architecture.md` | ADRs — why Riverpod, Drift, go_router, feature-first |
| `.ai/testing.md` | Test patterns, mocktail conventions, coverage goals |
| `.ai/features/timer.md` | Timer state machine, restore-on-launch logic |
| `.ai/features/reports.md` | Report queries, fl_chart data format, filters |
| `.ai/features/export.md` | CSV/PDF/JSON schemas, share_plus flow |
## Generated Files (do not edit manually)
| Pattern | Generator |
|---------------------------|------------------|
| `**/*.g.dart` | build_runner |
| `**/*.freezed.dart` | freezed |
| `lib/core/router/app_router.g.dart` | riverpod_generator |

2
VERSION Normal file
View file

@ -0,0 +1,2 @@
VERSION=0.1.0
BUILD_NUMBER=10

25
analysis_options.yaml Normal file
View file

@ -0,0 +1,25 @@
include: package:flutter_lints/flutter.yaml
linter:
rules:
# Style
always_use_package_imports: true
prefer_single_quotes: true
require_trailing_commas: true
# Safety
avoid_dynamic_calls: true
avoid_print: true
cancel_subscriptions: true
close_sinks: true
unawaited_futures: true
# Riverpod / architecture
public_member_api_docs: false
analyzer:
errors:
invalid_annotation_target: ignore # freezed generates these
exclude:
- "**/*.g.dart"
- "**/*.freezed.dart"

14
android/.gitignore vendored Normal file
View file

@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View file

@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.timetrack.timetrack"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.timetrack.timetrack"
// 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
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
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")
}
}
}
flutter {
source = "../.."
}

View file

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View file

@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="timetrack"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View file

@ -0,0 +1,5 @@
package com.timetrack.timetrack
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View file

@ -0,0 +1,12 @@
<?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> -->
</layer-list>

View file

@ -0,0 +1,12 @@
<?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> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,18 @@
<?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">
<!-- 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>
</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>

View file

@ -0,0 +1,18 @@
<?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">
<!-- 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>
</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>

View file

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

24
android/build.gradle.kts Normal file
View file

@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View file

@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true

View file

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip

View file

@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")

34
ios/.gitignore vendored Normal file
View file

@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
</dict>
</plist>

View file

@ -0,0 +1 @@
#include "Generated.xcconfig"

View file

@ -0,0 +1 @@
#include "Generated.xcconfig"

View file

@ -0,0 +1,620 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.timetrack.timetrack;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.timetrack.timetrack.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.timetrack.timetrack.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.timetrack.timetrack.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.timetrack.timetrack;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.timetrack.timetrack;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View file

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View file

@ -0,0 +1,16 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
}

View file

@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View file

@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

70
ios/Runner/Info.plist Normal file
View file

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Timetrack</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>timetrack</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

View file

@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View file

@ -0,0 +1,6 @@
import Flutter
import UIKit
class SceneDelegate: FlutterSceneDelegate {
}

View file

@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

4
l10n.yaml Normal file
View file

@ -0,0 +1,4 @@
arb-dir: lib/core/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations

Some files were not shown because too many files have changed in this diff Show more