initial commit
46
.ai/architecture.md
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Feature: Reports
|
||||||
|
|
||||||
|
## Views
|
||||||
|
| View | Period | Grouping |
|
||||||
|
|---------|---------------|-----------------------|
|
||||||
|
| Daily | Selected day | Per entry (list) |
|
||||||
|
| Weekly | Mon–Sun | 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
|
|
@ -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
|
|
@ -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
|
|
@ -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. 1–2 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
|
|
@ -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
|
|
@ -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
|
|
@ -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. 1–2 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 |
|
||||||
36
.tasks/done/TT-001-dependencies-setup.md
Normal 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
|
||||||
50
.tasks/done/TT-002-drift-database-schema.md
Normal 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
|
||||||
100
.tasks/done/TT-003-domain-models-freezed.md
Normal 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.
|
||||||
92
.tasks/done/TT-004-repository-implementations.md
Normal 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
|
||||||
114
.tasks/done/TT-005-riverpod-providers-notifiers.md
Normal 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`
|
||||||
76
.tasks/done/TT-006-timer-feature-ui.md
Normal 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`
|
||||||
73
.tasks/done/TT-007-entries-feature-ui.md
Normal 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`
|
||||||
69
.tasks/done/TT-008-projects-feature-ui.md
Normal 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`
|
||||||
82
.tasks/done/TT-009-reports-feature-ui.md
Normal 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 (0–23) | Minutes per hour |
|
||||||
|
| Week | Weekdays (Mo–Su)| Hours per day |
|
||||||
|
| Month | Weeks 1–5 | 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
|
||||||
98
.tasks/done/TT-010-settings-export-feature.md
Normal 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`.
|
||||||
131
.tasks/done/TT-011-localisation-i18n.md
Normal 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`
|
||||||
96
.tasks/done/TT-012-unit-tests.md
Normal 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
|
||||||
|
```
|
||||||
93
.tasks/done/TT-013-widget-tests.md
Normal 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
|
||||||
53
.tasks/done/TT-014-go-serve-binary.md
Normal 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`
|
||||||
57
.tasks/done/TT-015-apk-retention-script.md
Normal 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`)
|
||||||
106
.tasks/done/TT-016-makefile.md
Normal 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`
|
||||||
48
.tasks/done/TT-017-integration-test.md
Normal 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
|
||||||
31
.tasks/done/TT-018-go-server-web-flag.md
Normal 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`
|
||||||
22
.tasks/done/TT-018-quick-access-fallback-projects.md
Normal 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`
|
||||||
32
.tasks/done/TT-019-makefile-serve-web.md
Normal 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`
|
||||||
25
.tasks/done/TT-019-quick-access-grid-fill.md
Normal 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`
|
||||||
36
.tasks/done/TT-020-fix-rangerror-scored-sort.md
Normal 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`
|
||||||
31
.tasks/done/TT-020-makefile-build-and-serve.md
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
||||||
|
VERSION=0.1.0
|
||||||
|
BUILD_NUMBER=10
|
||||||
25
analysis_options.yaml
Normal 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
|
|
@ -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
|
||||||
44
android/app/build.gradle.kts
Normal 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 = "../.."
|
||||||
|
}
|
||||||
7
android/app/src/debug/AndroidManifest.xml
Normal 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>
|
||||||
45
android/app/src/main/AndroidManifest.xml
Normal 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>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.timetrack.timetrack
|
||||||
|
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
class MainActivity : FlutterActivity()
|
||||||
12
android/app/src/main/res/drawable-v21/launch_background.xml
Normal 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>
|
||||||
12
android/app/src/main/res/drawable/launch_background.xml
Normal 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>
|
||||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 544 B |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 442 B |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 721 B |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
18
android/app/src/main/res/values-night/styles.xml
Normal 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>
|
||||||
18
android/app/src/main/res/values/styles.xml
Normal 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>
|
||||||
7
android/app/src/profile/AndroidManifest.xml
Normal 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
|
|
@ -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)
|
||||||
|
}
|
||||||
2
android/gradle.properties
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
|
android.useAndroidX=true
|
||||||
5
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
|
||||||
26
android/settings.gradle.kts
Normal 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
|
|
@ -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
|
||||||
24
ios/Flutter/AppFrameworkInfo.plist
Normal 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>
|
||||||
1
ios/Flutter/Debug.xcconfig
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
#include "Generated.xcconfig"
|
||||||
1
ios/Flutter/Release.xcconfig
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
#include "Generated.xcconfig"
|
||||||
620
ios/Runner.xcodeproj/project.pbxproj
Normal 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 */;
|
||||||
|
}
|
||||||
7
ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
101
ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
Normal 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>
|
||||||
7
ios/Runner.xcworkspace/contents.xcworkspacedata
generated
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "group:Runner.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
16
ios/Runner/AppDelegate.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
122
ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 282 B |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 704 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 586 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 762 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
23
ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
5
ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
vendored
Normal 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.
|
||||||
37
ios/Runner/Base.lproj/LaunchScreen.storyboard
Normal 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>
|
||||||
26
ios/Runner/Base.lproj/Main.storyboard
Normal 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
|
|
@ -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>
|
||||||
1
ios/Runner/Runner-Bridging-Header.h
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
#import "GeneratedPluginRegistrant.h"
|
||||||
6
ios/Runner/SceneDelegate.swift
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class SceneDelegate: FlutterSceneDelegate {
|
||||||
|
|
||||||
|
}
|
||||||
12
ios/RunnerTests/RunnerTests.swift
Normal 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
|
|
@ -0,0 +1,4 @@
|
||||||
|
arb-dir: lib/core/l10n
|
||||||
|
template-arb-file: app_en.arb
|
||||||
|
output-localization-file: app_localizations.dart
|
||||||
|
output-class: AppLocalizations
|
||||||