From 03b9ae53976376711606b927c0e7e089e4e0d9f3 Mon Sep 17 00:00:00 2001 From: Codel1417 <13484789+Codel1417@users.noreply.github.com> Date: Sun, 28 Jan 2024 01:00:17 -0500 Subject: [PATCH] Initial animations, more localization, lock dev menu behind dev build --- android/app/build.gradle | 2 +- lib/Backend/ActionRegistry.dart | 22 +++ lib/Backend/AutoMove.dart | 2 +- lib/Backend/Bluetooth/BluetoothManager.dart | 14 +- lib/Backend/{ => Bluetooth}/btMessage.dart | 0 lib/Backend/Sensors.dart | 91 ++++++------ lib/Backend/moveLists.dart | 2 +- lib/Frontend/GoRouterConfig.dart | 127 +++++++++++------ lib/Frontend/Widgets/action_selector.dart | 62 ++++++++ lib/Frontend/Widgets/scan_for_new_device.dart | 119 ++++++++-------- lib/Frontend/Widgets/snack_bar_overlay.dart | 46 ++++++ lib/Frontend/intnDefs.dart | 47 +++++- lib/Frontend/pages/Actions.dart | 9 +- lib/Frontend/pages/DirectGearControl.dart | 2 +- lib/Frontend/pages/Home.dart | 127 ++++++++++++----- lib/Frontend/pages/Shell.dart | 13 +- .../pages/developer/developer_menu.dart | 8 ++ lib/Frontend/pages/move_list.dart | 130 +++++++++++------ lib/Frontend/pages/settings.dart | 19 +-- lib/Frontend/pages/triggers.dart | 35 ++--- .../{messages_uwu.arb => messages_ace.arb} | 1 + lib/l10n/messages_en.arb | 134 +++++++++++++++++- lib/main.dart | 40 +++--- pubspec.lock | 134 ++++++++++-------- pubspec.yaml | 8 +- 25 files changed, 838 insertions(+), 356 deletions(-) rename lib/Backend/{ => Bluetooth}/btMessage.dart (100%) create mode 100644 lib/Frontend/Widgets/action_selector.dart create mode 100644 lib/Frontend/Widgets/snack_bar_overlay.dart rename lib/l10n/{messages_uwu.arb => messages_ace.arb} (99%) diff --git a/android/app/build.gradle b/android/app/build.gradle index 076de154..91f82acc 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -22,7 +22,7 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } android { - compileSdkVersion 33 + compileSdkVersion 34 ndkVersion flutter.ndkVersion compileOptions { diff --git a/lib/Backend/ActionRegistry.dart b/lib/Backend/ActionRegistry.dart index caa89562..c6b20b19 100644 --- a/lib/Backend/ActionRegistry.dart +++ b/lib/Backend/ActionRegistry.dart @@ -61,3 +61,25 @@ Map> getAvailableActions(GetAvailableActionsRef } return sortedActions; } + +@Riverpod(dependencies: [KnownDevices]) +Map> getAllActions(GetAllActionsRef ref, Set deviceType) { + Map> sortedActions = {}; + for (BaseAction baseAction in List.from(ActionRegistry.allCommands)..addAll(ref.read(moveListsProvider))) { + Set? baseActions = {}; + // check if command matches device type + if (baseAction.deviceCategory.intersection(deviceType).isNotEmpty) { + // get category if it exists + if (sortedActions.containsKey(baseAction.actionCategory)) { + baseActions = sortedActions[baseAction.actionCategory]; + } + // add action to category + baseActions?.add(baseAction); + } + // store result + if (baseActions != null && baseActions.isNotEmpty) { + sortedActions[baseAction.actionCategory] = baseActions; + } + } + return sortedActions; +} diff --git a/lib/Backend/AutoMove.dart b/lib/Backend/AutoMove.dart index 8a91bc3a..ab2628bb 100644 --- a/lib/Backend/AutoMove.dart +++ b/lib/Backend/AutoMove.dart @@ -1,5 +1,5 @@ +import 'package:tail_app/Backend/Bluetooth/btMessage.dart'; import 'package:tail_app/Backend/Definitions/Device/BaseDeviceDefinition.dart'; -import 'package:tail_app/Backend/btMessage.dart'; //TODO: call on device connect void ChangeAutoMove(BaseStatefulDevice device) { diff --git a/lib/Backend/Bluetooth/BluetoothManager.dart b/lib/Backend/Bluetooth/BluetoothManager.dart index 76953bb4..1529be4d 100644 --- a/lib/Backend/Bluetooth/BluetoothManager.dart +++ b/lib/Backend/Bluetooth/BluetoothManager.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:awesome_snackbar_content/awesome_snackbar_content.dart'; import 'package:collection/collection.dart'; import 'package:cross_platform/cross_platform.dart'; import 'package:flutter_foreground_service/flutter_foreground_service.dart'; @@ -9,11 +10,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logging_flutter/logging_flutter.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:tail_app/Frontend/Widgets/snack_bar_overlay.dart'; import '../../main.dart'; import '../Definitions/Device/BaseDeviceDefinition.dart'; import '../DeviceRegistry.dart'; -import '../btMessage.dart'; +import 'btMessage.dart'; part 'BluetoothManager.g.dart'; @@ -145,14 +147,12 @@ class KnownDevices extends _$KnownDevices { } @Riverpod(keepAlive: true, dependencies: [reactiveBLE]) -StreamSubscription btStatus(BtStatusRef ref) { - return ref.read(reactiveBLEProvider).statusStream.listen((BleStatus event) { - Flogger.i("BluetoothState::$event"); //TODO: Do something with this - }); +Stream btStatus(BtStatusRef ref) { + return ref.read(reactiveBLEProvider).statusStream; } @Riverpod(keepAlive: true, dependencies: [reactiveBLE, KnownDevices]) -StreamSubscription btConnectStatus(BtConnectStatusRef ref) { +StreamSubscription btConnectStateHandler(BtConnectStateHandlerRef ref) { return ref.read(reactiveBLEProvider).connectedDeviceStream.listen((ConnectionStateUpdate event) { Flogger.i("ConnectedDevice::$event"); Map knownDevices = ref.watch(knownDevicesProvider); @@ -169,7 +169,7 @@ StreamSubscription btConnectStatus(BtConnectStatusRef ref knownDevices[event.deviceId]?.rxCharacteristicStream = null; knownDevices[event.deviceId]?.keepAliveStream = null; knownDevices[event.deviceId]?.battery.value = 0; - + ref.read(snackbarStreamProvider.notifier).add(AwesomeSnackbarContent(title: "Disconnected", message: "Disconnected from ${knownDevices[event.deviceId]?.baseStoredDevice.name}", contentType: ContentType.warning)); //remove foreground service if no devices connected if (Platform.isAndroid && knownDevices.values.where((element) => element.deviceConnectionState.value == DeviceConnectionState.connected).isEmpty) { ForegroundService().stop(); diff --git a/lib/Backend/btMessage.dart b/lib/Backend/Bluetooth/btMessage.dart similarity index 100% rename from lib/Backend/btMessage.dart rename to lib/Backend/Bluetooth/btMessage.dart diff --git a/lib/Backend/Sensors.dart b/lib/Backend/Sensors.dart index 74471644..a1c22e47 100644 --- a/lib/Backend/Sensors.dart +++ b/lib/Backend/Sensors.dart @@ -14,6 +14,7 @@ import 'package:proximity_sensor/proximity_sensor.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:shake/shake.dart'; +import '../Frontend/intnDefs.dart'; import '../main.dart'; import 'Bluetooth/BluetoothManager.dart'; import 'Definitions/Action/BaseAction.dart'; @@ -67,7 +68,7 @@ class Trigger { } } - List actions = []; + Map actions = {}; //TODO: Store action as a string, and find on demand Trigger(this.triggerDef) { //actions.addAll(triggerDefinition?.actionTypes.map((e) => TriggerAction(e))); @@ -75,7 +76,7 @@ class Trigger { Trigger.trigDef(this.triggerDefinition) { triggerDef = triggerDefinition!.name; - actions.addAll(triggerDefinition!.actionTypes.map((e) => TriggerAction(e))); + actions.addAll(triggerDefinition!.actionTypes.map((e, f) => MapEntry(e, TriggerAction(f)))); } factory Trigger.fromJson(Map json) => _$TriggerFromJson(json); @@ -89,12 +90,12 @@ abstract class TriggerDefinition implements Comparable { late Widget icon; Ref ref; - Future onEnable(List actions, Set deviceType); + Future onEnable(Map actions, Set deviceType); Future onDisable(); Permission? requiredPermission; - late List actionTypes; + late Map actionTypes; TriggerDefinition(this.ref); @@ -121,11 +122,11 @@ class WalkingTriggerDefinition extends TriggerDefinition { Stream? stepCountStream; WalkingTriggerDefinition(super.ref) { - super.name = "Walking"; - super.description = "Trigger an action on walking"; + super.name = triggerWalkingTitle(); + super.description = triggerWalkingDescription(); super.icon = const Icon(Icons.directions_walk); super.requiredPermission = Permission.activityRecognition; - super.actionTypes = ["Walking", "Stopped", "Even Step", "Odd Step", "Step"]; + super.actionTypes = {"Walking": triggerWalkingTitle(), "Stopped": triggerWalkingStopped(), "Even Step": triggerWalkingEvenStep(), "Odd Step": triggerWalkingOddStep(), "Step": triggerWalkingStep()}; } @override @@ -135,29 +136,29 @@ class WalkingTriggerDefinition extends TriggerDefinition { } @override - Future onEnable(List actions, Set deviceType) async { + Future onEnable(Map actions, Set deviceType) async { pedestrianStatusStream = Pedometer.pedestrianStatusStream; stepCountStream = Pedometer.stepCountStream; pedestrianStatusStream?.listen((PedestrianStatus event) { Flogger.i("PedestrianStatus:: ${event.status}"); if (event.status == "Walking") { - TriggerAction action = actions.firstWhere((TriggerAction element) => element.name == "Walking"); - sendCommands(deviceType, action.action, ref); + TriggerAction? action = actions["Walking"]; + sendCommands(deviceType, action?.action, ref); } else if (event.status == "Stopped") { - TriggerAction action = actions.firstWhere((TriggerAction element) => element.name == "Stopped"); - sendCommands(deviceType, action.action, ref); + TriggerAction? action = actions["Stopped"]; + sendCommands(deviceType, action?.action, ref); } }); stepCountStream?.listen((StepCount event) { Flogger.d("StepCount:: ${event.steps}"); - TriggerAction action = actions.firstWhere((TriggerAction element) => element.name == "Step"); - sendCommands(deviceType, action.action, ref); + TriggerAction? action = actions["Step"]; + sendCommands(deviceType, action?.action, ref); if (event.steps.isEven) { - TriggerAction action = actions.firstWhere((TriggerAction element) => element.name == "Even Step"); - sendCommands(deviceType, action.action, ref); + TriggerAction? action = actions["Even Step"]; + sendCommands(deviceType, action?.action, ref); } else { - TriggerAction action = actions.firstWhere((TriggerAction element) => element.name == "Odd Step"); - sendCommands(deviceType, action.action, ref); + TriggerAction? action = actions["Odd Step"]; + sendCommands(deviceType, action?.action, ref); } }); } @@ -167,11 +168,11 @@ class CoverTriggerDefinition extends TriggerDefinition { Stream? proximityStream; CoverTriggerDefinition(super.ref) { - super.name = "Cover"; - super.description = "Trigger an action by covering the proximity sensor"; + super.name = triggerCoverTitle(); + super.description = triggerCoverDescription(); super.icon = const Icon(Icons.sensors); super.requiredPermission = null; - super.actionTypes = ["Near", "Far"]; + super.actionTypes = {"Near": triggerCoverNear(), "Far": triggerCoverFar()}; } @override @@ -180,16 +181,16 @@ class CoverTriggerDefinition extends TriggerDefinition { } @override - Future onEnable(List actions, Set deviceType) async { + Future onEnable(Map actions, Set deviceType) async { proximityStream = ProximitySensor.events; proximityStream?.listen((int event) { Flogger.d("CoverEvent:: $event"); if (event >= 1) { - TriggerAction action = actions.firstWhere((TriggerAction element) => element.name == "Near"); - sendCommands(deviceType, action.action, ref); + TriggerAction? action = actions["Near"]; + sendCommands(deviceType, action?.action, ref); } else if (event == 0) { - TriggerAction action = actions.firstWhere((TriggerAction element) => element.name == "Far"); - sendCommands(deviceType, action.action, ref); + TriggerAction? action = actions["Far"]; + sendCommands(deviceType, action?.action, ref); } }); } @@ -199,11 +200,11 @@ class VolumeButtonTriggerDefinition extends TriggerDefinition { StreamSubscription? subscription; VolumeButtonTriggerDefinition(super.ref) { - super.name = "Volume Buttons"; - super.description = "Trigger an action by pressing the volume button"; + super.name = triggerVolumeButtonTitle(); + super.description = triggerVolumeButtonDescription(); super.icon = const Icon(Icons.volume_up); super.requiredPermission = null; - super.actionTypes = ["Volume Up", "Volume Down"]; + super.actionTypes = {"Volume Up": triggerVolumeButtonVolumeUp(), "Volume Down": triggerVolumeButtonVolumeDown()}; } @override @@ -215,15 +216,15 @@ class VolumeButtonTriggerDefinition extends TriggerDefinition { } @override - Future onEnable(List actions, Set deviceType) async { + Future onEnable(Map actions, Set deviceType) async { subscription = FlutterAndroidVolumeKeydown.stream.listen((event) { Flogger.d("Volume press detected:${event.name}"); if (event == HardwareButton.volume_down) { - TriggerAction action = actions.firstWhere((TriggerAction element) => element.name == "Volume Up"); - sendCommands(deviceType, action.action, ref); + TriggerAction? action = actions["Volume Up"]; + sendCommands(deviceType, action?.action, ref); } else if (event == HardwareButton.volume_up) { - TriggerAction action = actions.firstWhere((TriggerAction element) => element.name == "Volume Down"); - sendCommands(deviceType, action.action, ref); + TriggerAction? action = actions["Volume Down"]; + sendCommands(deviceType, action?.action, ref); } }); } @@ -233,11 +234,11 @@ class ShakeTriggerDefinition extends TriggerDefinition { ShakeDetector? detector; ShakeTriggerDefinition(super.ref) { - super.name = "Shake"; + super.name = triggerShakeTitle(); super.description = "Trigger an action by shaking your device"; super.icon = const Icon(Icons.vibration); super.requiredPermission = null; - super.actionTypes = ["Shake"]; + super.actionTypes = {"Shake": triggerShakeTitle()}; } @override @@ -247,11 +248,11 @@ class ShakeTriggerDefinition extends TriggerDefinition { } @override - Future onEnable(List actions, Set deviceType) async { + Future onEnable(Map actions, Set deviceType) async { detector = ShakeDetector.waitForStart(onPhoneShake: () { Flogger.d("Shake Detected"); - TriggerAction action = actions.firstWhere((TriggerAction element) => element.name == "Shake"); - sendCommands(deviceType, action.action, ref); + TriggerAction? action = actions["Shake"]; + sendCommands(deviceType, action?.action, ref); }); detector?.startListening(); } @@ -261,11 +262,11 @@ class TailProximityTriggerDefinition extends TriggerDefinition { Stream? btStream; TailProximityTriggerDefinition(super.ref) { - super.name = "Proximity"; - super.description = "Trigger an action if gear is nearby"; + super.name = triggerProximityTitle(); + super.description = triggerProximityDescription(); super.icon = const Icon(Icons.bluetooth_connected); super.requiredPermission = Permission.bluetoothScan; - super.actionTypes = ["Nearby Gear"]; + super.actionTypes = {"Nearby Gear": triggerProximityTitle()}; } @override @@ -274,12 +275,12 @@ class TailProximityTriggerDefinition extends TriggerDefinition { } @override - Future onEnable(List actions, Set deviceType) async { + Future onEnable(Map actions, Set deviceType) async { btStream = ref.read(reactiveBLEProvider).scanForDevices(withServices: DeviceRegistry.getAllIds()).where((event) => !ref.read(knownDevicesProvider).keys.contains(event.id)); btStream?.listen((DiscoveredDevice device) { Flogger.d("TailProximityTriggerDefinition:: $device"); - TriggerAction action = actions.firstWhere((TriggerAction element) => element.name == "Nearby Gear"); - sendCommands(deviceType, action.action, ref); + TriggerAction? action = actions["Nearby Gear"]; + sendCommands(deviceType, action?.action, ref); }); } } diff --git a/lib/Backend/moveLists.dart b/lib/Backend/moveLists.dart index eb7e44d2..45bf564e 100644 --- a/lib/Backend/moveLists.dart +++ b/lib/Backend/moveLists.dart @@ -5,9 +5,9 @@ import 'package:flutter/material.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:logging_flutter/logging_flutter.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:tail_app/Backend/Bluetooth/btMessage.dart'; import 'package:tail_app/Backend/Definitions/Action/BaseAction.dart'; import 'package:tail_app/Backend/Definitions/Device/BaseDeviceDefinition.dart'; -import 'package:tail_app/Backend/btMessage.dart'; import 'package:tail_app/Frontend/intnDefs.dart'; import '../main.dart'; diff --git a/lib/Frontend/GoRouterConfig.dart b/lib/Frontend/GoRouterConfig.dart index c46f5021..05fd2cbf 100644 --- a/lib/Frontend/GoRouterConfig.dart +++ b/lib/Frontend/GoRouterConfig.dart @@ -1,8 +1,8 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:tail_app/Frontend/pages/DirectGearControl.dart'; -import 'package:tail_app/Frontend/pages/Home.dart'; import 'package:tail_app/Frontend/pages/Shell.dart'; import 'package:tail_app/Frontend/pages/developer/developer_menu.dart'; import 'package:tail_app/Frontend/pages/developer/json_preview.dart'; @@ -17,7 +17,7 @@ final GlobalKey _shellNavigatorKey = GlobalKey() // GoRouter configuration final GoRouter router = GoRouter( - debugLogDiagnostics: true, + debugLogDiagnostics: kDebugMode, navigatorKey: _rootNavigatorKey, observers: [SentryNavigatorObserver()], routes: [ @@ -27,20 +27,22 @@ final GoRouter router = GoRouter( parentNavigatorKey: _rootNavigatorKey, builder: (BuildContext context, GoRouterState state) => const Settings(), routes: [ - GoRoute( - name: 'Developer Menu', - path: 'developer', - parentNavigatorKey: _rootNavigatorKey, - builder: (BuildContext context, GoRouterState state) => const DeveloperMenu(), - routes: [ - GoRoute( - name: 'JSON Viewer', - path: 'json', - parentNavigatorKey: _rootNavigatorKey, - builder: (BuildContext context, GoRouterState state) => const JsonPreview(), - ) - ], - ) + if (kDebugMode) ...[ + GoRoute( + name: 'Developer Menu', + path: 'developer', + parentNavigatorKey: _rootNavigatorKey, + builder: (BuildContext context, GoRouterState state) => const DeveloperMenu(), + routes: [ + GoRoute( + name: 'JSON Viewer', + path: 'json', + parentNavigatorKey: _rootNavigatorKey, + builder: (BuildContext context, GoRouterState state) => const JsonPreview(), + ) + ], + ) + ] ], ), GoRoute( @@ -50,41 +52,74 @@ final GoRouter router = GoRouter( builder: (BuildContext context, GoRouterState state) => const DirectGearControl(), ), ShellRoute( - navigatorKey: _shellNavigatorKey, - routes: [ - GoRoute( - name: 'Home', - path: '/', - parentNavigatorKey: _shellNavigatorKey, - builder: (BuildContext context, GoRouterState state) => const Home(title: "Home"), + navigatorKey: _shellNavigatorKey, + routes: [ + GoRoute( + name: 'Actions', + path: '/', + parentNavigatorKey: _shellNavigatorKey, + pageBuilder: (BuildContext context, GoRouterState state) => CustomTransitionPage( + child: const ActionPage(), + transitionsBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { + return FadeTransition( + opacity: animation, + child: child, + ); + }, ), - GoRoute( - name: 'Actions', - path: '/actions', - parentNavigatorKey: _shellNavigatorKey, - builder: (BuildContext context, GoRouterState state) => const ActionPage(), + ), + GoRoute( + name: 'Triggers', + path: '/triggers', + parentNavigatorKey: _shellNavigatorKey, + pageBuilder: (BuildContext context, GoRouterState state) => CustomTransitionPage( + child: const Triggers(), + transitionsBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { + return FadeTransition( + opacity: animation, + child: child, + ); + }, ), - GoRoute( - name: 'Triggers', - path: '/triggers', - parentNavigatorKey: _shellNavigatorKey, - builder: (context, GoRouterState state) => const Triggers(), + ), + GoRoute( + name: 'Sequences', + path: '/moveLists', + parentNavigatorKey: _shellNavigatorKey, + pageBuilder: (BuildContext context, GoRouterState state) => CustomTransitionPage( + child: const MoveListView(), + transitionsBuilder: (BuildContext context, Animation animation, Animation secondaryAnimation, Widget child) { + return FadeTransition( + opacity: animation, + child: child, + ); + }, ), - GoRoute( - name: 'Sequences', - path: '/moveLists', - parentNavigatorKey: _shellNavigatorKey, - builder: (BuildContext context, GoRouterState state) => const MoveListView(), - routes: [ - GoRoute( + routes: [ + GoRoute( name: 'Edit Sequence', path: 'editMoveList', parentNavigatorKey: _rootNavigatorKey, - builder: (context, state) => const EditMoveList(), - ), - ], - ), - ], - pageBuilder: (BuildContext context, GoRouterState state, Widget child) => NoTransitionPage(child: NavigationDrawerExample(child, state.matchedLocation))), + pageBuilder: (context, state) { + return CustomTransitionPage( + key: state.pageKey, + child: const EditMoveList(), + transitionsBuilder: (context, animation, secondaryAnimation, child) { + // Change the opacity of the screen using a Curve based on the the animation's + // value + return FadeTransition( + opacity: animation, + child: child, + ); + }, + ); + }), + ], + ), + ], + pageBuilder: (BuildContext context, GoRouterState state, Widget child) => NoTransitionPage( + child: NavigationDrawerExample(child, state.matchedLocation), + ), + ), ], ); diff --git a/lib/Frontend/Widgets/action_selector.dart b/lib/Frontend/Widgets/action_selector.dart new file mode 100644 index 00000000..e4bf77e2 --- /dev/null +++ b/lib/Frontend/Widgets/action_selector.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../Backend/ActionRegistry.dart'; +import '../../Backend/Definitions/Action/BaseAction.dart'; +import '../../Backend/Definitions/Device/BaseDeviceDefinition.dart'; + +class ActionSelector extends ConsumerWidget { + ActionSelector({super.key, required this.deviceType}); + + Set deviceType; + + @override + Widget build(BuildContext context, WidgetRef ref) { + Map> actionsCatMap = ref.read(getAllActionsProvider(deviceType)); + List catList = actionsCatMap.keys.toList(); + return Scaffold( + primary: true, + appBar: AppBar( + title: const Text('Select an Action'), + ), + body: ListView.builder( + primary: true, + itemCount: catList.length, + itemBuilder: (BuildContext context, int categoryIndex) { + List actionsForCat = actionsCatMap.values.toList()[categoryIndex].toList(); + return Column( + children: [ + Center( + child: Text( + catList[categoryIndex].friendly, + style: Theme.of(context).textTheme.titleMedium, + ), + ), + GridView.builder( + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: 125), + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: actionsForCat.length, + itemBuilder: (BuildContext context, int actionIndex) { + return InkWell( + onTap: () => Navigator.pop(context, actionsForCat[actionIndex]), + child: Card( + elevation: 2, + child: SizedBox( + height: 50, + width: 50, + child: Center( + child: Text(actionsForCat[actionIndex].name, semanticsLabel: actionsForCat[actionIndex].name), + ), + ), + ), + ); + }, + ) + ], + ); + }, + ), + ); + } +} diff --git a/lib/Frontend/Widgets/scan_for_new_device.dart b/lib/Frontend/Widgets/scan_for_new_device.dart index 1306e4f7..69b57a4c 100644 --- a/lib/Frontend/Widgets/scan_for_new_device.dart +++ b/lib/Frontend/Widgets/scan_for_new_device.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:reactive_ble_platform_interface/src/model/discovered_device.dart'; import 'package:tail_app/Backend/Definitions/Device/BaseDeviceDefinition.dart'; import '../../Backend/Bluetooth/BluetoothManager.dart'; @@ -27,65 +27,72 @@ class _ScanForNewDevice extends ConsumerState { @override Widget build(BuildContext context) { - final AsyncValue foundDevices = ref.watch(scanForDevicesProvider); - if (foundDevices.valueOrNull != null) { - DiscoveredDevice? value = foundDevices.valueOrNull; - if (value != null && !devices.containsKey(value.id)) { - if (ref.read(preferencesProvider).autoConnectNewDevices) { - Future(() => ref.read(knownDevicesProvider.notifier).connect(value)); - } else { - devices[value.id] = value; + AsyncValue btStatus = ref.watch(btStatusProvider); + if (btStatus.valueOrNull != null && btStatus.valueOrNull == BleStatus.ready) { + final AsyncValue foundDevices = ref.watch(scanForDevicesProvider); + if (foundDevices.valueOrNull != null) { + DiscoveredDevice? value = foundDevices.valueOrNull; + if (value != null && !devices.containsKey(value.id)) { + if (ref.read(preferencesProvider).autoConnectNewDevices) { + Future(() => ref.read(knownDevicesProvider.notifier).connect(value)); + } else { + devices[value.id] = value; + } } } - } - List devicesList = devices.values.toList(); - return Column( - children: [ - ListTile( - dense: true, - trailing: Switch( - onChanged: (bool value) { - setState(() { - ref.read(preferencesProvider).autoConnectNewDevices = value; - }); - ref.read(preferencesProvider.notifier).store(); - }, - value: ref.read(preferencesProvider).autoConnectNewDevices, - ), - title: Text(scanDevicesAutoConnectTitle()), - ), - ListView.builder( - shrinkWrap: true, - itemCount: devices.length, - controller: _controller, - itemBuilder: (context, index) { - return ListTile( - title: Text(getNameFromBTName(devicesList[index].name)), - trailing: Text(devicesList[index].id), - onTap: () { - ref.watch(knownDevicesProvider.notifier).connect(devicesList[index]); + List devicesList = devices.values.toList(); + return Column( + children: [ + ListTile( + dense: true, + trailing: Switch( + onChanged: (bool value) { setState(() { - devices.remove(devicesList[index].id); + ref.read(preferencesProvider).autoConnectNewDevices = value; }); - //Navigator.pop(context); + ref.read(preferencesProvider.notifier).store(); }, - ); - }, - ), - Padding( - padding: const EdgeInsets.only(top: 20), - child: Center( - child: Column( - children: [ - const CircularProgressIndicator(), - Padding( - padding: const EdgeInsets.all(16.0), - child: Text(scanDevicesScanMessage()), - ) - ], - ), - )), - ], - ); + value: ref.read(preferencesProvider).autoConnectNewDevices, + ), + title: Text(scanDevicesAutoConnectTitle()), + ), + ListView.builder( + shrinkWrap: true, + itemCount: devices.length, + controller: _controller, + itemBuilder: (context, index) { + return ListTile( + title: Text(getNameFromBTName(devicesList[index].name)), + trailing: Text(devicesList[index].id), + onTap: () { + ref.watch(knownDevicesProvider.notifier).connect(devicesList[index]); + setState(() { + devices.remove(devicesList[index].id); + }); + //Navigator.pop(context); + }, + ); + }, + ), + Padding( + padding: const EdgeInsets.only(top: 20), + child: Center( + child: Column( + children: [ + const CircularProgressIndicator(), + Padding( + padding: const EdgeInsets.all(16.0), + child: Text(scanDevicesScanMessage()), + ) + ], + ), + )), + ], + ); + } else { + return Center( + child: Text(actionsNoBluetooth()), //TODO: More detail + ); + } } } diff --git a/lib/Frontend/Widgets/snack_bar_overlay.dart b/lib/Frontend/Widgets/snack_bar_overlay.dart new file mode 100644 index 00000000..caa2c6f8 --- /dev/null +++ b/lib/Frontend/Widgets/snack_bar_overlay.dart @@ -0,0 +1,46 @@ +import 'dart:async'; + +import 'package:awesome_snackbar_content/awesome_snackbar_content.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'snack_bar_overlay.g.dart'; + +@Riverpod() +class SnackbarStream extends _$SnackbarStream { + StreamController streamController = StreamController(); + + @override + Stream build() => streamController.stream; + + void add(AwesomeSnackbarContent content) => streamController.add(content); +} + +class SnackBarOverlay extends ConsumerWidget { + const SnackBarOverlay({super.key, required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context, WidgetRef ref) { + AsyncValue value = ref.watch(snackbarStreamProvider); + if (value.hasValue) { + Future( + () { + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + elevation: 0, + behavior: SnackBarBehavior.floating, + backgroundColor: Colors.transparent, + content: value.value!, + ), + ); + }, + ); + } + return child; + } +} diff --git a/lib/Frontend/intnDefs.dart b/lib/Frontend/intnDefs.dart index 441be455..002caf3e 100644 --- a/lib/Frontend/intnDefs.dart +++ b/lib/Frontend/intnDefs.dart @@ -3,7 +3,7 @@ import 'package:intl/intl.dart'; //generate file for translation. Run when adding new translations //dart run intl_translation:extract_to_arb --locale=en --output-file='./lib/l10n/messages_en.arb' ./lib/Frontend/intnDefs.dart //convert to dart TODO: -// dart run intl_translation:generate_from_arb --output-dir=lib/l10n --no-use-deferred-loading lib/Frontend/intnDefs.dart lib/l10n/*_messages.arb +// dart run intl_translation:generate_from_arb --output-dir=lib/l10n --no-use-deferred-loading lib/Frontend/intnDefs.dart lib/l10n/*.arb String title() => Intl.message('Tail App', name: 'title', desc: 'The name of the app'); String subTitle() => Intl.message('All of the Tails', name: 'subTitle', desc: 'The sub-title which displays in the navigation drawer'); @@ -74,6 +74,10 @@ String sequencesEditTime() => Intl.message('Time', name: 'sequencesEditTime', de String sequencesEditHomeLabel() => Intl.message('Home the Gear', name: 'sequencesEditHomeLabel', desc: 'Label on the home tab of the move edit page'); +String sequencesEditDeleteTitle() => Intl.message('Delete Sequence', name: 'sequencesEditDeleteTitle', desc: 'Title of the dialog on the sequence edit page to delete the sequence'); + +String sequencesEditDeleteDescription() => Intl.message('Are you sure you want to delete this sequence?', name: 'sequencesEditDeleteDescription', desc: 'Message of the dialog on the sequence edit page to delete the sequence'); + String sequenceEditListDelayLabel(int howMany) => Intl.message( '''Intl.plural(howMany, one: ' Delay next move for $howMany second.', other: 'Delay next move for $howMany seconds.')''', name: 'sequenceEditListDelayLabel', @@ -84,6 +88,8 @@ String sequenceEditListDelayLabel(int howMany) => Intl.message( //Actions Page String actionsNoGear() => Intl.message('No Gear Connected', name: 'actionsNoGear', desc: 'Label on the actions page when no gear is connected'); +String actionsNoBluetooth() => Intl.message('Bluetooth is unavailable', name: 'actionsNoBluetooth', desc: 'Label on the actions page when bluetooth is unavailable'); + String actionsCategoryCalm() => Intl.message('Calm and Relaxed', name: 'actionsCategoryCalm', desc: 'calm action group label'); String actionsCategoryFast() => Intl.message('Fast and Excited', name: 'actionsCategoryFast', desc: 'fast action group label'); @@ -147,3 +153,42 @@ String manageDevicesForget() => Intl.message('Forget', name: 'manageDevicesForge String scanDevicesAutoConnectTitle() => Intl.message('Automatically connect to new devices', name: 'scanDevicesAutoConnectTitle', desc: 'scan for devices auto connect toggle title when scanning for a device'); String scanDevicesScanMessage() => Intl.message('Scanning for gear. Please make sure your gear is powered on and nearby', name: 'scanDevicesScanMessage', desc: 'scan for devices scan in progress message when scanning for a device'); + +//Triggers +String triggerWalkingTitle() => Intl.message('Walking', name: 'triggerWalkingTitle', desc: 'Walking/Step trigger title'); + +String triggerWalkingDescription() => Intl.message('Trigger an action on walking', name: 'triggerWalkingDescription', desc: 'Walking/Step trigger description'); + +String triggerWalkingStopped() => Intl.message('Stopped', name: 'triggerWalkingStopped', desc: 'Walking/Step trigger Stopped action label'); + +String triggerWalkingEvenStep() => Intl.message('Even Step', name: 'triggerWalkingEvenStep', desc: 'Walking/Step trigger Even Step action label'); + +String triggerWalkingOddStep() => Intl.message('Odd Step', name: 'triggerWalkingOddStep', desc: 'Walking/Step trigger Odd Step action label'); + +String triggerWalkingStep() => Intl.message('Step', name: 'triggerWalkingStep', desc: 'Walking/Step trigger Step action label'); + +String triggerCoverTitle() => Intl.message('Cover', name: 'triggerCoverTitle', desc: 'Cover trigger Title'); + +String triggerCoverDescription() => Intl.message("Trigger an action by covering the proximity sensor", name: 'triggerCoverDescription', desc: 'Cover trigger description'); + +String triggerCoverNear() => Intl.message("Near", name: 'triggerCoverNear', desc: 'Cover trigger near action label'); + +String triggerCoverFar() => Intl.message("Far", name: 'triggerCoverFar', desc: 'Cover trigger far action label'); + +String triggerVolumeButtonTitle() => Intl.message("Volume Buttons", name: 'triggerVolumeButtonTitle', desc: 'Volume Button trigger title'); + +String triggerVolumeButtonDescription() => Intl.message("Trigger an action by pressing the volume button", name: 'triggerVolumeButtonDescription', desc: 'Volume Button trigger description'); + +String triggerVolumeButtonVolumeUp() => Intl.message("Volume Up", name: 'triggerVolumeButtonVolumeUp', desc: 'Volume Button trigger volume up action label'); + +String triggerVolumeButtonVolumeDown() => Intl.message("Volume Down", name: 'triggerVolumeButtonVolumeDown', desc: 'Volume Button trigger volume down action label'); + +String triggerShakeTitle() => Intl.message("Shake", name: 'triggerShakeTitle', desc: 'Shake trigger title'); + +String triggerShakeDescription() => Intl.message("Trigger an action by shaking your device", name: 'triggerShakeDescription', desc: 'Shake trigger description'); + +String triggerProximityTitle() => Intl.message("Nearby Gear", name: 'triggerProximityTitle', desc: 'Proximity trigger title'); + +String triggerProximityDescription() => Intl.message("Trigger an action if gear is nearby", name: 'triggerProximityDescription', desc: 'Proximity trigger description'); + +String triggerActionNotSet() => Intl.message("No Action Set", name: 'triggerActionNotSet', desc: 'Trigger action label when no action set'); diff --git a/lib/Frontend/pages/Actions.dart b/lib/Frontend/pages/Actions.dart index a05aca3e..d51deb3f 100644 --- a/lib/Frontend/pages/Actions.dart +++ b/lib/Frontend/pages/Actions.dart @@ -3,7 +3,7 @@ import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:haptic_feedback/haptic_feedback.dart'; import 'package:multi_value_listenable_builder/multi_value_listenable_builder.dart'; -import 'package:tail_app/Frontend/intnDefs.dart'; +import 'package:tail_app/Frontend/pages/Home.dart'; import '../../Backend/ActionRegistry.dart'; import '../../Backend/Bluetooth/BluetoothManager.dart'; @@ -28,7 +28,8 @@ class ActionPageBuilder extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - if (ref.watch(knownDevicesProvider).isNotEmpty && ref.watch(knownDevicesProvider).values.where((element) => element.deviceConnectionState.value == DeviceConnectionState.connected).isNotEmpty) { + AsyncValue btStatus = ref.watch(btStatusProvider); + if (btStatus.valueOrNull != null && btStatus.valueOrNull == BleStatus.ready && ref.watch(knownDevicesProvider).isNotEmpty && ref.watch(knownDevicesProvider).values.where((element) => element.deviceConnectionState.value == DeviceConnectionState.connected).isNotEmpty) { Map> actionsCatMap = ref.watch(getAvailableActionsProvider); List catList = actionsCatMap.keys.toList(); return MultiValueListenableBuilder( @@ -81,9 +82,7 @@ class ActionPageBuilder extends ConsumerWidget { }, ); } else { - return Center( - child: Text(actionsNoGear()), - ); + return const Home(); } } } diff --git a/lib/Frontend/pages/DirectGearControl.dart b/lib/Frontend/pages/DirectGearControl.dart index ac7b1759..65d3e0b8 100644 --- a/lib/Frontend/pages/DirectGearControl.dart +++ b/lib/Frontend/pages/DirectGearControl.dart @@ -7,7 +7,7 @@ import 'package:go_router/go_router.dart'; import 'package:vector_math/vector_math.dart'; import '../../Backend/Bluetooth/BluetoothManager.dart'; -import '../../Backend/btMessage.dart'; +import '../../Backend/Bluetooth/btMessage.dart'; import '../../Backend/moveLists.dart'; import '../intnDefs.dart'; diff --git a/lib/Frontend/pages/Home.dart b/lib/Frontend/pages/Home.dart index f4d13cb8..575c3243 100644 --- a/lib/Frontend/pages/Home.dart +++ b/lib/Frontend/pages/Home.dart @@ -1,54 +1,40 @@ import 'package:flutter/material.dart'; +import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:logging_flutter/logging_flutter.dart'; +import 'package:open_settings/open_settings.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:tail_app/Frontend/intnDefs.dart'; +import 'package:url_launcher/url_launcher.dart'; -class Home extends ConsumerStatefulWidget { - const Home({super.key, required this.title}); +import '../../Backend/Bluetooth/BluetoothManager.dart'; - // This widget is the home page of your application. It is stateful, meaning - // that it has a State object (defined below) that contains fields that affect - // how it looks. - - // This class is the configuration for the state. It holds the values (in this - // case the title) provided by the parent (in this case the App widget) and - // used by the build method of the State. Fields in a Widget subclass are - // always marked "final". - - final String title; - - @override - ConsumerState createState() => _HomeState(); -} - -class _HomeState extends ConsumerState { - void changed(List changed) {} +class Home extends ConsumerWidget { + const Home({super.key}); @override - Widget build(BuildContext context) { - return ListView( - children: [ - Center( + Widget build(BuildContext context, WidgetRef ref) { + List values = [ + Center( + child: Padding( + padding: const EdgeInsets.all(8.0), child: Card( child: Column( mainAxisSize: MainAxisSize.min, children: [ - const ListTile( - leading: Icon(Icons.album), - title: Text('Welcome to Tail App'), - subtitle: Text('TODO: Set up state machine to show this page when no devices are connected, and to automatically scan for devices. Also include permission checks to make sure the device is ready to connect to gear'), + ListTile( + leading: const Icon(Icons.waving_hand), + title: Text(subTitle()), + subtitle: const Text('This is a fan made app to control The Tail Company tails, ears, and wings'), ), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( - onPressed: () {}, - child: const Text('Wan Wan'), - ), - const SizedBox(width: 8), - TextButton( - child: const Text('Awoo'), - onPressed: () { - /* ... */ + onPressed: () async { + await launchUrl(Uri.parse('https://thetailcompany.com/')); //TODO: Bug MT for referral code, at least for analytics }, + child: const Text('Tail Company Store'), ), const SizedBox(width: 8), ], @@ -57,7 +43,76 @@ class _HomeState extends ConsumerState { ), ), ), - ], + ), + ]; + AsyncValue btStatus = ref.watch(btStatusProvider); + if (btStatus.valueOrNull == null || btStatus.valueOrNull != BleStatus.ready) { + if (btStatus.valueOrNull == BleStatus.poweredOff || btStatus.valueOrNull == BleStatus.unsupported) { + values.add(Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Card( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const ListTile( + leading: Icon(Icons.bluetooth_disabled), + title: Text('Bluetooth is Unavailable'), + subtitle: Text('Bluetooth is required to connect to Gear'), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () async { + OpenSettings.openBluetoothSetting(); + }, + child: const Text('Open Settings'), + ), + const SizedBox(width: 8), + ], + ), + ], + ), + ), + ), + )); + } + if (btStatus.valueOrNull == BleStatus.unauthorized) { + values.add(Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Card( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const ListTile( + leading: Icon(Icons.bluetooth_disabled), + title: Text('Permission required'), + subtitle: Text('Permission is required to connect to nearby Gear.'), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () async { + Flogger.i("Permission BluetoothScan: ${await Permission.bluetoothScan.request()}"); + Flogger.i("Permission BluetoothConnect: ${await Permission.bluetoothConnect.request()}"); + }, + child: const Text('Grant Permissions'), + ), + const SizedBox(width: 8), + ], + ), + ], + ), + ), + ), + )); + } + } + return ListView( + children: values, ); } } diff --git a/lib/Frontend/pages/Shell.dart b/lib/Frontend/pages/Shell.dart index 1aef5535..9211f959 100644 --- a/lib/Frontend/pages/Shell.dart +++ b/lib/Frontend/pages/Shell.dart @@ -5,8 +5,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:side_sheet_material3/side_sheet_material3.dart'; +import 'package:tail_app/Frontend/Widgets/snack_bar_overlay.dart'; import 'package:upgrader/upgrader.dart'; +import '../../l10n/messages_all_locales.dart'; import '../Widgets/manage_devices.dart'; import '../intnDefs.dart'; @@ -23,7 +25,7 @@ class NavDestination { } List destinations = [ - NavDestination(actionsPage(), const Icon(Icons.widgets_outlined), const Icon(Icons.widgets), "/actions"), + NavDestination(actionsPage(), const Icon(Icons.widgets_outlined), const Icon(Icons.widgets), "/"), NavDestination(triggersPage(), const Icon(Icons.sensors_outlined), const Icon(Icons.sensors), "/triggers"), NavDestination(sequencesPage(), const Icon(Icons.list_outlined), const Icon(Icons.list), "/moveLists"), ]; @@ -32,7 +34,9 @@ class NavigationDrawerExample extends ConsumerStatefulWidget { Widget child; String location; - NavigationDrawerExample(this.child, this.location, {super.key}); + NavigationDrawerExample(this.child, this.location, {super.key}) { + initializeMessages('ace'); + } @override ConsumerState createState() => _NavigationDrawerExampleState(); @@ -72,7 +76,9 @@ class _NavigationDrawerExampleState extends ConsumerState knownDevices = ref.watch(knownDevicesProvider); await showModalSideSheet( context, header: manageDevices(), diff --git a/lib/Frontend/pages/developer/developer_menu.dart b/lib/Frontend/pages/developer/developer_menu.dart index 7ad8104e..8cd3dac8 100644 --- a/lib/Frontend/pages/developer/developer_menu.dart +++ b/lib/Frontend/pages/developer/developer_menu.dart @@ -31,6 +31,14 @@ class DeveloperMenu extends StatelessWidget { LogConsole.open(context); }, ), + ListTile( + title: const Text("Crash"), + leading: const Icon(Icons.bug_report), + subtitle: const Text("Test crash reporting"), + onTap: () { + throw Exception('Sentry Test'); + }, + ), ListTile( title: Text( "Stored JSON", diff --git a/lib/Frontend/pages/move_list.dart b/lib/Frontend/pages/move_list.dart index b2c951fb..3cabe93d 100644 --- a/lib/Frontend/pages/move_list.dart +++ b/lib/Frontend/pages/move_list.dart @@ -22,39 +22,45 @@ class _MoveListViewState extends ConsumerState { Widget build(BuildContext context) { final List allMoveLists = ref.watch(moveListsProvider); return Scaffold( - floatingActionButton: FloatingActionButton.extended( - icon: const Icon(Icons.add), - onPressed: () { - setState(() { - ref.watch(moveListsProvider.notifier).add(MoveList(sequencesPage(), DeviceType.values.toSet(), ActionCategory.sequence)); - ref.watch(moveListsProvider.notifier).store(); - }); - context.push("/moveLists/editMoveList", extra: ref.watch(moveListsProvider).last).then((value) => setState(() { - if (value != null) { - ref.watch(moveListsProvider).last = value; - ref.watch(moveListsProvider.notifier).store(); - } - })); - }, - label: Text(sequencesPage()), - ), - body: ListView.builder( - itemCount: allMoveLists.length, - primary: true, - itemBuilder: (context, index) { - return ListTile( + floatingActionButton: FloatingActionButton.extended( + icon: const Icon(Icons.add), + onPressed: () { + setState(() { + ref.watch(moveListsProvider.notifier).add(MoveList(sequencesPage(), DeviceType.values.toSet(), ActionCategory.sequence)); + ref.watch(moveListsProvider.notifier).store(); + }); + context.push("/moveLists/editMoveList", extra: ref.watch(moveListsProvider).last).then((value) => setState(() { + if (value != null) { + ref.watch(moveListsProvider).last = value; + ref.watch(moveListsProvider.notifier).store(); + } + })); + }, + label: Text(sequencesPage()), + ), + body: ListView.builder( + itemCount: allMoveLists.length, + primary: true, + itemBuilder: (context, index) { + return Hero( + tag: 'moveListEditNameTag', + child: ListTile( title: Text(allMoveLists[index].name), subtitle: Text("${allMoveLists[index].moves.length} moves"), //TODO: Localize trailing: IconButton( tooltip: sequencesEdit(), icon: const Icon(Icons.edit), onPressed: () { - context.push("/moveLists/editMoveList", extra: allMoveLists[index]).then((value) => setState(() { - if (value != null) { - allMoveLists[index] = value; - ref.watch(moveListsProvider.notifier).store(); - } - })); + context.push("/moveLists/editMoveList", extra: allMoveLists[index]).then( + (value) => setState( + () { + if (value != null) { + allMoveLists[index] = value; + ref.watch(moveListsProvider.notifier).store(); + } + }, + ), + ); }, ), onTap: () async { @@ -65,9 +71,11 @@ class _MoveListViewState extends ConsumerState { runAction(allMoveLists[index], element); }); }, - ); - }, - )); + ), + ); + }, + ), + ); } } @@ -98,6 +106,37 @@ class _EditMoveList extends ConsumerState with TickerProviderState appBar: AppBar( title: Text(sequencesEdit()), leading: IconButton(icon: const Icon(Icons.arrow_back), onPressed: () => context.pop(moveList)), + actions: [ + IconButton( + icon: const Icon(Icons.delete), + tooltip: sequencesEditDeleteTitle(), + onPressed: () { + showDialog( + context: context, + builder: (BuildContext context) => AlertDialog( + title: Text(sequencesEditDeleteTitle()), + content: Text(sequencesEditDeleteDescription()), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: Text(cancel()), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: Text(ok()), + ), + ], + ), + ).then((value) { + if (value == true) { + ref.read(moveListsProvider.notifier).remove(moveList!); + ref.read(moveListsProvider.notifier).store(); + context.pop(); + } + }); + }, + ) + ], ), floatingActionButton: FloatingActionButton.extended( icon: const Icon(Icons.add), @@ -126,20 +165,23 @@ class _EditMoveList extends ConsumerState with TickerProviderState children: [ Padding( padding: const EdgeInsets.all(16.0), - child: TextField( - controller: TextEditingController(text: moveList!.name), - decoration: InputDecoration(border: const OutlineInputBorder(), labelText: sequencesEditName()), - maxLines: 1, - maxLength: 30, - autocorrect: false, - onSubmitted: (nameValue) { - setState( - () { - moveList!.name = nameValue; - }, - ); - ref.read(moveListsProvider.notifier).store(); - }, + child: Hero( + tag: 'moveListEditNameTag', + child: TextField( + controller: TextEditingController(text: moveList!.name), + decoration: InputDecoration(border: const OutlineInputBorder(), labelText: sequencesEditName()), + maxLines: 1, + maxLength: 30, + autocorrect: false, + onSubmitted: (nameValue) { + setState( + () { + moveList!.name = nameValue; + }, + ); + ref.read(moveListsProvider.notifier).store(); + }, + ), ), ), ListTile( diff --git a/lib/Frontend/pages/settings.dart b/lib/Frontend/pages/settings.dart index 1954c1d1..bb77422c 100644 --- a/lib/Frontend/pages/settings.dart +++ b/lib/Frontend/pages/settings.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; @@ -69,14 +70,16 @@ class _SettingsState extends ConsumerState { }, ), ), - ListTile( - title: const Text("Development Menu"), - leading: const Icon(Icons.bug_report), - subtitle: const Text("It is illegal to read this message"), - onTap: () { - context.push('/settings/developer'); - }, - ), + if (kDebugMode) ...[ + ListTile( + title: const Text("Development Menu"), + leading: const Icon(Icons.bug_report), + subtitle: const Text("It is illegal to read this message"), + onTap: () { + context.push('/settings/developer'); + }, + ) + ], ], ), ); diff --git a/lib/Frontend/pages/triggers.dart b/lib/Frontend/pages/triggers.dart index 54b46649..8027680d 100644 --- a/lib/Frontend/pages/triggers.dart +++ b/lib/Frontend/pages/triggers.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:tail_app/Backend/ActionRegistry.dart'; import 'package:tail_app/Backend/Definitions/Action/BaseAction.dart'; import 'package:tail_app/Backend/Definitions/Device/BaseDeviceDefinition.dart'; import 'package:tail_app/Backend/Sensors.dart'; +import '../Widgets/action_selector.dart'; import '../intnDefs.dart'; class Triggers extends ConsumerStatefulWidget { @@ -127,28 +127,21 @@ class _TriggersState extends ConsumerState { ), ); results.addAll( - trigger.actions.map( + trigger.actions.values.map( (TriggerAction e) => ListTile( title: Text(e.name), - //TODO: Replace with window to select action/movelist. The dropdown is slow and cluttered - trailing: DropdownMenu( - initialSelection: e.action, - dropdownMenuEntries: ActionRegistry.allCommands - .map( - (BaseAction e) => DropdownMenuEntry( - label: e.name, - value: e, - leadingIcon: const Icon(Icons.moving), - ), - ) - .toList(), - onSelected: (BaseAction? value) { - setState( - () { - e.action = value; - ref.read(triggerListProvider.notifier).store(); - }, - ); + subtitle: Text(e.action?.name ?? triggerActionNotSet()), + trailing: IconButton( + icon: const Icon(Icons.edit), + onPressed: () async { + BaseAction? result = await showDialog( + context: context, + builder: (BuildContext context) { + return Dialog.fullscreen(child: ActionSelector(deviceType: trigger.deviceType)); + }); + setState(() { + e.action = result; + }); }, ), ), diff --git a/lib/l10n/messages_uwu.arb b/lib/l10n/messages_ace.arb similarity index 99% rename from lib/l10n/messages_uwu.arb rename to lib/l10n/messages_ace.arb index 6201e9a9..5d9632e9 100644 --- a/lib/l10n/messages_uwu.arb +++ b/lib/l10n/messages_ace.arb @@ -1,4 +1,5 @@ { + "@@locale": "ace", "sequencesEdit": "Edit Sequence", "@sequencesEdit": { "description": "Label for the edit icon on a sequence", diff --git a/lib/l10n/messages_en.arb b/lib/l10n/messages_en.arb index da906f77..fed5efed 100644 --- a/lib/l10n/messages_en.arb +++ b/lib/l10n/messages_en.arb @@ -1,6 +1,6 @@ { "@@locale": "en", - "@@last_modified": "2024-01-25T17:26:33.089227", + "@@last_modified": "2024-01-28T00:36:38.286072", "title": "Tail App", "@title": { "description": "The name of the app", @@ -205,6 +205,18 @@ "type": "text", "placeholders": {} }, + "sequencesEditDeleteTitle": "Delete Sequence", + "@sequencesEditDeleteTitle": { + "description": "Title of the dialog on the sequence edit page to delete the sequence", + "type": "text", + "placeholders": {} + }, + "sequencesEditDeleteDescription": "Are you sure you want to delete this sequence?", + "@sequencesEditDeleteDescription": { + "description": "Message of the dialog on the sequence edit page to delete the sequence", + "type": "text", + "placeholders": {} + }, "sequenceEditListDelayLabel": "Intl.plural(howMany, one: ' Delay next move for {howMany} second.', other: 'Delay next move for {howMany} seconds.')", "@sequenceEditListDelayLabel": { "description": "Delay move label on the edit sequences page.", @@ -221,6 +233,12 @@ "type": "text", "placeholders": {} }, + "actionsNoBluetooth": "Bluetooth is unavailable", + "@actionsNoBluetooth": { + "description": "Label on the actions page when bluetooth is unavailable", + "type": "text", + "placeholders": {} + }, "actionsCategoryCalm": "Calm and Relaxed", "@actionsCategoryCalm": { "description": "calm action group label", @@ -378,5 +396,119 @@ "description": "scan for devices scan in progress message when scanning for a device", "type": "text", "placeholders": {} + }, + "triggerWalkingTitle": "Walking", + "@triggerWalkingTitle": { + "description": "Walking/Step trigger title", + "type": "text", + "placeholders": {} + }, + "triggerWalkingDescription": "Trigger an action on walking", + "@triggerWalkingDescription": { + "description": "Walking/Step trigger description", + "type": "text", + "placeholders": {} + }, + "triggerWalkingStopped": "Stopped", + "@triggerWalkingStopped": { + "description": "Walking/Step trigger Stopped action label", + "type": "text", + "placeholders": {} + }, + "triggerWalkingEvenStep": "Even Step", + "@triggerWalkingEvenStep": { + "description": "Walking/Step trigger Even Step action label", + "type": "text", + "placeholders": {} + }, + "triggerWalkingOddStep": "Odd Step", + "@triggerWalkingOddStep": { + "description": "Walking/Step trigger Odd Step action label", + "type": "text", + "placeholders": {} + }, + "triggerWalkingStep": "Step", + "@triggerWalkingStep": { + "description": "Walking/Step trigger Step action label", + "type": "text", + "placeholders": {} + }, + "triggerCoverTitle": "Cover", + "@triggerCoverTitle": { + "description": "Cover trigger Title", + "type": "text", + "placeholders": {} + }, + "triggerCoverDescription": "Trigger an action by covering the proximity sensor", + "@triggerCoverDescription": { + "description": "Cover trigger description", + "type": "text", + "placeholders": {} + }, + "triggerCoverNear": "Near", + "@triggerCoverNear": { + "description": "Cover trigger near action label", + "type": "text", + "placeholders": {} + }, + "triggerCoverFar": "Far", + "@triggerCoverFar": { + "description": "Cover trigger far action label", + "type": "text", + "placeholders": {} + }, + "triggerVolumeButtonTitle": "Volume Buttons", + "@triggerVolumeButtonTitle": { + "description": "Volume Button trigger title", + "type": "text", + "placeholders": {} + }, + "triggerVolumeButtonDescription": "Trigger an action by pressing the volume button", + "@triggerVolumeButtonDescription": { + "description": "Volume Button trigger description", + "type": "text", + "placeholders": {} + }, + "triggerVolumeButtonVolumeUp": "Volume Up", + "@triggerVolumeButtonVolumeUp": { + "description": "Volume Button trigger volume up action label", + "type": "text", + "placeholders": {} + }, + "triggerVolumeButtonVolumeDown": "Volume Down", + "@triggerVolumeButtonVolumeDown": { + "description": "Volume Button trigger volume down action label", + "type": "text", + "placeholders": {} + }, + "triggerShakeTitle": "Shake", + "@triggerShakeTitle": { + "description": "Shake trigger title", + "type": "text", + "placeholders": {} + }, + "triggerShakeDescription": "Trigger an action by shaking your device", + "@triggerShakeDescription": { + "description": "Shake trigger description", + "type": "text", + "placeholders": {} + }, + "triggerProximityTitle": "Nearby Gear", + "@triggerProximityTitle": { + "description": "Proximity trigger title", + "type": "text", + "placeholders": {} + }, + "triggerProximityDescription": "Trigger an action if gear is nearby", + "@triggerProximityDescription": { + "description": "Proximity trigger description", + "type": "text", + "placeholders": {} + }, + "triggerActionNotSet": "No Action Set", + "@triggerActionNotSet": { + "description": "Trigger action label when no action set", + "type": "text", + "placeholders": {} } } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 4a526816..0ad4cf6d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,22 +6,31 @@ import 'package:dynamic_color/dynamic_color.dart'; import 'package:feedback_sentry/feedback_sentry.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; import 'package:logging_flutter/logging_flutter.dart'; -import 'package:permission_handler/permission_handler.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_logging/sentry_logging.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tail_app/Backend/Sensors.dart'; +import 'package:tail_app/Backend/Settings.dart'; +import 'package:tail_app/Backend/moveLists.dart'; +import 'package:tail_app/Frontend/intnDefs.dart'; import 'Backend/Bluetooth/BluetoothManager.dart'; import 'Frontend/GoRouterConfig.dart'; +import 'l10n/messages_all_locales.dart'; late SharedPreferences prefs; FutureOr beforeSend(SentryEvent event, {Hint? hint}) async { bool? reportingEnabled = prefs.getBool("AllowErrorReporting"); if (reportingEnabled == null || reportingEnabled) { + if (kDebugMode) { + print('Before sending sentry event'); + } return event; } else { return null; @@ -44,9 +53,12 @@ Future main() async { }, ); prefs = await SharedPreferences.getInstance(); + var localeLoaded = await initializeMessages('ace'); + Intl.defaultLocale = 'ace'; + Flogger.i("Loaded local: $localeLoaded"); await SentryFlutter.init( (options) { - options.dsn = 'https://2558d1aca0730fe5a59b946cd62154a6@o1187002.ingest.sentry.io/4506525602742272'; //TODO: Store as a secret + options.dsn = 'http://30dbd2cb36374c448885ee81aeae1419@192.168.50.189:8000/3'; options.addIntegration(LoggingIntegration()); options.attachScreenshot = true; options.attachViewHierarchy = true; @@ -59,7 +71,6 @@ Future main() async { options.enableAutoPerformanceTracing = true; options.beforeSend = beforeSend; }, - // Init your App. appRunner: () => runApp( DefaultAssetBundle( @@ -88,7 +99,6 @@ class TailApp extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { Flogger.i('Starting app'); - setupAsyncPermissions(); return DynamicColorBuilder( builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) { var light = ThemeData( @@ -99,12 +109,14 @@ class TailApp extends ConsumerWidget { useMaterial3: true, colorScheme: darkDynamic, ); - if (lightDynamic == null || darkDynamic == null) { + if (lightDynamic == null) { light = ThemeData( useMaterial3: true, brightness: Brightness.light, colorSchemeSeed: Colors.orange, ); + } + if (darkDynamic == null) { dark = ThemeData( useMaterial3: true, brightness: Brightness.dark, @@ -119,13 +131,14 @@ class TailApp extends ConsumerWidget { return BetterFeedback( themeMode: ThemeMode.system, darkTheme: FeedbackThemeData.dark(), - child: MaterialApp.router(title: 'All of the Tails', theme: theme, darkTheme: darkTheme, routerConfig: router, localizationsDelegates: const [ + child: MaterialApp.router(title: subTitle(), theme: theme, darkTheme: darkTheme, routerConfig: router, localizationsDelegates: const [ + AppLocalizations.delegate, // Add this line GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], supportedLocales: const [ Locale('en'), // English - Locale('es'), // Spanish + Locale('ace'), // UwU ]), ); }, @@ -133,14 +146,6 @@ class TailApp extends ConsumerWidget { }, ); } - - //Todo: make a screen to display required permissions - Future setupAsyncPermissions() async { - Flogger.i("Permission BluetoothScan: ${await Permission.bluetoothScan.request()}"); - Flogger.i("Permission BluetoothConnect: ${await Permission.bluetoothConnect.request()}"); - - //Flogger.i("Permission Location: ${await Permission.locationWhenInUse.request()}"); - } } class _EagerInitialization extends ConsumerWidget { @@ -154,7 +159,10 @@ class _EagerInitialization extends ConsumerWidget { // By using "watch", the provider will stay alive and not be disposed. ref.watch(reactiveBLEProvider); ref.watch(knownDevicesProvider); - ref.watch(btConnectStatusProvider); + ref.watch(btConnectStateHandlerProvider); + ref.watch(triggerListProvider); + ref.watch(moveListsProvider); + ref.watch(preferencesProvider); return child; } } diff --git a/pubspec.lock b/pubspec.lock index 9b8b4197..6f71f2e3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -65,6 +65,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + awesome_snackbar_content: + dependency: "direct main" + description: + name: awesome_snackbar_content + sha256: a94407ad596ac4b2f925b032c11f390cd3b3e640a9799f4983b86be0bc17f62b + url: "https://pub.dev" + source: hosted + version: "0.1.3" boolean_selector: dependency: transitive description: @@ -133,10 +141,10 @@ packages: dependency: transitive description: name: built_value - sha256: c9aabae0718ec394e5bc3c7272e6bb0dc0b32201a08fe185ec1d8401d3e39309 + sha256: a3ec2e0f967bc47f69f95009bb93db936288d61d5343b9436e378b28a2f830c6 url: "https://pub.dev" source: hosted - version: "8.8.1" + version: "8.9.0" characters: dependency: transitive description: @@ -201,14 +209,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" - color: - dependency: transitive - description: - name: color - sha256: ddcdf1b3badd7008233f5acffaf20ca9f5dc2cd0172b75f68f24526a5f5725cb - url: "https://pub.dev" - source: hosted - version: "3.0.0" convert: dependency: transitive description: @@ -253,18 +253,18 @@ packages: dependency: transitive description: name: custom_lint_builder - sha256: "8df6634b38a36a6c6cb74a9c0eb02e9ba0b0ab89b29e38e6daa86e8ed2c6288d" + sha256: badc07d7737b71e9a9f960f53463f06e09cc6ccdaa1779623015eaf9f9ee8410 url: "https://pub.dev" source: hosted - version: "0.5.8" + version: "0.5.10" custom_lint_core: dependency: transitive description: name: custom_lint_core - sha256: "2b235be098d157e244f18ea905a15a18c16a205e30553888fac6544bbf52f03f" + sha256: bfcc6b518c54d386ad0647ad7a4f415b4db3ea270d09fd9b7f4a1a2df07c3d84 url: "https://pub.dev" source: hosted - version: "0.5.8" + version: "0.5.10" dart_style: dependency: transitive description: @@ -273,14 +273,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" - dartx: - dependency: transitive - description: - name: dartx - sha256: "8b25435617027257d43e6508b5fe061012880ddfdaa75a71d607c3de2a13d244" - url: "https://pub.dev" - source: hosted - version: "1.2.0" device_info_plus: dependency: transitive description: @@ -390,22 +382,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.1" - flutter_gen: - dependency: "direct dev" - description: - name: flutter_gen - sha256: e46c44b2d612d660f913ebfa4423a5171513f4828c029a98aa847f4aecf624a4 - url: "https://pub.dev" - source: hosted - version: "5.4.0" - flutter_gen_core: - dependency: transitive - description: - name: flutter_gen_core - sha256: "3a6c3dbc1c0e260088e9c7ed1ba905436844e8c01a44799f6281edada9e45308" - url: "https://pub.dev" - source: hosted - version: "5.4.0" flutter_joystick: dependency: "direct main" description: @@ -495,10 +471,10 @@ packages: dependency: "direct main" description: name: flutter_reactive_ble - sha256: "7a0d245412dc8e1b72ce2adc423808583b42ce824b1be74001ff22c8bb5ada48" + sha256: e2184b7793f0da1bd522a6296995f2b7a2a6ca28c57419cb5f5129b4cbdccf02 url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.3.0" flutter_riverpod: dependency: "direct main" description: @@ -507,6 +483,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.9" + flutter_svg: + dependency: transitive + description: + name: flutter_svg + sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c + url: "https://pub.dev" + source: hosted + version: "2.0.9" flutter_test: dependency: "direct dev" description: flutter @@ -601,10 +585,10 @@ packages: dependency: transitive description: name: http - sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" + sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba url: "https://pub.dev" source: hosted - version: "0.13.6" + version: "1.2.0" http_multi_server: dependency: transitive description: @@ -745,10 +729,10 @@ packages: dependency: transitive description: name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" multi_value_listenable_builder: dependency: "direct main" description: @@ -757,6 +741,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.2" + open_settings: + dependency: "direct main" + description: + name: open_settings + sha256: ceb716dc476352aecb939805b6fa6a593168a5ed1abfe3caa022b6b1715e94ae + url: "https://pub.dev" + source: hosted + version: "2.0.2" os_detect: dependency: transitive description: @@ -797,6 +789,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.3" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.dev" + source: hosted + version: "1.0.1" path_provider_linux: dependency: transitive description: @@ -961,18 +961,18 @@ packages: dependency: transitive description: name: reactive_ble_mobile - sha256: e4623446d5fd6e641c984892ee1fa7c67499a2bb0971d85a500815e1d05db6fb + sha256: fd4a27cd9753fd50480a6351f7aa75703cff4b504060e4b2dbf7dafde20d03bb url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.3.0" reactive_ble_platform_interface: dependency: transitive description: name: reactive_ble_platform_interface - sha256: "8988d16497886dccc69dca1c3eebce28ae387371f3f948a4f1b03dec9954fb05" + sha256: "5a6e7e73d5c3ac778aa72f2c325ed37edfc6cb9029c66d5e2e8cc6a9c9c113ee" url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.3.0" riverpod: dependency: transitive description: @@ -1274,14 +1274,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.1" - time: - dependency: transitive - description: - name: time - sha256: ad8e018a6c9db36cb917a031853a1aae49467a93e0d464683e029537d848c221 - url: "https://pub.dev" - source: hosted - version: "2.1.4" timing: dependency: transitive description: @@ -1315,13 +1307,13 @@ packages: source: hosted version: "9.0.0" url_launcher: - dependency: transitive + dependency: "direct main" description: name: url_launcher - sha256: d25bb0ca00432a5e1ee40e69c36c85863addf7cc45e433769d61bed3fe81fd96 + sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c url: "https://pub.dev" source: hosted - version: "6.2.3" + version: "6.2.4" url_launcher_android: dependency: transitive description: @@ -1386,6 +1378,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.3.3" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "18f6690295af52d081f6808f2f7c69f0eed6d7e23a71539d75f4aeb8f0062172" + url: "https://pub.dev" + source: hosted + version: "1.1.9+2" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "531d20465c10dfac7f5cd90b60bbe4dd9921f1ec4ca54c83ebb176dbacb7bb2d" + url: "https://pub.dev" + source: hosted + version: "1.1.9+2" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "03012b0a33775c5530576b70240308080e1d5050f0faf000118c20e6463bc0ad" + url: "https://pub.dev" + source: hosted + version: "1.1.9+2" vector_math: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index c8784feb..0e9164bc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ dependencies: flutter_riverpod: ^2.4.9 logging: ^1.2.0 logging_flutter: ^3.0.0 - flutter_reactive_ble: ^5.2.0 + flutter_reactive_ble: ^5.3.0 shared_preferences: ^2.2.2 go_router: ^13.0.1 double_back_to_close_app: ^2.1.0 @@ -42,7 +42,10 @@ dependencies: multi_value_listenable_builder: ^0.0.2 feedback_sentry: ^3.0.0 flutter_android_volume_keydown: ^1.0.0 - intl: + intl: #pinned to flutter version + url_launcher: ^6.2.4 + open_settings: ^2.0.2 + awesome_snackbar_content: ^0.1.3 dev_dependencies: build_runner: flutter_test: @@ -51,7 +54,6 @@ dev_dependencies: custom_lint: riverpod_lint: riverpod_generator: - flutter_gen: ^5.4.0 flutter_native_splash: ^2.3.9 json_serializable: ^6.7.1 json_annotation: ^4.8.1