feat(todo): the four tabs in Todo's frame, washed in strawberry, Settings from Browse

ADR 0033, step 1: go_router with a StatefulShellRoute and halkyon_ui's
NavigationFrame as Train's (D1), the tabs Today, Upcoming, Inbox and
Browse, each saying tasks come later (D2), every screen washing down in
strawberry (D3), and Settings over the tabs from Browse's cog, grouped in
cards: the operator's, the account, What's new and About Halkyon Todo,
the version below (D4). The account sheet and the empty home go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-01 00:57:44 +01:00
commit 492c657980
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
22 changed files with 788 additions and 178 deletions

View file

@ -4,6 +4,6 @@ What holds for every app and package of this repository is in the [root AGENTS.m
- **A shell on the shared packages** (decided 2026-10-19; what Todo is to do and how it looks: ADR 0033, decided 2026-11-01): signing in, the instances, the account and the look work; nothing of a person's is kept or synced yet, which waits for Todo's first model (the developer decides what it keeps and syncs). Packages: `halkyon_todo_server`, `halkyon_todo_client` (generated, never edited), `halkyon_todo_flutter`; app ID `app.halkyon.todo` on every platform; no web build. Licences as Learn's (server SSPL, client and app AGPL; `LICENSE.md`).
- **The server** includes `halkyon_base` (ADR 0014) and configures it in `server.dart` (`todoBase()`, `lib/src/todo_base.dart`): its basics say it serves `todo` (never change it once apps use it), sign-in mails name Halkyon Todo, the source is this repository, `minimumAppBuild` 1, no data of its own to delete with a person yet (add a `deletePersonData` hook with Todo's first table). Authentication through the module's builders: a password, SimpleLogin on the register, email codes proving an address, server-side sessions; no anonymous accounts (ADR 0020, D25), nor GitHub (D36). **Accounts** (ADR 0016): Todo holds its own register when it runs alone, and is a member of the instance's register (Learn's server on learn.halkyon.eu) where the operator sets `ACCOUNT_REGISTER_URL` (`todoBase(accounts: AccountsMode.fromEnvironment())`) with the instance's shared `INSTANCE_SECRET` (`instanceSecret`); then sign-in, handles and profiles are the register's, the email settings are the register's alone, and an identified person's deletion goes to the register first. `addInstanceRoutes` serves the calls between the services on its web server, and the server doesn't start without the secret where it needs it. `schedulePruning` (`lib/src/pruning.dart`) keeps the module's pruning scheduled, one entry each however often the server starts (and cancels the one under the pruning's name before ADR 0020, D25): the anonymous accounts left from before daily, sign-up attempts and codes hourly, as the register the deletions its members haven't confirmed hourly, as a member the people the register no longer knows daily (each a no-op in the other mode). Session logs are never persisted, in any run mode. Migrations (2026-10-19): the initial one (Serverpod's tables, its auth modules' and the module's `email_identity`, `email_code_request`), `profiles` (`public_profile`) and `deletion-notices` (`account_deletion_notice`), nothing of Todo's own. Tests (`test/integration/todo_server_test.dart`) call the module's endpoints through the server's own dispatch (`halkyon_base.<endpoint>`), since the generated test tools expose the project's endpoints alone.
- **The app** is halkyon_app's `InstanceGate` and `SessionGate` with Todo's frame (`todoFrame`: "Todo" under "Halkyon", its icon, `todo.example.org`, its own sentence) and `ServerpodInstanceChecker(app: 'todo')`; `TodoDependencies` (`AppDependencies<TodoPerson>`) on one instance, `TodoPerson` (empty) per person; a person's app with an empty home and the account sheet: halkyon_app's `AccountTiles` (whose account on which instance, Switch account, Sign out, Delete account, which asks first) in Todo's words, and halkyon_app's About (`showAppAbout`: the source of what the instance runs, from `TodoDependencies.sourceCodeUrl`, and the licence list). Texts through gen-l10n in British English (the template), German (informal du), an empty `en_US` and `en_GB` (`test/l10n/arb_files_test.dart` keeps them in step). The look follows the system's theme mode and language until Todo has settings. Countries in where an instance keeps its data are named by halkyon_app's CLDR names (`AssetCountryNames`), its language picked by halkyon_app's `resolveAppLocale`. Widget tests start Todo's whole tree with halkyon_app's fakes (`test/app/todo_app_test.dart`).
- **The app** is halkyon_app's `InstanceGate` and `SessionGate` with Todo's frame (`todoFrame`: "Todo" under "Halkyon", its icon, `todo.example.org`, its own sentence) and `ServerpodInstanceChecker(app: 'todo')`; `TodoDependencies` (`AppDependencies<TodoPerson>`) on one instance, `TodoPerson` (empty) per person; a person's app in Todo's frame (ADR 0033, D1 to D4, since 1 Nov 2026), as Train's: go_router (`buildTodoRouter`, `TodoPaths`) with a `StatefulShellRoute`, halkyon_ui's `NavigationFrame` (`TodoShell`) with the tabs Today (where it opens), Upcoming, Inbox and Browse, each saying tasks come later until they do (`lib/src/tabs/`), every screen washing down in strawberry (`ScreenColor`, `TabScreen`); Settings (`lib/src/settings/`) over the tabs from Browse's cog, grouped in `SectionCard`s: the operator's card, the account (halkyon_app's `AccountTiles`, whose account on which instance, Switch account, Sign out, Delete account, which asks first, with SimpleLogin and the account's security) in Todo's words, What's new and halkyon_app's About (`showAppAbout`: the source of what the instance runs, from `TodoDependencies.sourceCodeUrl`, and the licence list), the version below (`TodoDependencies.appVersion`). What's new shows nothing yet: `main.dart` gives no `WhatsNewScope` until Todo's first release brings `assets/whats_new/`. Texts through gen-l10n in British English (the template), German (informal du), an empty `en_US` and `en_GB` (`test/l10n/arb_files_test.dart` keeps them in step). The look follows the system's theme mode and language until Todo has settings. Countries in where an instance keeps its data are named by halkyon_app's CLDR names (`AssetCountryNames`), its language picked by halkyon_app's `resolveAppLocale`. Widget tests start Todo's whole tree with halkyon_app's fakes (`test/app/todo_app_test.dart`).
- **The icon** is Lucide's `list-todo` fading from cobalt through pacific into fern (ADR 0031), drawn with the other apps' by the repository's `tool/app_icons.py` (`python3 -I tool/app_icons.py todo` from the root): `assets/icon/halkyon_todo_icon.png`, Lucide's ISC licence beside it, and every platform's launcher icon through flutter_launcher_icons. `registerLucideLicense` puts the notice in the licence list.
- **`serverpod start`** runs in `apps/todo/halkyon_todo_server` (it launches the app from `lib/driver.dart`); `.mcp.json`'s `--server-dir` names it when a session works on Todo. Its tests need throwaway passwords in its git-ignored `config/passwords.yaml` (`database`, `serviceSecret`, `serverSideSessionKeyHashPepper`, `emailSignInPepper`) or from the environment, as CI gives them. Learn's and Todo's development servers use the same ports: run one at a time.

View file

@ -6,11 +6,20 @@
"instanceFirstStartSubtitle": "Wähle die Instanz, auf der deine Aufgaben liegen sollen. Jede betreibt jemand; weitere kannst du später hinzufügen.",
"homeEmptyTitle": "Hier ist noch nichts",
"homeEmptyHint": "Aufgaben kommen mit einer späteren Version von Halkyon Todo.",
"accountTitle": "Konto",
"accountDeleteMessage": "Dein Konto wird auf dem Server und auf all deinen Geräten gelöscht. Dies kann nicht rückgängig gemacht werden.",
"aboutTitle": "Über"
"aboutTitle": "Über Halkyon Todo",
"shellTabToday": "Heute",
"shellTabUpcoming": "Demnächst",
"shellTabInbox": "Eingang",
"shellTabBrowse": "Übersicht",
"todayLaterTitle": "Hier stehen die Aufgaben von heute",
"upcomingLaterTitle": "Hier stehen die nächsten Tage",
"inboxLaterTitle": "Hier stehen Aufgaben ohne Projekt",
"browseLaterTitle": "Hier stehen deine Projekte und Labels",
"tasksLaterHint": "Aufgaben kommen mit einer späteren Version von Halkyon Todo.",
"settingsTitle": "Einstellungen",
"settingsVersion": "Version {version} (Build {build})"
}

View file

@ -8,11 +8,21 @@
"instanceFirstStartSubtitle": "Choose the instance to keep your tasks on. Someone runs each one; you can add others later.",
"homeEmptyTitle": "Nothing here yet",
"homeEmptyHint": "Tasks come with a later version of Halkyon Todo.",
"accountTitle": "Account",
"accountDeleteMessage": "Your account is deleted on the server and on all your devices. This can't be undone.",
"aboutTitle": "About"
"aboutTitle": "About Halkyon Todo",
"shellTabToday": "Today",
"shellTabUpcoming": "Upcoming",
"shellTabInbox": "Inbox",
"shellTabBrowse": "Browse",
"todayLaterTitle": "Today's tasks come here",
"upcomingLaterTitle": "The days ahead come here",
"inboxLaterTitle": "Tasks without a project come here",
"browseLaterTitle": "Your projects and labels come here",
"tasksLaterHint": "Tasks come with a later version of Halkyon Todo.",
"settingsTitle": "Settings",
"settingsVersion": "Version {version} (build {build})",
"@settingsVersion": {"placeholders": {"version": {"type": "String"}, "build": {"type": "String"}}}
}

View file

@ -118,18 +118,6 @@ abstract class AppLocalizations {
/// **'Choose the instance to keep your tasks on. Someone runs each one; you can add others later.'**
String get instanceFirstStartSubtitle;
/// No description provided for @homeEmptyTitle.
///
/// In en, this message translates to:
/// **'Nothing here yet'**
String get homeEmptyTitle;
/// No description provided for @homeEmptyHint.
///
/// In en, this message translates to:
/// **'Tasks come with a later version of Halkyon Todo.'**
String get homeEmptyHint;
/// No description provided for @accountTitle.
///
/// In en, this message translates to:
@ -145,8 +133,74 @@ abstract class AppLocalizations {
/// No description provided for @aboutTitle.
///
/// In en, this message translates to:
/// **'About'**
/// **'About Halkyon Todo'**
String get aboutTitle;
/// No description provided for @shellTabToday.
///
/// In en, this message translates to:
/// **'Today'**
String get shellTabToday;
/// No description provided for @shellTabUpcoming.
///
/// In en, this message translates to:
/// **'Upcoming'**
String get shellTabUpcoming;
/// No description provided for @shellTabInbox.
///
/// In en, this message translates to:
/// **'Inbox'**
String get shellTabInbox;
/// No description provided for @shellTabBrowse.
///
/// In en, this message translates to:
/// **'Browse'**
String get shellTabBrowse;
/// No description provided for @todayLaterTitle.
///
/// In en, this message translates to:
/// **'Today\'s tasks come here'**
String get todayLaterTitle;
/// No description provided for @upcomingLaterTitle.
///
/// In en, this message translates to:
/// **'The days ahead come here'**
String get upcomingLaterTitle;
/// No description provided for @inboxLaterTitle.
///
/// In en, this message translates to:
/// **'Tasks without a project come here'**
String get inboxLaterTitle;
/// No description provided for @browseLaterTitle.
///
/// In en, this message translates to:
/// **'Your projects and labels come here'**
String get browseLaterTitle;
/// No description provided for @tasksLaterHint.
///
/// In en, this message translates to:
/// **'Tasks come with a later version of Halkyon Todo.'**
String get tasksLaterHint;
/// No description provided for @settingsTitle.
///
/// In en, this message translates to:
/// **'Settings'**
String get settingsTitle;
/// No description provided for @settingsVersion.
///
/// In en, this message translates to:
/// **'Version {version} (build {build})'**
String settingsVersion(String version, String build);
}
class _AppLocalizationsDelegate

View file

@ -19,13 +19,6 @@ class AppLocalizationsDe extends AppLocalizations {
String get instanceFirstStartSubtitle =>
'Wähle die Instanz, auf der deine Aufgaben liegen sollen. Jede betreibt jemand; weitere kannst du später hinzufügen.';
@override
String get homeEmptyTitle => 'Hier ist noch nichts';
@override
String get homeEmptyHint =>
'Aufgaben kommen mit einer späteren Version von Halkyon Todo.';
@override
String get accountTitle => 'Konto';
@ -34,5 +27,41 @@ class AppLocalizationsDe extends AppLocalizations {
'Dein Konto wird auf dem Server und auf all deinen Geräten gelöscht. Dies kann nicht rückgängig gemacht werden.';
@override
String get aboutTitle => 'Über';
String get aboutTitle => 'Über Halkyon Todo';
@override
String get shellTabToday => 'Heute';
@override
String get shellTabUpcoming => 'Demnächst';
@override
String get shellTabInbox => 'Eingang';
@override
String get shellTabBrowse => 'Übersicht';
@override
String get todayLaterTitle => 'Hier stehen die Aufgaben von heute';
@override
String get upcomingLaterTitle => 'Hier stehen die nächsten Tage';
@override
String get inboxLaterTitle => 'Hier stehen Aufgaben ohne Projekt';
@override
String get browseLaterTitle => 'Hier stehen deine Projekte und Labels';
@override
String get tasksLaterHint =>
'Aufgaben kommen mit einer späteren Version von Halkyon Todo.';
@override
String get settingsTitle => 'Einstellungen';
@override
String settingsVersion(String version, String build) {
return 'Version $version (Build $build)';
}
}

View file

@ -19,13 +19,6 @@ class AppLocalizationsEn extends AppLocalizations {
String get instanceFirstStartSubtitle =>
'Choose the instance to keep your tasks on. Someone runs each one; you can add others later.';
@override
String get homeEmptyTitle => 'Nothing here yet';
@override
String get homeEmptyHint =>
'Tasks come with a later version of Halkyon Todo.';
@override
String get accountTitle => 'Account';
@ -34,7 +27,43 @@ class AppLocalizationsEn extends AppLocalizations {
'Your account is deleted on the server and on all your devices. This can\'t be undone.';
@override
String get aboutTitle => 'About';
String get aboutTitle => 'About Halkyon Todo';
@override
String get shellTabToday => 'Today';
@override
String get shellTabUpcoming => 'Upcoming';
@override
String get shellTabInbox => 'Inbox';
@override
String get shellTabBrowse => 'Browse';
@override
String get todayLaterTitle => 'Today\'s tasks come here';
@override
String get upcomingLaterTitle => 'The days ahead come here';
@override
String get inboxLaterTitle => 'Tasks without a project come here';
@override
String get browseLaterTitle => 'Your projects and labels come here';
@override
String get tasksLaterHint =>
'Tasks come with a later version of Halkyon Todo.';
@override
String get settingsTitle => 'Settings';
@override
String settingsVersion(String version, String build) {
return 'Version $version (build $build)';
}
}
/// The translations for English, as used in the United Kingdom (`en_GB`).

View file

@ -1,59 +0,0 @@
import 'package:halkyon_app/halkyon_app.dart';
import 'package:halkyon_todo_flutter/l10n/app_localizations.dart';
import 'package:halkyon_todo_flutter/src/app/todo_frame.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:material_ui/material_ui.dart';
/// The account: halkyon_app's sections (whose it is, on which instance,
/// switching to the account on another, signing out and deleting it;
/// SimpleLogin; its password and authenticators) in Todo's words where only
/// Todo can say it; and About.
Future<void> showAccountSheet(BuildContext context) => showAdaptiveSheet(
context,
builder: (context) => const _AccountSheet(),
);
class const _AccountSheet() extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return SheetScaffold(
title: l10n.accountTitle,
// What failed (signing out, deleting the account, reading it) shows
// on the sheet, not behind its barrier.
hasSnackBars: true,
body: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
AccountTiles(deleteMessage: l10n.accountDeleteMessage),
// Accounts waiting for approval, for whoever operates the
// instance alone (Halkyon Learn's ADR 0021, D34).
const InstanceOperatorTiles(),
// SimpleLogin keeps the account where the instance offers it
// (ADR 0020, D21), and a password and an authenticator, signing
// in on other devices and in the instance's other apps (ADR 0017).
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [SimpleLoginSection(), AccountSecuritySection()],
),
),
ListTile(
leading: const Icon(LucideIcons.info),
title: Text(l10n.aboutTitle),
// halkyon_app's About: the app's name and icon, the source of
// what the instance runs, which both licences ask to offer, and
// the licence list.
onTap: () => showAppAbout(
context,
icon: const Image(image: todoIcon, width: 48, height: 48),
),
),
],
),
);
}
}

View file

@ -4,7 +4,7 @@ import 'package:halkyon_todo_flutter/l10n/app_localizations.dart';
import 'package:halkyon_todo_flutter/src/app/todo_dependencies.dart';
import 'package:halkyon_todo_flutter/src/app/todo_frame.dart';
import 'package:halkyon_todo_flutter/src/app/todo_languages.dart';
import 'package:halkyon_todo_flutter/src/home/home_screen.dart';
import 'package:halkyon_todo_flutter/src/app/todo_router.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:material_ui/material_ui.dart';
@ -48,31 +48,49 @@ class const TodoApp({
}
/// A signed-in person's app, built anew for every person ([SessionGate]):
/// their [PersonScope] around its navigator, so every screen and sheet is
/// inside it, and the home.
/// their [PersonScope] around its router's navigator, so every screen and
/// sheet is inside it, and the tabs ([buildTodoRouter]). Every screen washes
/// down from the top in strawberry (ADR 0033, D3), as Learn's in cobalt and
/// Train's in carrot.
class const TodoPersonApp({
/// Whose app it is.
required final SignedIn person,
super.key,
}) extends StatelessWidget {
}) extends StatefulWidget {
@override
Widget build(BuildContext context) => MaterialApp(
State<TodoPersonApp> createState() => _TodoPersonAppState();
}
class _TodoPersonAppState() extends State<TodoPersonApp> {
final _router = buildTodoRouter();
@override
void dispose() {
_router.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => MaterialApp.router(
routerConfig: _router,
onGenerateTitle: (context) => AppLocalizations.of(context).appTitle,
theme: appTheme(Brightness.light),
darkTheme: appTheme(Brightness.dark),
localizationsDelegates: todoLocalizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
localeListResolutionCallback: resolveAppLocale,
// How many accounts wait, for whoever operates the instance: the
// account sheet's tiles and the badge on its button.
// How many accounts wait, for whoever operates the instance: Settings'
// operator card and the badge on Browse and its cog.
builder: (context, child) => PersonScope(
person: person,
person: widget.person,
child: BlocProvider(
create: (context) =>
OperatingCubit(AppScope.of(context).instanceOperators),
child: child,
child: ScreenColor(
color: PaletteColor.strawberry.background,
child: child!,
),
),
),
home: const HomeScreen(),
);
}

View file

@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart';
import 'package:halkyon_app/halkyon_app.dart';
import 'package:halkyon_oidc_flutter/halkyon_oidc_flutter.dart';
import 'package:halkyon_todo_client/halkyon_todo_client.dart' show Client;
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart';
import 'package:serverpod_flutter/serverpod_flutter.dart';
@ -68,6 +69,10 @@ class TodoDependencies({
/// Opens a signed-in person's dependencies ([openPerson]).
required final OpenTodoPerson _openPerson,
/// This build's version and number, which Settings shows at its foot so
/// that a report can name the release; none in tests that don't say.
final ({String version, int? build}) appVersion = (version: '', build: null),
final Future<void> Function()? _onDispose,
}) implements AppDependencies<TodoPerson> {
/// The dependencies above [context] ([AppScope]).
@ -150,6 +155,7 @@ class TodoDependencies({
assistants: assistants,
assistantOperators: assistantOperators,
fileSaver: PlatformFileSaver(),
appVersion: (version: device.appVersion, build: device.appBuild),
sourceCodeUrl: () async =>
(await client.modules.base.instance.basics()).sourceCodeUrl,
openPerson: (person) async {
@ -184,13 +190,23 @@ class TodoDevice({
/// Where the device keeps what it holds of the account on each instance.
required final InstanceStorage storage,
/// This build's number (after the `+` in the pubspec's version), which the
/// server knows apps by (`AppCompatibility`); null where it has none.
required final int? appBuild,
/// This build's version (before the `+`).
final String appVersion = '',
}) {
/// Makes them, [builtIn] being the instance the build names.
static Future<TodoDevice> create({required Instance builtIn}) async {
final package = await PackageInfo.fromPlatform();
final support = (await getApplicationSupportDirectory()).path;
return TodoDevice(
support: support,
storage: InstanceStorage(support, builtIn: builtIn),
appBuild: int.tryParse(package.buildNumber),
appVersion: package.version,
);
}
}

View file

@ -0,0 +1,18 @@
/// The app's locations (ADR 0033, D2): the four tabs, and Settings over
/// them.
abstract final class TodoPaths() {
/// Today, where the app opens.
static const today = '/';
/// Upcoming: from today on.
static const upcoming = '/upcoming';
/// Inbox: what has no project yet.
static const inbox = '/inbox';
/// Browse: the projects and labels, and the cog to Settings.
static const browse = '/browse';
/// Settings, over the tabs.
static const settings = '/settings';
}

View file

@ -0,0 +1,63 @@
import 'package:go_router/go_router.dart';
import 'package:halkyon_todo_flutter/src/app/todo_paths.dart';
import 'package:halkyon_todo_flutter/src/settings/settings_screen.dart';
import 'package:halkyon_todo_flutter/src/tabs/browse_tab.dart';
import 'package:halkyon_todo_flutter/src/tabs/inbox_tab.dart';
import 'package:halkyon_todo_flutter/src/tabs/today_tab.dart';
import 'package:halkyon_todo_flutter/src/tabs/todo_shell.dart';
import 'package:halkyon_todo_flutter/src/tabs/upcoming_tab.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
/// The router of one person's app (ADR 0033, D1), as Learn's and Train's:
/// the four tabs in a shell, each with its own stack of screens, and
/// Settings over them. Every route is built inside the person's scope, so
/// screens read their repositories from `PersonScope`.
GoRouter buildTodoRouter() => GoRouter(
routes: [
StatefulShellRoute(
builder: (context, state, shell) => TodoShell(shell: shell),
navigatorContainerBuilder: (context, shell, tabs) =>
TabsStack(index: shell.currentIndex, children: tabs),
// In the order of the shell's destinations.
branches: [
StatefulShellBranch(
routes: [
GoRoute(
path: TodoPaths.today,
builder: (context, state) => const TodayTab(),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: TodoPaths.upcoming,
builder: (context, state) => const UpcomingTab(),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: TodoPaths.inbox,
builder: (context, state) => const InboxTab(),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: TodoPaths.browse,
builder: (context, state) => const BrowseTab(),
),
],
),
],
),
// In the frame without the rail from medium windows on, as Learn's.
GoRoute(
path: TodoPaths.settings,
builder: (context, state) => const FramedScreen(child: SettingsScreen()),
),
],
);

View file

@ -1,44 +0,0 @@
import 'package:halkyon_app/halkyon_app.dart';
import 'package:halkyon_todo_flutter/l10n/app_localizations.dart';
import 'package:halkyon_todo_flutter/src/account/account_sheet.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:material_ui/material_ui.dart';
/// A signed-in person's home: nothing yet, as Todo keeps nothing until its
/// first model, and the way to the account; a reset of the person's keys
/// waiting above it (`KeysResetBanner`).
class const HomeScreen({super.key}) extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Scaffold(
appBar: AppBar(
title: Text(l10n.appTitle),
actions: [
IconButton(
tooltip: l10n.accountTitle,
icon: const WaitingAccountsBadge(
child: Icon(LucideIcons.circleUserRound),
),
onPressed: () => showAccountSheet(context),
),
],
),
body: Column(
children: [
const KeysResetBanner(),
Expanded(
child: Center(
child: EmptyListMessage(
icon: LucideIcons.listChecks,
title: l10n.homeEmptyTitle,
hint: l10n.homeEmptyHint,
),
),
),
],
),
);
}
}

View file

@ -0,0 +1,144 @@
import 'package:go_router/go_router.dart';
import 'package:halkyon_app/halkyon_app.dart';
import 'package:halkyon_todo_flutter/l10n/app_localizations.dart';
import 'package:halkyon_todo_flutter/src/app/todo_dependencies.dart';
import 'package:halkyon_todo_flutter/src/app/todo_frame.dart';
import 'package:halkyon_todo_flutter/src/app/todo_paths.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:material_ui/material_ui.dart';
/// The cog to [SettingsScreen], atop Browse (ADR 0033, D2), with the count of
/// accounts waiting for whoever operates the instance.
class const SettingsButton({super.key}) extends StatelessWidget {
@override
Widget build(BuildContext context) => IconButton(
tooltip: AppLocalizations.of(context).settingsTitle,
icon: const WaitingAccountsBadge(child: Icon(LucideIcons.settings)),
onPressed: () => context.push(TodoPaths.settings),
);
}
/// Settings, over the tabs (ADR 0033, D4), grouped in cards as Learn's and
/// Train's: the operator's card for whoever operates the instance; Todo's
/// own groups as their settings come (none is shown empty); the account,
/// near the foot (halkyon_app's sections: whose it is on which instance,
/// switching, signing out and deleting it, SimpleLogin, its password and
/// authenticators) in Todo's words where only Todo can say it; What's new and
/// About Halkyon Todo last; and at the very foot which release this is.
class const SettingsScreen({super.key}) extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final theme = Theme.of(context);
return TopGradient(
background: theme.colorScheme.surfaceContainerLow,
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.transparent,
systemOverlayStyle: TopGradient.overlayStyle(context),
title: Text(l10n.settingsTitle),
),
body: Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: const BoxConstraints(
maxWidth: readableContentMaxWidth,
),
child: SingleChildScrollView(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Accounts waiting for approval, for whoever operates
// the instance alone (Halkyon Learn's ADR 0021, D34),
// first, as the cog's count leads here.
const InstanceOperatorCard(below: 12),
SectionCard(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 4),
child: Text(
l10n.accountTitle,
style: theme.textTheme.titleMedium,
),
),
AccountTiles(deleteMessage: l10n.accountDeleteMessage),
// SimpleLogin keeps the account where the instance
// offers it (ADR 0020, D21), and a password and an
// authenticator, signing in on other devices and in
// the instance's other apps (ADR 0017).
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SimpleLoginSection(),
AccountSecuritySection(),
],
),
),
],
),
),
const SizedBox(height: 12),
SectionCard(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
children: [
// What changed in this version, as shown once after
// the update (halkyon_app's, Train's ADR 0030, D61).
const WhatsNewTile(),
ListTile(
leading: const Icon(LucideIcons.info),
title: Text(l10n.aboutTitle),
// halkyon_app's About: the app's name and icon,
// the source of what the instance runs, which
// both licences ask to offer, and the licence
// list.
onTap: () => showAppAbout(
context,
icon: const Image(
image: todoIcon,
width: 48,
height: 48,
),
),
),
],
),
),
const SizedBox(height: 24),
const _Version(),
],
),
),
),
),
),
);
}
}
/// Which release of the app this is, so that a report can name it.
class const _Version() extends StatelessWidget {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final (:version, :build) = TodoDependencies.of(context).appVersion;
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Text(
AppLocalizations.of(context).settingsVersion(version, '${build ?? ''}'),
textAlign: TextAlign.center,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
);
}
}

View file

@ -0,0 +1,23 @@
import 'package:halkyon_todo_flutter/l10n/app_localizations.dart';
import 'package:halkyon_todo_flutter/src/settings/settings_screen.dart';
import 'package:halkyon_todo_flutter/src/tabs/tab_screen.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:material_ui/material_ui.dart';
/// Browse (ADR 0033, D2): the projects and labels, to come, and the cog to
/// Settings.
class const BrowseTab({super.key}) extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return TabScreen(
title: l10n.shellTabBrowse,
actions: const [SettingsButton()],
body: TasksLater(
icon: LucideIcons.layoutList,
title: l10n.browseLaterTitle,
hint: l10n.tasksLaterHint,
),
);
}
}

View file

@ -0,0 +1,20 @@
import 'package:halkyon_todo_flutter/l10n/app_localizations.dart';
import 'package:halkyon_todo_flutter/src/tabs/tab_screen.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:material_ui/material_ui.dart';
/// Inbox (ADR 0033, D2): the tasks put in no project of their own, to come.
class const InboxTab({super.key}) extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return TabScreen(
title: l10n.shellTabInbox,
body: TasksLater(
icon: LucideIcons.inbox,
title: l10n.inboxLaterTitle,
hint: l10n.tasksLaterHint,
),
);
}
}

View file

@ -0,0 +1,64 @@
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:material_ui/material_ui.dart';
/// A tab's screen (ADR 0033, D3): its [title] in a transparent app bar over
/// Todo's wash from the top ([TopGradient], in the strawberry the app's
/// [ScreenColor] gives), as Train's tabs wash in carrot and Learn's in
/// cobalt.
class const TabScreen({
/// The tab's name, in the app bar.
required final String title,
/// The tab's content.
required final Widget body,
/// The app bar's actions.
final List<Widget> actions = const [],
/// The screen's button, above the navigation bar.
final Widget? floatingActionButton,
super.key,
}) extends StatelessWidget {
@override
Widget build(BuildContext context) => TopGradient(
background: Theme.of(context).colorScheme.surface,
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.transparent,
systemOverlayStyle: TopGradient.overlayStyle(context),
title: Text(title),
actions: [...actions, const SizedBox(width: 4)],
),
body: body,
// Clear of the navigation bar floating over the foot on a phone.
floatingActionButton: switch (floatingActionButton) {
final button? => Padding(
padding: EdgeInsets.only(
bottom: FloatingNavigationBar.coverOf(context),
),
child: button,
),
null => null,
},
),
);
}
/// What a tab says until tasks come (ADR 0033, step 1).
class const TasksLater({
/// The tab's own icon.
required final IconData icon,
/// What the tab is to hold.
required final String title,
/// That tasks come later.
required final String hint,
super.key,
}) extends StatelessWidget {
@override
Widget build(BuildContext context) => Center(
child: EmptyListMessage(icon: icon, title: title, hint: hint),
);
}

View file

@ -0,0 +1,30 @@
import 'package:halkyon_app/halkyon_app.dart';
import 'package:halkyon_todo_flutter/l10n/app_localizations.dart';
import 'package:halkyon_todo_flutter/src/tabs/tab_screen.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:material_ui/material_ui.dart';
/// Today (ADR 0033, D2), where the app opens: what is due today and what is
/// overdue, to come; a reset of the person's keys waiting above it
/// (`KeysResetBanner`).
class const TodayTab({super.key}) extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return TabScreen(
title: l10n.shellTabToday,
body: Column(
children: [
const KeysResetBanner(),
Expanded(
child: TasksLater(
icon: LucideIcons.calendarCheck,
title: l10n.todayLaterTitle,
hint: l10n.tasksLaterHint,
),
),
],
),
);
}
}

View file

@ -0,0 +1,63 @@
import 'package:go_router/go_router.dart';
import 'package:halkyon_app/halkyon_app.dart';
import 'package:halkyon_todo_flutter/l10n/app_localizations.dart';
import 'package:halkyon_todo_flutter/src/app/todo_frame.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:material_ui/material_ui.dart';
/// The frame around Todo's tabs (ADR 0033, D1, D2): halkyon_ui's
/// [NavigationFrame], as every Halkyon app's, with Today, Upcoming, Inbox
/// and Browse, a bar floating above the foot below medium windows and a rail
/// with Todo's icon on top from medium on. Every tab keeps its own stack of
/// screens. After an update, what's new shows once (halkyon_app's
/// [WhatsNewOnUpdate]), where the app is given it.
class const TodoShell({
/// The tabs' navigators, from `StatefulShellRoute`.
required final StatefulNavigationShell shell,
super.key,
}) extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return WhatsNewOnUpdate(
child: NavigationFrame(
appName: l10n.appTitle,
icon: todoIcon,
selectedIndex: shell.currentIndex,
// A tab chosen again goes back to its first screen.
onSelected: (index) => shell.goBranch(
index,
initialLocation: index == shell.currentIndex,
),
destinations: [
(
label: l10n.shellTabToday,
icon: const Icon(LucideIcons.calendarCheck),
selectedIcon: null,
),
(
label: l10n.shellTabUpcoming,
icon: const Icon(LucideIcons.calendarDays),
selectedIcon: null,
),
(
label: l10n.shellTabInbox,
icon: const Icon(LucideIcons.inbox),
selectedIcon: null,
),
(
label: l10n.shellTabBrowse,
// The cog to Settings is atop Browse: the count of accounts
// waiting for whoever operates the instance shows on its tab.
icon: const WaitingAccountsBadge(
child: Icon(LucideIcons.layoutList),
),
selectedIcon: null,
),
],
child: shell,
),
);
}
}

View file

@ -0,0 +1,20 @@
import 'package:halkyon_todo_flutter/l10n/app_localizations.dart';
import 'package:halkyon_todo_flutter/src/tabs/tab_screen.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:material_ui/material_ui.dart';
/// Upcoming (ADR 0033, D2): the days from today on, to come.
class const UpcomingTab({super.key}) extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return TabScreen(
title: l10n.shellTabUpcoming,
body: TasksLater(
icon: LucideIcons.calendarDays,
title: l10n.upcomingLaterTitle,
hint: l10n.tasksLaterHint,
),
);
}
}

View file

@ -12,6 +12,7 @@ import file_selector_macos
import flutter_secure_storage_darwin
import flutter_web_auth_2
import google_sign_in_ios
import package_info_plus
import sign_in_with_apple
import url_launcher_macos
import window_to_front
@ -24,6 +25,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin"))
FlutterWebAuth2Plugin.register(with: registry.registrar(forPlugin: "FlutterWebAuth2Plugin"))
GoogleSignInPlugin.register(with: registry.registrar(forPlugin: "GoogleSignInPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
SignInWithApplePlugin.register(with: registry.registrar(forPlugin: "SignInWithApplePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
WindowToFrontPlugin.register(with: registry.registrar(forPlugin: "WindowToFrontPlugin"))

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