diff --git a/lib/src/models/data/home.dart b/lib/src/models/data/home.dart index 925d6c153..616edb3bd 100644 --- a/lib/src/models/data/home.dart +++ b/lib/src/models/data/home.dart @@ -32,7 +32,7 @@ class HomeModel extends Model { /// Sets a new message that will disappear in 3 seconds. void setMessage({required Severity severity, required String text, bool permanent = false}) { - if (_hasError) return; // Don't replace error messages + if (_hasError && severity != Severity.critical) return; // Don't replace critical messages _messageTimer?.cancel(); // the new message might be cleared if the old one were about to message = TaskbarMessage(severity: severity, text: text); notifyListeners(); diff --git a/lib/src/models/data/views.dart b/lib/src/models/data/views.dart index 0d6ebdda7..f2cf8e9ed 100644 --- a/lib/src/models/data/views.dart +++ b/lib/src/models/data/views.dart @@ -83,12 +83,8 @@ class DashboardView { static final List uiViews = [ DashboardView(name: Routes.science, builder: (context) => SciencePage()), DashboardView(name: Routes.autonomy, builder: (context) => MapPage()), - logView, ]; - /// The [LogsPage] view. - static final logView = DashboardView(name: Routes.logs, builder: (context) => LogsPage()); - /// A blank view. static final blank = DashboardView( name: Routes.blank, @@ -142,10 +138,4 @@ class ViewsModel extends Model { } notifyListeners(); } - - /// Opens the log page in a full-screen view. - void openLogs() { - views = [DashboardView.logView]; - notifyListeners(); - } } diff --git a/lib/src/models/view/builders/autonomy_command.dart b/lib/src/models/view/builders/autonomy_command.dart index 0b690f863..ff179502b 100644 --- a/lib/src/models/view/builders/autonomy_command.dart +++ b/lib/src/models/view/builders/autonomy_command.dart @@ -56,7 +56,7 @@ class AutonomyCommandBuilder extends ValueBuilder { if (_handshake != null) { models.home.setMessage(severity: Severity.info, text: "Command received"); } else { - models.home.setMessage(severity: Severity.error, text: "The rover did not receive that command"); + models.home.setMessage(severity: Severity.error, text: "Command not received"); } isLoading = false; notifyListeners(); @@ -72,12 +72,12 @@ class AutonomyCommandBuilder extends ValueBuilder { models.sockets.autonomy.sendMessage(message); models.sockets.autonomy.sendMessage(message); models.sockets.autonomy.sendMessage(message); - models.home.setMessage(severity: Severity.info, text: "Aborting autonomy..."); + models.home.setMessage(severity: Severity.info, text: "Aborting..."); await Future.delayed(const Duration(seconds: 1)); if (_handshake != null) { models.home.setMessage(severity: Severity.info, text: "Command received"); } else { - models.home.setMessage(severity: Severity.critical, text: "The rover did not receive that command"); + models.home.setMessage(severity: Severity.critical, text: "Command not received"); } isLoading = false; notifyListeners(); diff --git a/lib/src/models/view/logs.dart b/lib/src/models/view/logs.dart index ffcc4934b..4fdce5b61 100644 --- a/lib/src/models/view/logs.dart +++ b/lib/src/models/view/logs.dart @@ -83,16 +83,21 @@ class LogsViewModel with ChangeNotifier { /// Disables [LogsOptionsViewModel.autoscroll] when the user scrolls manually. void onScroll() { - final enableAutoscroll = scrollController.position.pixels == scrollController.position.maxScrollExtent; + final enableAutoscroll = scrollController.position.pixels == 0; options.setAutoscroll(input: enableAutoscroll); } /// Scrolls to the bottom when a new log appears (if [LogsOptionsViewModel.autoscroll] is true). void onNewLog() { notifyListeners(); - if (options.autoscroll && scrollController.hasClients) { - scrollController.jumpTo(scrollController.position.maxScrollExtent); - } + if (!scrollController.hasClients) return; + scrollController.jumpTo(options.autoscroll ? 0 : scrollController.offset + 64); + } + + /// Jumps to the bottom of the logs. + void jumpToBottom() { + if (!scrollController.hasClients) return; + scrollController.animateTo(0, duration: const Duration(milliseconds: 500), curve: Curves.easeOutBack); } /// Updates the UI. @@ -106,6 +111,6 @@ class LogsViewModel with ChangeNotifier { if (log.level.value > options.levelFilter.value) continue; result.add(log); } - return result; + return result.reversed.toList(); } } diff --git a/lib/src/models/view/map.dart b/lib/src/models/view/map.dart index e769bdcd4..c20e09142 100644 --- a/lib/src/models/view/map.dart +++ b/lib/src/models/view/map.dart @@ -102,7 +102,7 @@ class AutonomyModel with ChangeNotifier { markCell(result, marker, AutonomyCell.marker); } // Marks the rover and destination -- these should be last - markCell(result, data.destination, AutonomyCell.destination); + if (data.hasDestination()) markCell(result, data.destination, AutonomyCell.destination); markCell(result, roverPosition, AutonomyCell.rover); return result; } @@ -152,7 +152,7 @@ class AutonomyModel with ChangeNotifier { /// A handler to call when new data arrives. Updates [data] and the UI. void onNewData(AutonomyData value) { - data.mergeFromMessage(value); + data = value; services.files.logData(value); notifyListeners(); } diff --git a/lib/src/pages/home.dart b/lib/src/pages/home.dart index 8592bc51f..37cc0841c 100644 --- a/lib/src/pages/home.dart +++ b/lib/src/pages/home.dart @@ -95,11 +95,9 @@ class HomePageState extends State{ icon: const Icon(Icons.menu), onPressed: () => setState(() => showSidebar = !showSidebar), ),), - ], - ), - bottomNavigationBar: Footer(), + bottomNavigationBar: const Footer(), body: Row( children: [ const Expanded(child: ViewsWidget()), diff --git a/lib/src/pages/logs.dart b/lib/src/pages/logs.dart index 04e5205d5..05730bf01 100644 --- a/lib/src/pages/logs.dart +++ b/lib/src/pages/logs.dart @@ -2,7 +2,6 @@ import "package:flutter/material.dart"; import "package:rover_dashboard/data.dart"; import "package:rover_dashboard/models.dart"; -import "package:rover_dashboard/pages.dart"; import "package:rover_dashboard/widgets.dart"; /// A widget to show the options for the logs page. @@ -65,6 +64,7 @@ class LogsOptions extends ReactiveWidget { value: model.autoscroll, onChanged: (input) { model.setAutoscroll(input: input); + if (input ?? false) viewModel.jumpToBottom(); viewModel.update(); }, ),), @@ -86,57 +86,56 @@ class LogsState extends State { final model = LogsViewModel(); @override - Widget build(BuildContext context) => Stack( - children: [ - Column(children: [ - const SizedBox(height: 50), - LogsOptions(model), - const Divider(), - Expanded(child: LogsBody(model)), - ],), - Container( - color: context.colorScheme.surface, - height: 50, - child: Row(children: [ - const SizedBox(width: 8), - Text("Logs", style: context.textTheme.headlineMedium), - const Spacer(), - IconButton( - icon: const Icon(Icons.delete_forever), - onPressed: models.logs.clear, - tooltip: "Clear logs", - ), - IconButton( - icon: const Icon(Icons.help), - tooltip: "Help", - onPressed: () => showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text("Logs help"), - content: Column(mainAxisSize: MainAxisSize.min, children: [ - const Text("This page contains all logs received by the dashboard.\nSelecting a level means that only messages of that level or higher will be shown.", textAlign: TextAlign.center,), - const SizedBox(height: 4), - ListTile(leading: criticalWidget, title: const Text("Critical"), subtitle: const Text("The rover is in a broken state and may shutdown")), - const ListTile(leading: errorWidget, title: Text("Error"), subtitle: Text("Something you tried didn't work, but the rover can still function")), - const ListTile(leading: warningWidget, title: Text("Warning"), subtitle: Text("Something may have gone wrong, you should check it out")), - ListTile(leading: infoWidget, title: const Text("Info"), subtitle: const Text("The rover is functioning normally")), - const ListTile(leading: debugWidget, title: Text("Debug"), subtitle: Text("Extra information that shows what the rover's thinking")), - const ListTile(leading: traceWidget, title: Text("Trace"), subtitle: Text("Values from the code to debug specific issues")), - const SizedBox(height: 12), - ],), - actions: [ - ElevatedButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text("Close"), - ), - ], - ), + Widget build(BuildContext context) => Scaffold( + body: Column(children: [ + const SizedBox(height: 12), + LogsOptions(model), + const Divider(), + Expanded(child: LogsBody(model)), + ],), + appBar: AppBar( + title: const Text("Logs"), + actions: [ + IconButton( + icon: const Icon(Icons.help), + tooltip: "Help", + onPressed: () => showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text("Logs help"), + content: Column(mainAxisSize: MainAxisSize.min, children: [ + const Text("This page contains all logs received by the dashboard.\nSelecting a level means that only messages of that level or higher will be shown.", textAlign: TextAlign.center,), + const SizedBox(height: 4), + ListTile(leading: criticalWidget, title: const Text("Critical"), subtitle: const Text("The rover is in a broken state and may shutdown")), + const ListTile(leading: errorWidget, title: Text("Error"), subtitle: Text("Something you tried didn't work, but the rover can still function")), + const ListTile(leading: warningWidget, title: Text("Warning"), subtitle: Text("Something may have gone wrong, you should check it out")), + ListTile(leading: infoWidget, title: const Text("Info"), subtitle: const Text("The rover is functioning normally")), + const ListTile(leading: debugWidget, title: Text("Debug"), subtitle: Text("Extra information that shows what the rover's thinking")), + const ListTile(leading: traceWidget, title: Text("Trace"), subtitle: Text("Values from the code to debug specific issues")), + const SizedBox(height: 12), + ],), + actions: [ + ElevatedButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text("Close"), + ), + ], ), ), - const ViewsSelector(currentView: Routes.logs), - ],), - ), - ], + ), + IconButton( + icon: const Icon(Icons.vertical_align_bottom), + onPressed: model.jumpToBottom, + tooltip: "Jump to bottom", + ), + IconButton( + icon: const Icon(Icons.delete_forever), + onPressed: models.logs.clear, + tooltip: "Clear logs", + ), + ], + ), + bottomNavigationBar: const Footer(showLogs: false), ); } @@ -158,6 +157,7 @@ class LogsBody extends ReactiveWidget { : ListView.builder( itemCount: model.logs.length, controller: model.scrollController, + reverse: true, itemBuilder: (context, index) => LogWidget(model.logs[index]), ); } diff --git a/lib/src/widgets/navigation/footer.dart b/lib/src/widgets/navigation/footer.dart index 7eca8e353..843f4ef73 100644 --- a/lib/src/widgets/navigation/footer.dart +++ b/lib/src/widgets/navigation/footer.dart @@ -3,18 +3,24 @@ import "package:provider/provider.dart"; import "package:rover_dashboard/data.dart"; import "package:rover_dashboard/models.dart"; +import "package:rover_dashboard/pages.dart"; import "package:rover_dashboard/services.dart"; import "package:rover_dashboard/widgets.dart"; /// The footer, responsible for showing vitals and logs. class Footer extends StatelessWidget { + /// Whether to show logs. Disable this when on the logs page. + final bool showLogs; + /// Creates the footer. + const Footer({this.showLogs = true}); + @override Widget build(BuildContext context) => ColoredBox( color: Theme.of(context).colorScheme.secondary, child: Wrap( alignment: WrapAlignment.spaceBetween, children: [ - const MessageDisplay(), + MessageDisplay(showLogs: showLogs), Wrap( // Groups these elements together even when wrapping // mainAxisSize: MainAxisSize.min, children: [ @@ -227,12 +233,15 @@ class SerialButton extends StatelessWidget { /// Displays the latest [TaskbarMessage] from [HomeModel.message]. class MessageDisplay extends ReactiveWidget { + /// Whether to show an option to open the logs page. + final bool showLogs; + + /// Provides a const constructor for this widget. + const MessageDisplay({required this.showLogs}) : super(shouldDispose: false); + @override HomeModel createModel() => models.home; - /// Provides a const constructor for this widget. - const MessageDisplay() : super(shouldDispose: false); - /// Gets the appropriate icon for the given severity. IconData getIcon(Severity? severity) { switch (severity) { @@ -259,21 +268,20 @@ class MessageDisplay extends ReactiveWidget { Widget build(BuildContext context, HomeModel model) => SizedBox( height: 48, child: InkWell( - onTap: models.views.openLogs, + onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => LogsPage())), child: Card( shadowColor: Colors.transparent, color: getColor(model.message?.severity), shape: ContinuousRectangleBorder( borderRadius: BorderRadius.circular(12), ), - child: Row( + child: (model.message == null && !showLogs) ? const SizedBox() : Row( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(width: 4), Icon(getIcon(model.message?.severity)), const SizedBox(width: 4), - if (model.message == null) - const Text("Open logs") + if (model.message == null) const Text("Open logs") else Tooltip( message: "Click to open logs", child: models.settings.easterEggs.enableClippy