diff --git a/assets/logo-dark.png b/assets/logo-dark.png new file mode 100644 index 000000000..f5ad75094 Binary files /dev/null and b/assets/logo-dark.png differ diff --git a/assets/logo-light.png b/assets/logo-light.png new file mode 100644 index 000000000..9d7213162 Binary files /dev/null and b/assets/logo-light.png differ diff --git a/lib/mini.dart b/lib/mini.dart new file mode 100644 index 000000000..c07ad3651 --- /dev/null +++ b/lib/mini.dart @@ -0,0 +1,240 @@ +import "dart:async"; +import "dart:io"; +import "package:flutter/material.dart"; + +import "package:rover_dashboard/app.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/src/pages/mini_home.dart"; +import "package:rover_dashboard/src/pages/mini_logs.dart"; +import "package:rover_dashboard/src/pages/mini_metrics.dart"; +import "package:rover_dashboard/widgets.dart"; + +/// View model for the Mini dashboard home page +/// +/// Stores the function to define the extra widget displayed on the +/// footer, and initializes all necessary data, services, and other +/// view models +class MiniViewModel with ChangeNotifier { + Widget Function(BuildContext context)? _footerWidget; + + /// Constructor for [MiniViewModel], calls [init] to setup mini dashboard + MiniViewModel() { + init(); + } + + set footerWidget(Widget Function(BuildContext context)? footerWidget) { + _footerWidget = footerWidget; + notifyListeners(); + } + + /// The builder for the footer widget + Widget Function(BuildContext context)? get footerWidget => _footerWidget; + + /// Initializes necessary systems and models for the Mini Dashboard + /// + /// Sets the rover type to localhost and disables the sockets until + /// it is manually turned on by the user + Future init() async { + await services.init(); + await models.init(); + await models.sockets.setRover(RoverType.localhost); + await models.sockets.disable(); + + models.settings.addListener(notifyListeners); + + notifyListeners(); + } + + @override + void dispose() { + models.settings.removeListener(notifyListeners); + super.dispose(); + } +} + +/// The main app page for the Mini dashboard +/// +/// Displays a header with the dashboard version, a tab bar view +/// to select between the home page, metrics/controls, logs, and +/// a page to display a view +class MiniHomePage extends StatelessWidget { + /// The Mini Dashboard view model + final MiniViewModel model; + + /// A const constructor for the mini home page + const MiniHomePage({required this.model}); + + @override + Widget build(BuildContext context) => Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + title: Text("Dashboard v${models.home.version ?? ''}"), + actions: [ + IconButton( + onPressed: () => Navigator.of(context).pushNamed(Routes.settings), + icon: const Icon(Icons.settings), + ), + const SizedBox(width: 10), + const PowerButton(), + const SizedBox(width: 5), + ], + ), + body: DefaultTabController( + length: 4, + child: Column( + children: [ + const TabBar( + tabs: [ + Tab(text: "Home"), + Tab(text: "Metrics & Controls"), + Tab(text: "Logs"), + Tab(text: "View"), + ], + ), + Expanded( + child: TabBarView( + children: [ + const MiniHome(), + MiniMetrics(models.rover.metrics), + MiniLogs(miniViewModel: model), + ViewsWidget(), + ], + ), + ), + ], + ), + ), + bottomNavigationBar: MiniFooter(model), + ); +} + +/// Button to set the rover status to [RoverStatus.POWER_OFF], shutting off the rover +/// +/// Displays a confirmation dialog before shutting down +class PowerButton extends StatelessWidget { + /// Constructor for power button + const PowerButton({super.key}); + + @override + Widget build(BuildContext context) => Container( + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + padding: EdgeInsets.zero, + child: IconButton( + icon: const Icon( + Icons.power_settings_new, + color: Colors.red, + ), + onPressed: () async { + await showDialog( + context: context, + barrierDismissible: false, + builder: (_) => AlertDialog( + title: const Text("Are you sure?"), + content: const Text("This will turn off the rover and you must physically turn it back on again"), + actions: [ + TextButton(child: const Text("Cancel"), onPressed: () => Navigator.of(context).pop()), + ElevatedButton( + onPressed: () { + models.rover.settings.setStatus(RoverStatus.POWER_OFF); + Navigator.of(context).pop(); + }, + child: const Text("Continue"), + ), + ], + ), + ); + }, + ), + ); +} + +/// The footer for the mini dashboard +/// +/// Displays any necessary messages in the left of the footer, and +/// a custom defined widget on the right side. The custom widget space +/// is used by pages such as the Logs page to display extra information +/// in a small amount of space +class MiniFooter extends ReusableReactiveWidget { + /// Const constructor for the mini dashboard footer + const MiniFooter(super.model) : super(); + + @override + Widget build(BuildContext context, MiniViewModel model) => ColoredBox( + color: context.colorScheme.secondary, + child: Row( + children: [ + MessageDisplay(showLogs: false), + const Spacer(), + if (model.footerWidget != null) model.footerWidget!.call(context), + ], + ), + ); +} + +/// The main widget for the mini dashboard +/// +/// Initializes the Material App, necessary themes, and defines the +/// routes to the home and settings page +class MiniDashboard extends ReactiveWidget { + /// Const constructor for the mini dashboard + const MiniDashboard(); + + @override + MiniViewModel createModel() => MiniViewModel(); + + @override + Widget build(BuildContext context, MiniViewModel model) => MaterialApp( + title: "Binghamton University Rover Team", + debugShowCheckedModeBanner: false, + themeMode: models.isReady ? models.settings.dashboard.themeMode : ThemeMode.system, + theme: ThemeData( + colorScheme: const ColorScheme.light( + primary: binghamtonGreen, + secondary: binghamtonGreen, + ), + appBarTheme: const AppBarTheme( + backgroundColor: binghamtonGreen, + foregroundColor: Colors.white, + ), + ), + darkTheme: ThemeData.from( + colorScheme: const ColorScheme.dark( + primary: binghamtonGreen, + secondary: binghamtonGreen, + ), + ), + home: MiniHomePage(model: model), + routes: { + Routes.home: (_) => MiniHomePage(model: model), + Routes.settings: (_) => SettingsPage(), + }, + ); +} + +/// Network errors that can be fixed by a simple reset. +const networkErrors = {1234, 1231}; + +void main() async { + runZonedGuarded(() => runApp(const MiniDashboard()), (error, stack) async { + if (error is SocketException && networkErrors.contains(error.osError!.errorCode)) { + models.home.setMessage(severity: Severity.critical, text: "Network error, restart by clicking the network icon"); + } else { + models.home.setMessage(severity: Severity.critical, text: "Dashboard error. See the logs", logMessage: false); + models.logs.handleLog( + BurtLog( + level: BurtLogLevel.critical, + title: "Dashboard Error. Click for details", + body: "$error\n$stack", + device: Device.DASHBOARD, + ), + ); + Error.throwWithStackTrace(error, stack); + } + }); +} diff --git a/lib/src/data/metrics/vitals.dart b/lib/src/data/metrics/vitals.dart index f76fa30ce..4a29aa2ca 100644 --- a/lib/src/data/metrics/vitals.dart +++ b/lib/src/data/metrics/vitals.dart @@ -4,7 +4,9 @@ import "package:rover_dashboard/models.dart"; /// Metrics about the vitals of the rover. class VitalsMetrics extends Metrics { /// A const constructor. - VitalsMetrics() : super(DriveData()); + VitalsMetrics() : super(DriveData()) { + models.rover.metrics.drive.addListener(notifyListeners); + } @override Version parseVersion(Message message) => Version(major: 0, minor: 0); diff --git a/lib/src/models/data/sockets.dart b/lib/src/models/data/sockets.dart index 68ff50cea..4196767cf 100644 --- a/lib/src/models/data/sockets.dart +++ b/lib/src/models/data/sockets.dart @@ -22,6 +22,9 @@ class Sockets extends Model { /// The rover-like system currently in use. RoverType rover = RoverType.rover; + /// Whether or not the sockets are currently enabled + bool isEnabled = true; + /// The [InternetAddress] to use instead of the address on the rover. InternetAddress? get addressOverride => switch (rover) { RoverType.rover => null, @@ -48,20 +51,31 @@ class Sockets extends Model { _ => null, }; - @override - Future init() async { - for (final socket in sockets) { - socket.connectionStatus.addListener(() => socket.connectionStatus.value + @override + Future init() async { + isEnabled = true; + for (final socket in sockets) { + socket.connectionStatus.addListener(() => socket.connectionStatus.value ? onConnect(socket.device) : onDisconnect(socket.device), ); socket.messages.listen(models.messages.addMessage); await socket.init(); - } - final level = Logger.level; - Logger.level = LogLevel.warning; - await updateSockets(); - Logger.level = level; + } + final level = Logger.level; + Logger.level = LogLevel.warning; + await updateSockets(); + Logger.level = level; + notifyListeners(); + } + + /// Disconnects from all sockets without restarting them + Future disable() async { + isEnabled = false; + for (final socket in sockets) { + await socket.dispose(); + } + notifyListeners(); } @override diff --git a/lib/src/models/rover/metrics.dart b/lib/src/models/rover/metrics.dart index a1fe22ead..ed8b51b9c 100644 --- a/lib/src/models/rover/metrics.dart +++ b/lib/src/models/rover/metrics.dart @@ -20,7 +20,7 @@ class RoverMetrics extends Model { final gripper = GripperMetrics(); /// Vitals data from the rover. - final vitals = VitalsMetrics(); + late final vitals = VitalsMetrics(); /// A list of all the metrics to iterate over. /// diff --git a/lib/src/models/view/builders/settings_builder.dart b/lib/src/models/view/builders/settings_builder.dart index a879040b0..2387afdf0 100644 --- a/lib/src/models/view/builders/settings_builder.dart +++ b/lib/src/models/view/builders/settings_builder.dart @@ -383,7 +383,7 @@ class SettingsBuilder extends ValueBuilder { value.network.toJson(), )); await models.settings.update(value); - if (resetSockets) { + if (resetSockets && models.sockets.isEnabled) { await models.sockets.reset(); } models.video.reset(); diff --git a/lib/src/pages/logs.dart b/lib/src/pages/logs.dart index 1f6333d1c..d73d6ad80 100644 --- a/lib/src/pages/logs.dart +++ b/lib/src/pages/logs.dart @@ -121,6 +121,77 @@ class LogsOptions extends ReusableReactiveWidget { ); } +/// Returns a list of widgets that are used as the header or footer actions for the log page +List getLogsActions(BuildContext context, LogsViewModel model) => [ + IconButton( + icon: const Icon(Icons.help, color: Colors.white), + tooltip: "Help", + onPressed: () => showDialog( + context: context, + builder: (context) => AlertDialog( + scrollable: true, + 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"), + ), + ], + ), + ), + ), + IconButton( + icon: const Icon(Icons.vertical_align_bottom, color: Colors.white), + onPressed: model.jumpToBottom, + tooltip: "Jump to Bottom", + ), + IconButton( + icon: const Icon(Icons.delete_forever, color: Colors.white), + onPressed: models.logs.clear, + tooltip: "Clear Logs", + ), + ]; + /// The logs page, containing the [LogsOptions] and [LogsBody] widgets. /// /// This page lets the user view logs, set filters, and reboot the rover. @@ -146,45 +217,7 @@ class LogsState extends State { ],), 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"), - ), - ], - ), - ), - ), - 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", - ), - ], + actions: getLogsActions(context, model), ), bottomNavigationBar: const Footer(showLogs: false), ); diff --git a/lib/src/pages/mini_home.dart b/lib/src/pages/mini_home.dart new file mode 100644 index 000000000..eafbdb036 --- /dev/null +++ b/lib/src/pages/mini_home.dart @@ -0,0 +1,258 @@ +import "package:flutter/material.dart"; +import "package:rover_dashboard/data.dart"; +import "package:rover_dashboard/models.dart"; +import "package:rover_dashboard/widgets.dart"; + +/// The homepage for the Mini Dashboard +/// +/// Displays voltage/current information, subsystem statuses, gamepad selection, +/// and options to enable/disable the dashboard and rover +class MiniHome extends StatelessWidget { + /// Const constructor for the MiniHome + const MiniHome({super.key}); + + @override + Widget build(BuildContext context) => Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + flex: 3, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + flex: 5, + child: MiniHomeVoltage(models.rover.metrics.drive), + ), + const Divider(), + Expanded( + flex: 2, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GamepadButton(models.rover.controller1), + GamepadButton(models.rover.controller2), + GamepadButton(models.rover.controller3), + ], + ), + ), + const Divider(), + SizedBox( + height: 75, + child: MiniHomeToggleOptions(models.sockets), + ), + ], + ), + ), + const SizedBox(width: 5), + Expanded( + flex: 2, + child: MiniHomeSystemStatus(LogsViewModel()), + ), + ], + ); +} + +/// The voltage display for the mini home page, listens to drive metrics to update data +/// +/// Displays a battery charge icon, voltage, current, and battery temperature +class MiniHomeVoltage extends ReusableReactiveWidget { + /// Const constructor for the home voltage widget + const MiniHomeVoltage(super.model); + + /// An appropriate battery icon in increments of 1/8 battery level. + IconData getBatteryIcon(double percentage) { + if (percentage >= 0.84) { + return Icons.battery_full; + } // 80-100 + else if (percentage >= 0.72) { + return Icons.battery_6_bar; + } // 60-80 + else if (percentage >= 0.60) { + return Icons.battery_5_bar; + } // 60-80 + else if (percentage >= 0.48) { + return Icons.battery_4_bar; + } // 60-80 + else if (percentage >= 0.36) { + return Icons.battery_3_bar; + } // 60-80 + else if (percentage >= 0.24) { + return Icons.battery_2_bar; + } // 40-60 + else if (percentage >= 0.12) { + return Icons.battery_1_bar; + } // 20-40 + else { + return Icons.battery_0_bar; + } // 0-20 + } + + @override + Widget build(BuildContext context, DriveMetrics model) => Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 150, + height: 150, + child: FittedBox( + fit: BoxFit.fill, + child: Icon( + getBatteryIcon(model.batteryPercentage), + ), + ), + ), + const SizedBox(width: 5), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${model.batteryVoltage.toStringAsFixed(2)} V", + style: context.textTheme.displaySmall, + ), + Text( + "${model.data.batteryCurrent.toStringAsFixed(2)} A", + style: context.textTheme.displaySmall, + ), + Text( + "${model.data.batteryTemperature.toStringAsFixed(2)} °C", + style: context.textTheme.displaySmall, + ), + ], + ), + ], + ); +} + +/// Toggle options that appear at the bottom of the home page +/// +/// Displays switches for enabling the dashboard or setting the rover to idle +class MiniHomeToggleOptions extends ReusableReactiveWidget { + /// Const constructor + const MiniHomeToggleOptions(super.model); + + @override + Widget build(BuildContext context, Sockets model) => Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Row( + children: [ + const Text("Dashboard Enabled"), + const SizedBox(width: 5), + Switch( + value: model.isEnabled, + onChanged: (enabled) async { + if (enabled) { + if (model.rover != RoverType.localhost) { + await model.setRover(RoverType.localhost); + } + } + + if (!enabled) { + for (final socket in model.sockets) { + await socket.dispose(); + } + await model.disable(); + } else { + await model.init(); + } + }, + ), + ], + ), + Row( + children: [ + const Text("Idle"), + const SizedBox(width: 5), + ValueListenableBuilder( + valueListenable: models.rover.status, + builder: (context, value, child) => Switch( + value: value == RoverStatus.IDLE, + onChanged: (idle) async { + final value = idle ? RoverStatus.IDLE : RoverStatus.MANUAL; + + await models.rover.settings.setStatus(value); + }, + ), + ), + ], + ), + ], + ); +} + +/// Systems status cards for the mini home page +/// +/// Displays a color status indicator and a button to restart the system +class MiniHomeSystemStatus extends ReusableReactiveWidget { + /// Const constructor for system status cards + const MiniHomeSystemStatus(super.model); + + /// Returns the appropriate status icon for the log messages received from [device] + Widget statusIcon(Device? device) { + final socket = models.sockets.socketForDevice(device ?? Device.DEVICE_UNDEFINED); + final lowestLevel = model.options.getSeverity(device ?? Device.DEVICE_UNDEFINED); + + Color? iconColor = switch (lowestLevel) { + BurtLogLevel.critical => Colors.red, + BurtLogLevel.info || BurtLogLevel.debug || BurtLogLevel.trace => Colors.green, + BurtLogLevel.warning => Colors.yellow, // Separate line in case if we need to change it at any point + BurtLogLevel.error => Colors.red, + _ => null, + }; + + if (device == null || socket == null || !socket.isConnected) { + iconColor = Colors.black; + } + + return Icon(Icons.circle, color: iconColor); + } + + @override + Widget build(BuildContext context, LogsViewModel model) => Column( + children: [ + const Spacer(), + Expanded( + flex: 8, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: Image.asset( + context.colorScheme.brightness == Brightness.light ? "assets/logo-light.png" : "assets/logo-dark.png", + ), + ), + ), + const Spacer(), + for (final device in [Device.SUBSYSTEMS, Device.VIDEO, Device.AUTONOMY]) + SizedBox( + width: 300, + child: Card( + color: Colors.white, + elevation: 3, + child: Column( + children: [ + ListTile( + title: Text(device.humanName, style: const TextStyle(color: Colors.black)), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + statusIcon(device), + TextButton.icon( + icon: const Icon(Icons.restart_alt), + onPressed: () { + model.options.resetDevice(device); + }, + label: const Text("Restart Device"), + ), + ], + ), + const SizedBox(height: 5), + ], + ), + ), + ), + const SizedBox(height: 5), + ], + ); +} diff --git a/lib/src/pages/mini_logs.dart b/lib/src/pages/mini_logs.dart new file mode 100644 index 000000000..31999f073 --- /dev/null +++ b/lib/src/pages/mini_logs.dart @@ -0,0 +1,52 @@ +import "dart:async"; + +import "package:flutter/material.dart"; +import "package:rover_dashboard/mini.dart"; +import "package:rover_dashboard/models.dart"; +import "package:rover_dashboard/pages.dart"; + +/// Logs tab for the mini dashboard +/// +/// Displays the same content of the normal logs page but without the extra header and footer +class MiniLogs extends StatefulWidget { + /// MiniViewModel used to control the widget on the footer of the mini dashboard + final MiniViewModel miniViewModel; + + /// Constructor for mini logs + const MiniLogs({required this.miniViewModel, super.key}); + + @override + State createState() => _MiniLogsState(); +} + +class _MiniLogsState extends State { + /// [LogsViewModel] used to view the different elements of the logs page + final LogsViewModel logsViewModel = LogsViewModel(); + + @override + void initState() { + // The footer has to be set in Timer.run since otherwise it will try to rebuild during build + Timer.run( + () => widget.miniViewModel.footerWidget = (context) => Row( + children: getLogsActions(context, logsViewModel), + ), + ); + super.initState(); + } + + @override + void dispose() { + Timer.run(() => widget.miniViewModel.footerWidget = null); + super.dispose(); + } + + @override + Widget build(BuildContext context) => Column( + children: [ + const SizedBox(height: 12), + LogsOptions(logsViewModel.options), + const Divider(), + Expanded(child: LogsBody(logsViewModel)), + ], + ); +} diff --git a/lib/src/pages/mini_metrics.dart b/lib/src/pages/mini_metrics.dart new file mode 100644 index 000000000..e7ba485e1 --- /dev/null +++ b/lib/src/pages/mini_metrics.dart @@ -0,0 +1,59 @@ +import "package:flutter/material.dart"; +import "package:rover_dashboard/models.dart"; +import "package:rover_dashboard/widgets.dart"; + +/// Metrics for the Mini Dashboard +/// +/// Displays 2 columns of expansion tiles, the left is the regular rover +/// metrics, and the right is the controls +class MiniMetrics extends ReusableReactiveWidget { + /// Const constructor for mini metrics + const MiniMetrics(super.model); + + @override + Widget build(BuildContext context, RoverMetrics model) => Row( + children: [ + const Spacer(), + Expanded( + flex: 3, + child: ListView( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + children: [ + Text( + "Metrics", + style: context.textTheme.displaySmall, + textAlign: TextAlign.center, + ), + for (final metrics in model.allMetrics) MetricsList(metrics), + ], + ), + ), + const Expanded(child: VerticalDivider()), + Expanded( + flex: 3, + child: ListView( + children: [ + Text( + "Controls", + style: context.textTheme.displaySmall, + textAlign: TextAlign.center, + ), + ControlsDisplay( + controller: models.rover.controller1, + gamepadNum: 1, + ), + ControlsDisplay( + controller: models.rover.controller2, + gamepadNum: 2, + ), + ControlsDisplay( + controller: models.rover.controller3, + gamepadNum: 3, + ), + ], + ), + ), + const Spacer(), + ], + ); +} diff --git a/lib/src/widgets/generic/gamepad.dart b/lib/src/widgets/generic/gamepad.dart index b9d5c3713..c37f4994e 100644 --- a/lib/src/widgets/generic/gamepad.dart +++ b/lib/src/widgets/generic/gamepad.dart @@ -36,11 +36,19 @@ class GamepadButton extends ReusableReactiveWidget { IconButton( icon: Stack( children: [ - const Icon(Icons.sports_esports), + const SizedBox(height: 32), + Icon(Icons.sports_esports, color: context.colorScheme.onSurface), Positioned( - bottom: -2, - right: -2, - child: Text("${model.index + 1}", style: const TextStyle(fontSize: 12, color: Colors.white)), + bottom: 0, + right: 4.5, + child: Text( + "${model.index + 1}", + style: TextStyle( + fontSize: 12, + color: context.colorScheme.onSurface, + fontWeight: FontWeight.bold, + ), + ), ), ], ), diff --git a/lib/src/widgets/navigation/footer.dart b/lib/src/widgets/navigation/footer.dart index 7148a58ac..dee6f8fae 100644 --- a/lib/src/widgets/navigation/footer.dart +++ b/lib/src/widgets/navigation/footer.dart @@ -255,7 +255,9 @@ class MessageDisplay extends ReusableReactiveWidget { Widget build(BuildContext context, HomeModel model) => SizedBox( height: 48, child: InkWell( - onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => LogsPage())), + onTap: showLogs + ? () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => LogsPage())) + : null, child: Card( shadowColor: Colors.transparent, color: getColor(model.message?.severity),