diff --git a/logo.png b/assets/logo.png similarity index 100% rename from logo.png rename to assets/logo.png diff --git a/assets/rover.png b/assets/rover.png new file mode 100644 index 000000000..a07a7f127 Binary files /dev/null and b/assets/rover.png differ diff --git a/lib/src/data/settings.dart b/lib/src/data/settings.dart index 43e628ac5..8a8e26843 100644 --- a/lib/src/data/settings.dart +++ b/lib/src/data/settings.dart @@ -200,14 +200,22 @@ class AutonomySettings { /// /// Implement these! Ask Levi for details. class EasterEggsSettings { + /// Whether to do a SEGA-like intro during boot. + final bool segaIntro; + /// A const constructor. - const EasterEggsSettings(); + const EasterEggsSettings({ + required this.segaIntro, + }); /// Parses easter eggs settings from JSON. - EasterEggsSettings.fromJson(Json? json); // ignore: avoid_unused_constructor_parameters + EasterEggsSettings.fromJson(Json? json) : + segaIntro = json?["segaIntro"] ?? true; /// Serializes these settings to JSON. - Json toJson() => { }; + Json toJson() => { + "segaIntro": segaIntro, + }; } /// Contains the settings for running the dashboard and the rover. diff --git a/lib/src/models/data/settings.dart b/lib/src/models/data/settings.dart index c3fe55aed..56b018aea 100644 --- a/lib/src/models/data/settings.dart +++ b/lib/src/models/data/settings.dart @@ -22,6 +22,9 @@ class SettingsModel extends Model { /// The user's autonomy settings. AutonomySettings get autonomy => all.autonomy; + /// The user's easter egg settings. + EasterEggsSettings get easterEggs => all.easterEggs; + @override Future init() async { all = await services.files.readSettings(); diff --git a/lib/src/models/view/builders/settings_builder.dart b/lib/src/models/view/builders/settings_builder.dart index fdd2f754f..d438475cb 100644 --- a/lib/src/models/view/builders/settings_builder.dart +++ b/lib/src/models/view/builders/settings_builder.dart @@ -211,6 +211,28 @@ class AutonomySettingsBuilder extends ValueBuilder { AutonomySettings get value => AutonomySettings(blockSize: blockSize.value); } +/// A [ValueBuilder] that modifies an [EasterEggsSettings]. +class EasterEggsSettingsBuilder extends ValueBuilder { + /// Whether to show a SEGA intro. See [EasterEggsSettings.segaIntro]. + bool segaIntro; + + /// Fills in the fields with the given [initial] settings. + EasterEggsSettingsBuilder(EasterEggsSettings initial) : + segaIntro = initial.segaIntro; + + @override + bool get isValid => true; + + @override + EasterEggsSettings get value => EasterEggsSettings(segaIntro: segaIntro); + + /// Updates the value of [EasterEggsSettings.segaIntro]. + void updateSegaIntro(bool input) { // ignore: avoid_positional_boolean_parameters + segaIntro = input; + notifyListeners(); + } +} + /// A [ValueBuilder] representing an [ArmSettings]. class SettingsBuilder extends ValueBuilder { /// The [NetworkSettings] view model. @@ -228,6 +250,9 @@ class SettingsBuilder extends ValueBuilder { /// The [AutonomySettings] view model. final AutonomySettingsBuilder autonomy; + /// The [EasterEggsSettings] view model. + final EasterEggsSettingsBuilder easterEggs; + /// Whether the page is loading. bool isLoading = false; @@ -237,13 +262,15 @@ class SettingsBuilder extends ValueBuilder { network = NetworkSettingsBuilder(models.settings.network), arm = ArmSettingsBuilder(models.settings.arm), video = VideoSettingsBuilder(models.settings.video), - science = ScienceSettingsBuilder(models.settings.science) + science = ScienceSettingsBuilder(models.settings.science), + easterEggs = EasterEggsSettingsBuilder(models.settings.easterEggs) { autonomy.addListener(notifyListeners); network.addListener(notifyListeners); arm.addListener(notifyListeners); video.addListener(notifyListeners); science.addListener(notifyListeners); + easterEggs.addListener(notifyListeners); } @override @@ -258,7 +285,7 @@ class SettingsBuilder extends ValueBuilder { autonomy: autonomy.value, network: network.value, video: video.value, - easterEggs: const EasterEggsSettings(), + easterEggs: easterEggs.value, science: science.value, arm: arm.value, ); diff --git a/lib/src/pages/settings.dart b/lib/src/pages/settings.dart index 5b4301d21..875cc50dd 100644 --- a/lib/src/pages/settings.dart +++ b/lib/src/pages/settings.dart @@ -132,10 +132,14 @@ class SettingsPage extends StatelessWidget { ], ), const Divider(), - const ValueEditor( + ValueEditor( name: "Easter eggs", children: [ - ListTile(title: Text("Coming soon!")), + SwitchListTile( + title: const Text("Enable SEGA Intro"), + value: model.easterEggs.segaIntro, + onChanged: model.easterEggs.updateSegaIntro, + ), ], ), const Divider(), diff --git a/lib/src/pages/splash.dart b/lib/src/pages/splash.dart index 3d5a94168..6911cff8c 100644 --- a/lib/src/pages/splash.dart +++ b/lib/src/pages/splash.dart @@ -4,6 +4,7 @@ import "package:flutter/services.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"; /// Initializes the dashboard and handles errors. class SplashPage extends StatefulWidget { @@ -11,6 +12,16 @@ class SplashPage extends StatefulWidget { SplashPageState createState() => SplashPageState(); } +/// The state of the SEGA animation. +enum SegaState { + /// The rover is off-screen to the left, facing the right, and the text is transparent. + partOne, + /// The rover is off-screen to the right, facing the right, and the text is 30% opaque. + partTwo, + /// The rover is off-screen to the left, facing the left, and the text is 100% opaque. + partThree, +} + /// Initializes the dashboard and handles errors. class SplashPageState extends State{ /// The error message produced during initialization, if any. @@ -19,12 +30,24 @@ class SplashPageState extends State{ /// The current task, if any. String? current; + /// The state of the SEGA animation. + SegaState state = SegaState.partOne; + @override void initState() { super.initState(); init(); } + /// Starts the SEGA animation. + Future initAnimation() async { + // await Future.delayed(const Duration(milliseconds: 1000)); + setState(() => state = SegaState.partTwo); + await Future.delayed(const Duration(milliseconds: 1000)); + setState(() => state = SegaState.partThree); + await Future.delayed(const Duration(milliseconds: 1000)); + } + /// Calls [Services.init] and [Models.init] while monitoring for errors. Future init() async { try { @@ -37,34 +60,51 @@ class SplashPageState extends State{ current = "models"; await models.init(); + if (models.settings.easterEggs.segaIntro) await initAnimation(); + if (mounted) { + await Navigator.of(context).pushReplacementNamed(Routes.home); + } } catch (error) { setState(() => errorText = error.toString()); rethrow; } - - if (mounted) { - await Navigator.of(context).pushReplacementNamed(Routes.home); - } } @override Widget build(BuildContext context) => Scaffold( - body: Center(child: errorText == null - ? const CircularProgressIndicator() - : Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Spacer(flex: 2), - Text("Something went wrong", style: Theme.of(context).textTheme.displayLarge), - const Spacer(), - Text("The error occurred when trying to initialize $current", style: Theme.of(context).textTheme.headlineLarge), - const SizedBox(height: 24), - Text("Here is the exact error:", style: Theme.of(context).textTheme.titleLarge), - const SizedBox(height: 16), - Text(errorText!), - const Spacer(flex: 2), - ], - ), + body: Stack( + alignment: Alignment.center, + children: [ + AnimatedOpacity( + duration: const Duration(milliseconds: 1000), + opacity: switch (state) { + SegaState.partOne => 0, + SegaState.partTwo => 0.3, + SegaState.partThree => 1, + }, + child: Text("Binghamton University\nRover Team", textAlign: TextAlign.center, style: context.textTheme.displayMedium), + ), + AnimatedAlign( + duration: const Duration(milliseconds: 750), + alignment: switch (state) { + SegaState.partOne => const Alignment(-1.5, 0), + SegaState.partTwo => const Alignment(1.5, 0), + SegaState.partThree => const Alignment(-1.5, 0), + }, + child: Transform.flip( + flipX: switch (state) { + SegaState.partOne => true, + SegaState.partTwo => true, + SegaState.partThree => false, + }, + child: SizedBox( + width: 150, + height: 150, + child: Image.asset("assets/rover.png"), + ), + ), + ), + ], ), ); } diff --git a/pubspec.yaml b/pubspec.yaml index 1d62ea2c8..8ff528b21 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,18 +44,18 @@ dependency_overrides: # Generates icons for the given platforms # Run: flutter pub run icons_launcher:create icons_launcher: - image_path: "logo.png" + image_path: "assets/logo.png" platforms: android: - # adaptive_foreground_image: "logo.png" + # adaptive_foreground_image: "assets/logo.png" # adaptive_background_color: "#000000" - # adaptive_monochrome_image: "logo.png" + # adaptive_monochrome_image: "assets/logo.png" enable: true windows: enable: true flutter_launcher_icons: - image_path: "logo.png" + image_path: "assets/logo.png" android: true windows: generate: true @@ -67,7 +67,7 @@ msix_config: display_name: Dashboard publisher_display_name: Binghamton University Rover Team identity_name: edu.binghamton.rover - logo_path: logo.png + logo_path: assets/logo.png trim_logo: false capabilities: internetClientServer, privateNetworkClientServer, usb, serialcommunication, lowLevel output_name: Dashboard @@ -78,3 +78,5 @@ msix_config: # The following section is specific to Flutter packages. flutter: uses-material-design: true + assets: + - assets/rover.png