diff --git a/lib/Frontend/pages/action_selector.dart b/lib/Frontend/pages/action_selector.dart index 4b9246e4..14c17265 100644 --- a/lib/Frontend/pages/action_selector.dart +++ b/lib/Frontend/pages/action_selector.dart @@ -1,6 +1,9 @@ +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'package:tail_app/Backend/Bluetooth/bluetooth_manager.dart'; +import 'package:tail_app/Frontend/Widgets/tutorial_card.dart'; import 'package:tail_app/Frontend/utils.dart'; import 'package:tail_app/constants.dart'; @@ -40,73 +43,105 @@ class _ActionSelectorState extends ConsumerState { @override Widget build(BuildContext context) { + Set knownDeviceTypes = ref + .watch(knownDevicesProvider) + .values + .map( + (e) => e.baseDeviceDefinition.deviceType, + ) + .toSet(); return Scaffold( - primary: true, - appBar: AppBar( - title: Text(actionsSelectScreen()), - actions: [ - IconButton( - onPressed: () { - if (selected.isEmpty) { - context.pop(true); - } else { - context.pop(selected); - } - }, - icon: const Icon(Icons.save), - tooltip: triggersSelectSaveLabel(), - ), - IconButton( - onPressed: () { - setState(() { - selected.clear(); - }); - }, - icon: const Icon(Icons.clear), - tooltip: triggersSelectClearLabel(), - ) - ], - ), - 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.titleLarge, - ), - ), - GridView.builder( - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: 125), - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: actionsForCat.length, - itemBuilder: (BuildContext context, int actionIndex) { - BaseAction baseAction = actionsForCat[actionIndex]; - bool isSelected = selected.contains(baseAction); - return TweenAnimationBuilder( - builder: (context, value, child) { - Color? color = Color.lerp(Theme.of(context).cardColor, Theme.of(context).colorScheme.primary, value); - return Card( - clipBehavior: Clip.antiAlias, - elevation: 2, - color: color, - child: cardChild(isSelected, baseAction, color!), - ); - }, - tween: isSelected ? Tween(begin: 0, end: 1) : Tween(begin: 1, end: 0), - duration: animationTransitionDuration, - ); - }) - ], - ); - }, - ), - ); + appBar: AppBar( + title: Text(actionsSelectScreen()), + actions: [ + IconButton( + onPressed: () { + setState(() { + selected = actionsCatMap.values.flattened.toList(); + }); + }, + icon: const Icon(Icons.select_all), + tooltip: triggersSelectAllLabel(), + ), + IconButton( + onPressed: () { + setState(() { + selected.clear(); + }); + }, + icon: const Icon(Icons.deselect), + tooltip: triggersSelectClearLabel(), + ) + ], + ), + extendBody: true, + bottomNavigationBar: ButtonBar( + alignment: MainAxisAlignment.center, + children: [ + FilledButton( + onPressed: () { + setState(() { + if (selected.isEmpty) { + context.pop(true); + } else { + context.pop(selected); + } + }); + }, + child: Text(triggersSelectSaveLabel()), + ) + ], + ), + body: ListView( + primary: true, + children: [ + PageInfoCard(text: triggerActionSelectorTutorialLabel()), + ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: catList.length, + itemBuilder: (BuildContext context, int categoryIndex) { + List actionsForCat = actionsCatMap.values.toList()[categoryIndex].toList(); + bool hasConnectedDevice = actionsForCat.map((e) => e.deviceCategory).flattened.toSet().intersection(knownDeviceTypes).isNotEmpty; + return Theme( + data: Theme.of(context).copyWith(dividerColor: Colors.transparent), + child: ExpansionTile( + initiallyExpanded: hasConnectedDevice, + title: Text( + catList[categoryIndex].friendly, + style: Theme.of(context).textTheme.titleLarge, + ), + children: [ + GridView.builder( + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(maxCrossAxisExtent: 125), + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: actionsForCat.length, + itemBuilder: (BuildContext context, int actionIndex) { + BaseAction baseAction = actionsForCat[actionIndex]; + bool isSelected = selected.contains(baseAction); + return TweenAnimationBuilder( + builder: (context, value, child) { + Color? color = Color.lerp(Theme.of(context).colorScheme.primary, Theme.of(context).cardColor, value); + return Card( + clipBehavior: Clip.antiAlias, + elevation: 2, + color: color, + child: cardChild(isSelected, baseAction, color!), + ); + }, + tween: isSelected ? Tween(begin: 1, end: 0) : Tween(begin: 0, end: 1), + duration: animationTransitionDuration, + ); + }) + ], + ), + ); + }, + ), + ], + )); } InkWell cardChild(bool isSelected, BaseAction baseAction, Color color) { diff --git a/lib/Frontend/pages/triggers.dart b/lib/Frontend/pages/triggers.dart index b8b9cca0..f79c862e 100644 --- a/lib/Frontend/pages/triggers.dart +++ b/lib/Frontend/pages/triggers.dart @@ -1,8 +1,10 @@ import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:choice/choice.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:multi_value_listenable_builder/multi_value_listenable_builder.dart'; +import 'package:tail_app/Backend/Bluetooth/bluetooth_manager.dart'; import 'package:tail_app/Backend/Definitions/Action/base_action.dart'; import 'package:tail_app/Backend/Definitions/Device/device_definition.dart'; import 'package:tail_app/Backend/sensors.dart'; @@ -267,9 +269,16 @@ class _TriggerEditState extends ConsumerState { ), firstChild: Builder(builder: (context) { String text = ""; + Iterable knownDevices = ref.read(knownDevicesProvider).values; for (String actionUUID in e.actions) { BaseAction? baseAction = ref.watch(getActionFromUUIDProvider(actionUUID)); - if (baseAction != null) { + if (baseAction != null && + (knownDevices.isEmpty || + knownDevices + .where( + (element) => baseAction.deviceCategory.contains(element.baseDeviceDefinition.deviceType), + ) + .isNotEmpty)) { if (text.isNotEmpty) { text += ', '; } @@ -294,7 +303,14 @@ class _TriggerEditState extends ConsumerState { return Dialog.fullscreen( backgroundColor: Theme.of(context).canvasColor, child: ActionSelector( - actionSelectorInfo: ActionSelectorInfo(deviceType: widget.trigger.deviceType.toSet(), selectedActions: []), + actionSelectorInfo: ActionSelectorInfo( + deviceType: widget.trigger.deviceType.toSet(), + selectedActions: e.actions + .map( + (e) => ref.read(getActionFromUUIDProvider(e)), + ) + .whereNotNull() + .toList()), )); }, ); diff --git a/lib/Frontend/translation_string_definitions.dart b/lib/Frontend/translation_string_definitions.dart index a29e44cf..37967c54 100644 --- a/lib/Frontend/translation_string_definitions.dart +++ b/lib/Frontend/translation_string_definitions.dart @@ -42,7 +42,9 @@ String sequencesPageDescription() => Intl.message('Create custom Actions for you // Triggers Page String triggersSelectLabel() => Intl.message('Select a Trigger Type', name: 'triggersSelectLabel', desc: 'The title of the add trigger dialog'); -String triggersSelectClearLabel() => Intl.message('Remove Actions', name: 'triggersSelectClearLabel', desc: 'The button label on the trigger select screen for clearing the selected actions'); +String triggersSelectClearLabel() => Intl.message('Select None', name: 'triggersSelectClearLabel', desc: 'The button label on the trigger select screen for clearing the selected actions'); + +String triggersSelectAllLabel() => Intl.message('Select All', name: 'triggersSelectAllLabel', desc: 'The button label on the trigger select screen for clearing the selected actions'); String triggersSelectSaveLabel() => Intl.message('Save Actions', name: 'triggersSelectSaveLabel', desc: 'The button label on the trigger select screen for saving the selected actions'); @@ -331,3 +333,6 @@ String scanRemoveDemoGear() => Intl.message("Remove all fake gear", name: 'scanR String scanDemoGearTip() => Intl.message("Want to try out the app but are waiting for your gear to arrive? Add a fake gear. This lets you experience the app as if you had your gear, or if you want to try out gear you currently do not own. This enables a new section on the 'Scan For New Gear' page.", name: 'scanDemoGearTip', desc: 'Tip Card description for the demo gear on the scan for new devices page'); + +String triggerActionSelectorTutorialLabel() => Intl.message("Select as many actions as you want. An action will be randomly selected that is compatible with connected gear. GlowTip and Sound actions will trigger alongside Move actions.", + name: 'triggerActionSelectorTutorialLabel', desc: 'Label for the tutorial card on the Action selector for triggers');