feat(train): Home, Maps, Record, Groups and Profile in Learn's frame, washed in carrot (ADR 0030, D60)
Train's tabs are Strava's five where one screen with four tabs at its top had held workouts, days, plans and routes: Home the workouts, Maps the routes, Record recording and the interval plans, Groups a screen saying they come later, Profile the days and the way to the account. A device that doesn't record has no Record tab, its plans beside the days in Profile. The frame is halkyon_ui's NavigationFrame, driven by go_router as Learn's: a workout's page opens in Home and a day's in Profile with the tabs beside them; the route and plan editors and the recording cover them, their content handed over as it is. Every tab washes down from the top in carrot, the warm end of Train's icon, as Learn's in cobalt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
b45db6ea7c
commit
d4d749eb9f
23 changed files with 800 additions and 202 deletions
File diff suppressed because one or more lines are too long
|
|
@ -556,5 +556,12 @@
|
|||
"recordWheelTyped": "{mm} mm, wie du ihn eingegeben hast",
|
||||
"recordWheelDefault": "{mm} mm, bis GPS ihn auf 2 km Fahrt gemessen hat",
|
||||
"recordWheelHint": "In Millimetern, wie auf dem Reifen (700×25C: 2105); leer, damit GPS ihn misst",
|
||||
"recordWheelSave": "Speichern"
|
||||
"recordWheelSave": "Speichern",
|
||||
"shellTabHome": "Startseite",
|
||||
"shellTabMaps": "Karten",
|
||||
"shellTabRecord": "Aufzeichnen",
|
||||
"shellTabGroups": "Gruppen",
|
||||
"shellTabProfile": "Profil",
|
||||
"groupsLaterTitle": "Gruppen kommen später",
|
||||
"groupsLaterHint": "Mit anderen trainieren, in euren eigenen Gruppen, kommt in einer späteren Version von Train."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -703,5 +703,12 @@
|
|||
"recordWheelDefault": "{mm} mm until GPS has measured it on a ride of 2 km",
|
||||
"@recordWheelDefault": {"placeholders": {"mm": {"type": "int"}}},
|
||||
"recordWheelHint": "In millimetres, as the tyre says (700×25C: 2105); empty to have GPS measure it",
|
||||
"recordWheelSave": "Save"
|
||||
"recordWheelSave": "Save",
|
||||
"shellTabHome": "Home",
|
||||
"shellTabMaps": "Maps",
|
||||
"shellTabRecord": "Record",
|
||||
"shellTabGroups": "Groups",
|
||||
"shellTabProfile": "Profile",
|
||||
"groupsLaterTitle": "Groups come later",
|
||||
"groupsLaterHint": "Training with others, in groups of your own, comes in a later version of Train."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3363,6 +3363,48 @@ abstract class AppLocalizations {
|
|||
/// In en, this message translates to:
|
||||
/// **'Save'**
|
||||
String get recordWheelSave;
|
||||
|
||||
/// No description provided for @shellTabHome.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Home'**
|
||||
String get shellTabHome;
|
||||
|
||||
/// No description provided for @shellTabMaps.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Maps'**
|
||||
String get shellTabMaps;
|
||||
|
||||
/// No description provided for @shellTabRecord.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Record'**
|
||||
String get shellTabRecord;
|
||||
|
||||
/// No description provided for @shellTabGroups.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Groups'**
|
||||
String get shellTabGroups;
|
||||
|
||||
/// No description provided for @shellTabProfile.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Profile'**
|
||||
String get shellTabProfile;
|
||||
|
||||
/// No description provided for @groupsLaterTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Groups come later'**
|
||||
String get groupsLaterTitle;
|
||||
|
||||
/// No description provided for @groupsLaterHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Training with others, in groups of your own, comes in a later version of Train.'**
|
||||
String get groupsLaterHint;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
|
|
|||
|
|
@ -2019,4 +2019,26 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get recordWheelSave => 'Speichern';
|
||||
|
||||
@override
|
||||
String get shellTabHome => 'Startseite';
|
||||
|
||||
@override
|
||||
String get shellTabMaps => 'Karten';
|
||||
|
||||
@override
|
||||
String get shellTabRecord => 'Aufzeichnen';
|
||||
|
||||
@override
|
||||
String get shellTabGroups => 'Gruppen';
|
||||
|
||||
@override
|
||||
String get shellTabProfile => 'Profil';
|
||||
|
||||
@override
|
||||
String get groupsLaterTitle => 'Gruppen kommen später';
|
||||
|
||||
@override
|
||||
String get groupsLaterHint =>
|
||||
'Mit anderen trainieren, in euren eigenen Gruppen, kommt in einer späteren Version von Train.';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2007,6 +2007,28 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get recordWheelSave => 'Save';
|
||||
|
||||
@override
|
||||
String get shellTabHome => 'Home';
|
||||
|
||||
@override
|
||||
String get shellTabMaps => 'Maps';
|
||||
|
||||
@override
|
||||
String get shellTabRecord => 'Record';
|
||||
|
||||
@override
|
||||
String get shellTabGroups => 'Groups';
|
||||
|
||||
@override
|
||||
String get shellTabProfile => 'Profile';
|
||||
|
||||
@override
|
||||
String get groupsLaterTitle => 'Groups come later';
|
||||
|
||||
@override
|
||||
String get groupsLaterHint =>
|
||||
'Training with others, in groups of your own, comes in a later version of Train.';
|
||||
}
|
||||
|
||||
/// The translations for English, as used in the United Kingdom (`en_GB`).
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import 'package:halkyon_train_flutter/l10n/app_localizations.dart';
|
|||
import 'package:halkyon_train_flutter/src/app/train_dependencies.dart';
|
||||
import 'package:halkyon_train_flutter/src/app/train_frame.dart';
|
||||
import 'package:halkyon_train_flutter/src/app/train_languages.dart';
|
||||
import 'package:halkyon_train_flutter/src/app/train_router.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/day/bloc/days_cubit.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/health/widget/health_screen.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/music/data/cadence_model.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/music/data/music_downloads.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/music/data/recorder_music.dart';
|
||||
|
|
@ -16,11 +16,9 @@ import 'package:halkyon_train_flutter/src/feature/recording/bloc/recording_bloc.
|
|||
import 'package:halkyon_train_flutter/src/feature/recording/widget/announce_speech.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/recording/widget/recording_texts.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/route/bloc/routes_cubit.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/route/widget/route_import.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/route/widget/route_speech.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/workout/bloc/workouts_cubit.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/workout/model/workout.dart';
|
||||
import 'package:halkyon_train_flutter/src/home/home_screen.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
|
|
@ -64,15 +62,31 @@ class const TrainApp({
|
|||
}
|
||||
|
||||
/// 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 ([buildTrainRouter]). Every screen washes
|
||||
/// down from the top in carrot, the warm end of Train's icon (ADR 0030,
|
||||
/// D60), as Learn's in cobalt.
|
||||
class const TrainPersonApp({
|
||||
/// Whose app it is.
|
||||
required final SignedIn person,
|
||||
super.key,
|
||||
}) extends StatelessWidget {
|
||||
}) extends StatefulWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) => MaterialApp(
|
||||
State<TrainPersonApp> createState() => _TrainPersonAppState();
|
||||
}
|
||||
|
||||
class _TrainPersonAppState() extends State<TrainPersonApp> {
|
||||
final _router = buildTrainRouter();
|
||||
|
||||
@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),
|
||||
|
|
@ -83,7 +97,7 @@ class const TrainPersonApp({
|
|||
// account sheet's tiles and the badge on its button. The workouts above
|
||||
// the navigator, so home and a workout's page share their deletions.
|
||||
builder: (context, child) => PersonScope(
|
||||
person: person,
|
||||
person: widget.person,
|
||||
child: MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
|
|
@ -115,15 +129,14 @@ class const TrainPersonApp({
|
|||
},
|
||||
),
|
||||
],
|
||||
child: _Recording(child: child!),
|
||||
child: _Recording(
|
||||
child: ScreenColor(
|
||||
color: PaletteColor.carrot.background,
|
||||
child: child!,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Health Connect links to Train's rationale from its own screens, and
|
||||
// what other apps wrote there is read as Train comes to the front (D31).
|
||||
// GPX files Android hands Train are brought in as routes (D45).
|
||||
home: const HealthConnectListener(
|
||||
child: RouteFilesListener(child: HomeScreen()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
import 'package:halkyon_train_flutter/src/feature/day/model/day.dart';
|
||||
|
||||
/// The app's locations (ADR 0030, D60): the five tabs, a workout's page in
|
||||
/// Home and a day's in Profile, and the editors and the recording over the
|
||||
/// tabs.
|
||||
abstract final class TrainPaths() {
|
||||
/// Home, where the app opens: the workouts.
|
||||
static const home = '/';
|
||||
|
||||
/// Maps: the routes.
|
||||
static const maps = '/maps';
|
||||
|
||||
/// Record, where the device records, with the interval plans.
|
||||
static const record = '/record';
|
||||
|
||||
/// Groups, to come.
|
||||
static const groups = '/groups';
|
||||
|
||||
/// Profile: the days.
|
||||
static const profile = '/profile';
|
||||
|
||||
/// A workout's page, in Home.
|
||||
static String workout(String id) => '/workouts/$id';
|
||||
|
||||
/// The page of the day [date], in Profile, by the band's name for it.
|
||||
static String day(DateTime date) => '/profile/days/${dayKey(date)}';
|
||||
|
||||
/// A route, to plan, over the tabs: the stored one [id], or a new one.
|
||||
static String route([String? id]) => '/routes/${id ?? 'new'}';
|
||||
|
||||
/// An interval plan, to write, over the tabs: the stored one [id], or a
|
||||
/// new one.
|
||||
static String plan([String? id]) => '/plans/${id ?? 'new'}';
|
||||
|
||||
/// The recording under way or stopped, over the tabs.
|
||||
static const recording = '/recording';
|
||||
}
|
||||
119
apps/train/halkyon_train_flutter/lib/src/app/train_router.dart
Normal file
119
apps/train/halkyon_train_flutter/lib/src/app/train_router.dart
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
import 'package:go_router/go_router.dart';
|
||||
import 'package:halkyon_train_flutter/src/app/train_paths.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/day/model/day.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/day/widget/day_screen.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/plan/model/plan.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/plan/widget/plan_screen.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/recording/widget/recording_screen.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/route/model/route.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/route/widget/route_screen.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/workout/widget/workout_screen.dart';
|
||||
import 'package:halkyon_train_flutter/src/tabs/groups_tab.dart';
|
||||
import 'package:halkyon_train_flutter/src/tabs/home_tab.dart';
|
||||
import 'package:halkyon_train_flutter/src/tabs/maps_tab.dart';
|
||||
import 'package:halkyon_train_flutter/src/tabs/profile_tab.dart';
|
||||
import 'package:halkyon_train_flutter/src/tabs/record_tab.dart';
|
||||
import 'package:halkyon_train_flutter/src/tabs/train_shell.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
|
||||
/// The router of one person's app (ADR 0030, D60), as Learn's: the five tabs
|
||||
/// in a shell, each with its own stack of screens, and the editors and the
|
||||
/// recording over them. Every route is built inside the person's scope, so
|
||||
/// screens read their repositories from `PersonScope`. A route or plan is
|
||||
/// handed to its editor as it is (`extra`): a new one exists nowhere else
|
||||
/// yet.
|
||||
GoRouter buildTrainRouter() => GoRouter(
|
||||
routes: [
|
||||
StatefulShellRoute(
|
||||
builder: (context, state, shell) => TrainShell(shell: shell),
|
||||
navigatorContainerBuilder: (context, shell, tabs) =>
|
||||
TabsStack(index: shell.currentIndex, children: tabs),
|
||||
// In the order of the shell's destinations; Record's branch stays
|
||||
// where the device doesn't record, its tab left out.
|
||||
branches: [
|
||||
StatefulShellBranch(
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: TrainPaths.home,
|
||||
builder: (context, state) => const HomeTab(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'workouts/:id',
|
||||
builder: (context, state) =>
|
||||
workoutPage(context, state.pathParameters['id']!),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
StatefulShellBranch(
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: TrainPaths.maps,
|
||||
builder: (context, state) => const MapsTab(),
|
||||
),
|
||||
],
|
||||
),
|
||||
StatefulShellBranch(
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: TrainPaths.record,
|
||||
builder: (context, state) => const RecordTab(),
|
||||
),
|
||||
],
|
||||
),
|
||||
StatefulShellBranch(
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: TrainPaths.groups,
|
||||
builder: (context, state) => const GroupsTab(),
|
||||
),
|
||||
],
|
||||
),
|
||||
StatefulShellBranch(
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: TrainPaths.profile,
|
||||
builder: (context, state) => const ProfileTab(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'days/:date',
|
||||
builder: (context, state) => dayPage(
|
||||
context,
|
||||
dateOfKey(state.pathParameters['date'])!,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/routes/:id',
|
||||
builder: (context, state) => routePage(
|
||||
context,
|
||||
state.extra! as RouteContent,
|
||||
id: _stored(state),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/plans/:id',
|
||||
builder: (context, state) => planPage(
|
||||
context,
|
||||
state.extra! as PlanContent,
|
||||
id: _stored(state),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: TrainPaths.recording,
|
||||
builder: (context, state) => const RecordingScreen(),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
/// The stored row an editor's location names; null for a new one.
|
||||
String? _stored(GoRouterState state) => switch (state.pathParameters['id']) {
|
||||
'new' => null,
|
||||
final id => id,
|
||||
};
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_train_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_train_flutter/src/app/train_dependencies.dart';
|
||||
import 'package:halkyon_train_flutter/src/app/train_paths.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/day/bloc/day_cubit.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/day/model/day.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/day/widget/day_texts.dart';
|
||||
|
|
@ -12,16 +14,16 @@ import 'package:halkyon_ui/halkyon_ui.dart';
|
|||
import 'package:intl/intl.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// Opens the page of the day [date].
|
||||
Future<void> openDayScreen(BuildContext context, DateTime date) {
|
||||
/// Opens the page of the day [date], in Profile (D60).
|
||||
Future<void> openDayScreen(BuildContext context, DateTime date) =>
|
||||
context.push(TrainPaths.day(date));
|
||||
|
||||
/// The page of the day [date], as the router builds it.
|
||||
Widget dayPage(BuildContext context, DateTime date) {
|
||||
final train = PersonScope.of<TrainPerson>(context);
|
||||
return Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => BlocProvider(
|
||||
create: (context) => DayCubit(train.days, date)..open(),
|
||||
child: DayScreen(date: date),
|
||||
),
|
||||
),
|
||||
return BlocProvider(
|
||||
create: (context) => DayCubit(train.days, date)..open(),
|
||||
child: DayScreen(date: date),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_train_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_train_flutter/src/app/train_dependencies.dart';
|
||||
import 'package:halkyon_train_flutter/src/app/train_paths.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/plan/bloc/plan_editor_cubit.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/plan/bloc/plans_cubit.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/plan/model/plan.dart';
|
||||
|
|
@ -14,27 +16,20 @@ import 'package:halkyon_ui/halkyon_ui.dart';
|
|||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// Opens [plan] to be changed, or made where [id] is null.
|
||||
/// Opens [plan] to be changed, or made where [id] is null, over the tabs
|
||||
/// (D60).
|
||||
Future<void> openPlanScreen(
|
||||
BuildContext context,
|
||||
PlanContent plan, {
|
||||
String? id,
|
||||
}) {
|
||||
}) => context.push(TrainPaths.plan(id), extra: plan);
|
||||
|
||||
/// The editor of [plan], stored as [id] or new, as the router builds it.
|
||||
Widget planPage(BuildContext context, PlanContent plan, {String? id}) {
|
||||
final train = PersonScope.of<TrainPerson>(context);
|
||||
final plans = context.read<PlansCubit>();
|
||||
return Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: plans),
|
||||
BlocProvider(
|
||||
create: (context) =>
|
||||
PlanEditorCubit(train.plans, plan: plan, id: id),
|
||||
),
|
||||
],
|
||||
child: const PlanScreen(),
|
||||
),
|
||||
),
|
||||
return BlocProvider(
|
||||
create: (context) => PlanEditorCubit(train.plans, plan: plan, id: id),
|
||||
child: const PlanScreen(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@ import 'dart:async';
|
|||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_train_client/halkyon_train_client.dart' show MapSource;
|
||||
import 'package:halkyon_train_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_train_flutter/src/app/train_dependencies.dart';
|
||||
import 'package:halkyon_train_flutter/src/app/train_paths.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/map/widget/map_tiles.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/plan/model/plan.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/plan/widget/plan_texts.dart';
|
||||
|
|
@ -23,18 +25,9 @@ import 'package:latlong2/latlong.dart';
|
|||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// Opens the recording screen over [context]'s, with its bloc.
|
||||
Future<void> openRecordingScreen(BuildContext context) {
|
||||
final bloc = context.read<RecordingBloc>();
|
||||
return Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => BlocProvider.value(
|
||||
value: bloc,
|
||||
child: const RecordingScreen(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
/// Opens the recording screen over the tabs (D60).
|
||||
Future<void> openRecordingScreen(BuildContext context) =>
|
||||
context.push(TrainPaths.recording);
|
||||
|
||||
/// A recording under way, or stopped (ADR 0030, D21): the moving time, the
|
||||
/// distance and the pace now, GPS's state, the plan's step and Lap where it
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_train_client/halkyon_train_client.dart' show MapSource;
|
||||
import 'package:halkyon_train_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_train_flutter/src/app/train_dependencies.dart';
|
||||
import 'package:halkyon_train_flutter/src/app/train_paths.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/map/data/offline_maps.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/map/widget/map_tiles.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/recording/data/recorder_platform.dart';
|
||||
|
|
@ -36,37 +38,35 @@ const routeActivities = [
|
|||
Activity.cycling,
|
||||
];
|
||||
|
||||
/// Opens [route] to be changed, or planned where [id] is null; a new one
|
||||
/// starts where the phone is, where the device can tell (D44).
|
||||
/// Opens [route] to be changed, or planned where [id] is null, over the tabs
|
||||
/// (D60).
|
||||
Future<void> openRouteScreen(
|
||||
BuildContext context,
|
||||
RouteContent route, {
|
||||
String? id,
|
||||
}) {
|
||||
}) => context.push(TrainPaths.route(id), extra: route);
|
||||
|
||||
/// The planner of [route], stored as [id] or new, as the router builds it; a
|
||||
/// new one starts where the phone is, where the device can tell (D44).
|
||||
Widget routePage(BuildContext context, RouteContent route, {String? id}) {
|
||||
final train = PersonScope.of<TrainPerson>(context);
|
||||
final routes = context.read<RoutesCubit>();
|
||||
final platform = train.recording?.platform;
|
||||
return Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: routes),
|
||||
BlocProvider(
|
||||
create: (context) => RouteEditorCubit(
|
||||
train.routes,
|
||||
train.routing,
|
||||
route: route,
|
||||
id: id,
|
||||
here: platform == null ? null : () => _here(platform),
|
||||
)..open(),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => PlaceSearchCubit(train.placeSearch)..open(),
|
||||
),
|
||||
],
|
||||
child: RouteScreen(maps: train.maps.source()),
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (context) => RouteEditorCubit(
|
||||
train.routes,
|
||||
train.routing,
|
||||
route: route,
|
||||
id: id,
|
||||
here: platform == null ? null : () => _here(platform),
|
||||
)..open(),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => PlaceSearchCubit(train.placeSearch)..open(),
|
||||
),
|
||||
],
|
||||
child: RouteScreen(maps: train.maps.source()),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_train_client/halkyon_train_client.dart' show MapSource;
|
||||
import 'package:halkyon_train_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_train_flutter/src/app/train_dependencies.dart';
|
||||
import 'package:halkyon_train_flutter/src/app/train_paths.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/map/widget/map_tiles.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/plan/widget/plan_texts.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/recording/model/recording.dart';
|
||||
|
|
@ -22,23 +24,16 @@ import 'package:lucide_icons_flutter/lucide_icons.dart';
|
|||
import 'package:material_ui/material_ui.dart' hide Split;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
/// Opens the page of the workout [id] over [context]'s.
|
||||
Future<void> openWorkoutScreen(BuildContext context, String id) {
|
||||
/// Opens the page of the workout [id], in Home (D60).
|
||||
Future<void> openWorkoutScreen(BuildContext context, String id) =>
|
||||
context.push(TrainPaths.workout(id));
|
||||
|
||||
/// The page of the workout [id], as the router builds it.
|
||||
Widget workoutPage(BuildContext context, String id) {
|
||||
final train = PersonScope.of<TrainPerson>(context);
|
||||
final workouts = context.read<WorkoutsCubit>();
|
||||
return Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: workouts),
|
||||
BlocProvider(
|
||||
create: (context) =>
|
||||
WorkoutCubit(train.workouts, train.maps, id)..open(),
|
||||
),
|
||||
],
|
||||
child: const WorkoutScreen(),
|
||||
),
|
||||
),
|
||||
return BlocProvider(
|
||||
create: (context) => WorkoutCubit(train.workouts, train.maps, id)..open(),
|
||||
child: const WorkoutScreen(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
import 'package:halkyon_train_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_train_flutter/src/tabs/tab_screen.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// Groups (ADR 0030, D60): training with others, to come; until then, what
|
||||
/// it will be.
|
||||
class const GroupsTab({super.key}) extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
return TabScreen(
|
||||
title: l10n.shellTabGroups,
|
||||
body: Center(
|
||||
child: EmptyListMessage(
|
||||
icon: LucideIcons.users,
|
||||
title: l10n.groupsLaterTitle,
|
||||
hint: l10n.groupsLaterHint,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
37
apps/train/halkyon_train_flutter/lib/src/tabs/home_tab.dart
Normal file
37
apps/train/halkyon_train_flutter/lib/src/tabs/home_tab.dart
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_train_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_train_flutter/src/app/train_dependencies.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/workout/widget/workouts_list.dart';
|
||||
import 'package:halkyon_train_flutter/src/tabs/tab_screen.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// Home (ADR 0030, D60): the person's workouts (D24), a reset of their keys
|
||||
/// waiting above them (`KeysResetBanner`).
|
||||
class const HomeTab({super.key}) extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final records = PersonScope.of<TrainPerson>(context).recording != null;
|
||||
return TabScreen(
|
||||
title: l10n.shellTabHome,
|
||||
body: Column(
|
||||
children: [
|
||||
const KeysResetBanner(),
|
||||
Expanded(
|
||||
child: WorkoutsList(
|
||||
empty: Center(
|
||||
child: EmptyListMessage(
|
||||
icon: LucideIcons.activity,
|
||||
title: l10n.homeEmptyTitle,
|
||||
hint: records ? l10n.homeRecordHint : l10n.homeEmptyHint,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
13
apps/train/halkyon_train_flutter/lib/src/tabs/maps_tab.dart
Normal file
13
apps/train/halkyon_train_flutter/lib/src/tabs/maps_tab.dart
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import 'package:halkyon_train_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/route/widget/routes_list.dart';
|
||||
import 'package:halkyon_train_flutter/src/tabs/tab_screen.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// Maps (ADR 0030, D60): the person's routes (D44), and planning another.
|
||||
class const MapsTab({super.key}) extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) => TabScreen(
|
||||
title: AppLocalizations.of(context).shellTabMaps,
|
||||
body: const RoutesList(),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_train_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_train_flutter/src/account/account_sheet.dart';
|
||||
import 'package:halkyon_train_flutter/src/app/train_dependencies.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/day/widget/days_list.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/plan/widget/plans_list.dart';
|
||||
import 'package:halkyon_train_flutter/src/tabs/tab_screen.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// Profile (ADR 0030, D60): the person's days from the Loop (D37), and the
|
||||
/// way to their account. Where the device doesn't record, and so has no
|
||||
/// Record tab, the interval plans (D42) are here beside the days.
|
||||
class const ProfileTab({super.key}) extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final records = PersonScope.of<TrainPerson>(context).recording != null;
|
||||
final account = IconButton(
|
||||
tooltip: l10n.accountTitle,
|
||||
icon: const WaitingAccountsBadge(
|
||||
child: Icon(LucideIcons.circleUserRound),
|
||||
),
|
||||
onPressed: () => showAccountSheet(context),
|
||||
);
|
||||
if (records) {
|
||||
return TabScreen(
|
||||
title: l10n.shellTabProfile,
|
||||
actions: [account],
|
||||
body: const DaysList(),
|
||||
);
|
||||
}
|
||||
return DefaultTabController(
|
||||
length: 2,
|
||||
child: TabScreen(
|
||||
title: l10n.shellTabProfile,
|
||||
actions: [account],
|
||||
bottom: TabBar(
|
||||
tabs: [
|
||||
Tab(text: l10n.homeDays),
|
||||
Tab(text: l10n.homePlans),
|
||||
],
|
||||
),
|
||||
body: const TabBarView(children: [DaysList(), PlansList()]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import 'package:halkyon_train_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_train_flutter/src/feature/plan/widget/plans_list.dart';
|
||||
import 'package:halkyon_train_flutter/src/tabs/tab_screen.dart';
|
||||
import 'package:halkyon_train_flutter/src/tabs/train_shell.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// Record (ADR 0030, D60), where the device records (D21): recording a
|
||||
/// workout, or back to the one under way, and the interval plans to record
|
||||
/// it by (D42).
|
||||
class const RecordTab({super.key}) extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) => TabScreen(
|
||||
title: AppLocalizations.of(context).shellTabRecord,
|
||||
body: const PlansList(),
|
||||
floatingActionButton: const RecordButton(),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// A tab's screen (ADR 0030, D60): its [title] in a transparent app bar over
|
||||
/// Train's wash from the top ([TopGradient], in the carrot the app's
|
||||
/// [ScreenColor] gives), as Learn's tabs wash 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 [],
|
||||
|
||||
/// Below the title: a tab row.
|
||||
final PreferredSizeWidget? bottom,
|
||||
|
||||
/// 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)],
|
||||
bottom: bottom,
|
||||
),
|
||||
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,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue