- Dart 88.5%
- C++ 4.5%
- CMake 3.6%
- Kotlin 1.9%
- Makefile 0.5%
- Other 1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .forgejo/workflows | ||
| .githooks | ||
| android | ||
| ios | ||
| lib | ||
| linux | ||
| macos | ||
| test | ||
| tool | ||
| windows | ||
| .gitignore | ||
| .metadata | ||
| analysis_options.yaml | ||
| CONTRIBUTING.md | ||
| Makefile | ||
| pubspec.lock | ||
| pubspec.yaml | ||
| README.md | ||
Contacts Manager
Contact account transfer manager for Flutter.
The app moves contacts between the contact accounts configured on the device
(Google, Outlook, the local Device bucket, …) from a single screen: one list
shows the source account's contacts, a dropdown below the app bar picks the
target account, and a per-row swipe stages the contact for the move. Staged
moves queue in a move list — opened from the outbox action at any time — where
each entry shows its from → to account pair, can be removed individually, and —
after a confirmation — executes as one batch with a single Undo. The address
book is the device's real one, read through flutter_contacts and written
through a first-party raw-contact channel.
Transfer flow
- Source view — the list shows one account's contacts, switched through the app-bar account menu. The search field below the target dropdown filters it by name.
- Target account — the dropdown offers every account except the source; it is disabled while the device has only one account.
- Swipe to stage — dragging a row right stages it for the current target (dragging left unstages); a long-press enters selection mode, where select-all and stage-all operate on the visible rows. Nothing is moved by staging — it only queues the instruction.
- Move list — the outbox action (badge counting staged moves) opens the pending list: one row per staged contact, its from → to pair with account pills, a per-row remove, and a "Move N contacts" button that first asks for confirmation ("Move N contacts from A to B?"). Confirming runs the batch: the button is replaced by a determinate progress panel ("Moving k of N") with an Abort action. Aborting stops between contacts — never mid-move — reverts everything already moved, and re-stages the batch. Each move that lands leaves the list; each failure keeps its entry staged and is reported. The all-success outcome returns to the transfer screen with a snackbar whose one Undo reverses the whole batch in reverse completion order and re-queues the reverted contacts as a staged batch — the pending list ends up exactly as it was before the move; the snackbar dismisses itself after ten seconds.
Accounts and colors
Accounts are derived from raw-contact attribution — an account exists iff at least one raw contact belongs to it. Each account gets a deterministic identity accent: FNV-1a 32-bit over the account id's UTF-8 bytes selects one of eight hue slots, shaded by theme brightness. The same id resolves to the same color on every run; there is no registry and no persistence.
Permissions
The app requests READ_CONTACTS and WRITE_CONTACTS together in one system
dialog, asked only on the first explicit "Grant access" tap — never
automatically on launch. Until access is granted the app body is replaced by
a blocked state offering the dialog, or the system settings page when
permission is permanently denied. The manifest declares no INTERNET
permission: the app never touches the network.
Data safety
- Create before delete — a transfer first creates the contact in the target account and only then deletes the source's raw-contact rows, so a failed second write never loses the contact.
- Source-account-only deletion — exactly the raw contacts the move proved as its source are deleted; nothing else on the device is touched.
- Undo semantics — a move returns a receipt; Undo recreates the contact in its source account from the current device data (edits made since the move are kept — only the account change is reversed) and deletes the created rows. A receipt made stale by outside changes is skipped, not retried; the rest of the batch still unwinds.
Quickstart
flutter pub get
flutter run # default platform
flutter run -d linux # Linux desktop
flutter build apk --debug --target-platform android-arm64
make hooks # install pre-commit / commit-msg git hooks
Verification
make check # format-check + analyze + full headless test suite
make test # unit + widget + golden + tool tests (headless)
make coverage # suite with --coverage + line-coverage gate
make update-goldens # regenerate golden references after UI changes
make check is the one gate: run it before every commit. make coverage
enforces the line-coverage floor (COVERAGE_MIN, default 0.85) through
tool/coverage_gate.dart. Golden tests pin the rendered UI to committed
reference PNGs (13 images), so a visual regression fails the suite in headless
CI; regenerate them with make update-goldens only after an intentional UI
change, and review the diff.
Test layout
test/
├── unit/ Business logic & data mapping (no widgets, no platform channels)
├── widget/ Widgets/screens in isolation, headless
├── golden/ Rendered UI pinned to committed reference PNGs
├── fixtures/ Deterministic shared test data
├── fakes/ Fake platform-interface implementations
└── tool/ Tests for the repo's tool/ scripts
Interactive widgets expose stable Keys (from
lib/core/constants/app_keys.dart) so tests and automation can drive the app
without matching on text.
Architecture
Clean architecture with three layers:
lib/
├── core/ Constants, utilities, extensions
├── data/ Data sources, models, repository implementations
├── domain/ Entities, use cases, repository interfaces
└── presentation/ UI screens, widgets, providers, routing
State management is Riverpod, routing is GoRouter. The code
generation toolchain (make gen: build_runner, freezed,
json_serializable) is set up in pubspec.yaml for when generated models
arrive; none exist yet, so day-to-day edits need no generation step.
Contributing
Development workflow, test pyramid, branch naming, and the review loop are documented in CONTRIBUTING.md. Every feature follows it.