diff --git a/lib/constants.dart b/lib/constants.dart index 260416434..a25691e6a 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -232,7 +232,7 @@ String degreeSymbol = '°'; String enterAngleRange = 'Enter angle (0 - 360)'; String errorCannotBeEmpty = 'Cannot be empty'; String servoValidNumberRange = 'Please enter a valid number between 0 and 360'; -String ok = 'Ok'; +String ok = 'OK'; String roboticArm = 'Robotic Arm'; String play = 'Play'; String pause = 'Pause'; @@ -368,6 +368,24 @@ String shareAppMenu = 'Share App'; String privacyPolicyMenu = 'Privacy Policy'; String shopLink = 'https://pslab.io/shop/'; String shopError = 'Could not open the shop link'; +String showLuxmeterConfig = 'Lux Meter Configurations'; +String luxmeterConfigurations = 'Lux Meter Configurations'; +String updatePeriod = 'Update Period'; +String updatePeriodHint = + 'Please provide time interval at which data will be updated (100 ms to 1000 ms)'; +String highLimit = 'High Limit'; +String highLimitHint = + 'Please provide the maximum limit of lux value to be recorded (10 Lx to 10000 Lx)'; +String sensorGain = 'Sensor Gain'; +String sensorGainHint = 'Please set gain of the sensor'; +String locationData = 'Include Location Data'; +String locationDataHint = 'Include the location data in the logged file'; +String activeSensor = 'Active Sensor'; +String ms = 'ms'; +String inBuiltSensor = 'In-built Sensor'; +String updatePeriodErrorMessage = + 'Entered update period is not within the limits!'; +String highLimitErrorMessage = 'Entered High limit is not within the limits!'; String baroMeterBulletPoint1 = 'The Barometer can be used to measure Atmospheric pressure. This instrument is compatible with either the built in pressure sensor on any android device or the BMP-180 pressure sensor'; String baroMeterBulletPoint2 = diff --git a/lib/models/luxmeter_config.dart b/lib/models/luxmeter_config.dart new file mode 100644 index 000000000..d00292344 --- /dev/null +++ b/lib/models/luxmeter_config.dart @@ -0,0 +1,51 @@ +class LuxMeterConfig { + final int updatePeriod; + final int highLimit; + final String activeSensor; + final int sensorGain; + final bool includeLocationData; + + const LuxMeterConfig({ + this.updatePeriod = 1000, + this.highLimit = 2000, + this.activeSensor = 'In-built Sensor', + this.sensorGain = 1, + this.includeLocationData = true, + }); + + LuxMeterConfig copyWith({ + int? updatePeriod, + int? highLimit, + String? activeSensor, + int? sensorGain, + bool? includeLocationData, + }) { + return LuxMeterConfig( + updatePeriod: updatePeriod ?? this.updatePeriod, + highLimit: highLimit ?? this.highLimit, + activeSensor: activeSensor ?? this.activeSensor, + sensorGain: sensorGain ?? this.sensorGain, + includeLocationData: includeLocationData ?? this.includeLocationData, + ); + } + + Map toJson() { + return { + 'updatePeriod': updatePeriod, + 'highLimit': highLimit, + 'activeSensor': activeSensor, + 'sensorGain': sensorGain, + 'includeLocationData': includeLocationData, + }; + } + + factory LuxMeterConfig.fromJson(Map json) { + return LuxMeterConfig( + updatePeriod: json['updatePeriod'] ?? 1000, + highLimit: json['highLimit'] ?? 2000, + activeSensor: json['activeSensor'] ?? 'In-built Sensor', + sensorGain: json['sensorGain'] ?? 1, + includeLocationData: json['includeLocationData'] ?? true, + ); + } +} diff --git a/lib/providers/luxmeter_config_provider.dart b/lib/providers/luxmeter_config_provider.dart new file mode 100644 index 000000000..621c82cef --- /dev/null +++ b/lib/providers/luxmeter_config_provider.dart @@ -0,0 +1,71 @@ +import 'package:flutter/foundation.dart'; +import 'dart:convert'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:pslab/models/luxmeter_config.dart'; + +class LuxMeterConfigProvider extends ChangeNotifier { + LuxMeterConfig _config = const LuxMeterConfig(); + + LuxMeterConfig get config => _config; + + LuxMeterConfigProvider() { + _loadConfigFromPrefs(); + } + + Future _loadConfigFromPrefs() async { + final prefs = await SharedPreferences.getInstance(); + final jsonString = prefs.getString('lux_config'); + if (jsonString != null) { + final Map jsonMap = json.decode(jsonString); + _config = LuxMeterConfig.fromJson(jsonMap); + notifyListeners(); + } + } + + Future _saveConfigToPrefs() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('lux_config', json.encode(_config.toJson())); + } + + void updateConfig(LuxMeterConfig newConfig) { + _config = newConfig; + notifyListeners(); + _saveConfigToPrefs(); + } + + void updateUpdatePeriod(int updatePeriod) { + _config = _config.copyWith(updatePeriod: updatePeriod); + notifyListeners(); + _saveConfigToPrefs(); + } + + void updateHighLimit(int highLimit) { + _config = _config.copyWith(highLimit: highLimit); + notifyListeners(); + _saveConfigToPrefs(); + } + + void updateActiveSensor(String activeSensor) { + _config = _config.copyWith(activeSensor: activeSensor); + notifyListeners(); + _saveConfigToPrefs(); + } + + void updateSensorGain(int sensorGain) { + _config = _config.copyWith(sensorGain: sensorGain); + notifyListeners(); + _saveConfigToPrefs(); + } + + void updateIncludeLocationData(bool includeLocationData) { + _config = _config.copyWith(includeLocationData: includeLocationData); + notifyListeners(); + _saveConfigToPrefs(); + } + + void resetToDefaults() { + _config = const LuxMeterConfig(); + notifyListeners(); + _saveConfigToPrefs(); + } +} diff --git a/lib/providers/luxmeter_state_provider.dart b/lib/providers/luxmeter_state_provider.dart index 1788d6e89..b43d20f87 100644 --- a/lib/providers/luxmeter_state_provider.dart +++ b/lib/providers/luxmeter_state_provider.dart @@ -5,6 +5,7 @@ import 'package:pslab/others/logger_service.dart'; import 'package:light/light.dart'; import 'package:flutter/foundation.dart'; import 'package:pslab/constants.dart'; +import 'package:pslab/providers/luxmeter_config_provider.dart'; class LuxMeterStateProvider extends ChangeNotifier { double _currentLux = 0.0; @@ -23,8 +24,23 @@ class LuxMeterStateProvider extends ChangeNotifier { int _dataCount = 0; bool _sensorAvailable = false; + LuxMeterConfigProvider? _configProvider; + Function(String)? onSensorError; + void setConfigProvider(LuxMeterConfigProvider configProvider) { + _configProvider = configProvider; + _configProvider?.addListener(_onConfigChanged); + } + + void _onConfigChanged() { + if (_configProvider != null) { + // TODO + } + } + + LuxMeterConfigProvider? get configProvider => _configProvider; + void initializeSensors({Function(String)? onError}) { onSensorError = onError; @@ -79,6 +95,7 @@ class LuxMeterStateProvider extends ChangeNotifier { @override void dispose() { + _configProvider?.removeListener(_onConfigChanged); disposeSensors(); super.dispose(); } diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart index 7426db62d..0a399583a 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -45,6 +45,7 @@ Color snackBarContentColor = Colors.white; Color guideDrawerBackgroundColor = Colors.white; Color guideDrawerHeadingColor = Colors.black87; Color guideDrawerHighlightColor = Colors.black54; +Color hintTextColor = Colors.grey; List knobLabelColors = [ Color(0xFFD32F2F), // CH1 Color(0xFFD32F2F), // CAP diff --git a/lib/view/luxmeter_config_screen.dart b/lib/view/luxmeter_config_screen.dart new file mode 100644 index 000000000..5329a43dd --- /dev/null +++ b/lib/view/luxmeter_config_screen.dart @@ -0,0 +1,176 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; +import 'package:pslab/constants.dart'; +import 'package:pslab/providers/luxmeter_config_provider.dart'; +import 'package:pslab/view/widgets/config_widgets.dart'; + +import '../theme/colors.dart'; + +class LuxMeterConfigScreen extends StatefulWidget { + const LuxMeterConfigScreen({super.key}); + + @override + State createState() => _LuxMeterConfigScreenState(); +} + +class _LuxMeterConfigScreenState extends State { + final TextEditingController _updatePeriodController = TextEditingController(); + final TextEditingController _highLimitController = TextEditingController(); + final TextEditingController _sensorGainController = TextEditingController(); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + final provider = + Provider.of(context, listen: false); + _updatePeriodController.text = provider.config.updatePeriod.toString(); + _highLimitController.text = provider.config.highLimit.toString(); + _sensorGainController.text = provider.config.sensorGain.toString(); + }); + } + + @override + void dispose() { + _updatePeriodController.dispose(); + _highLimitController.dispose(); + _sensorGainController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.surface, + resizeToAvoidBottomInset: true, + appBar: AppBar( + systemOverlayStyle: SystemUiOverlayStyle(statusBarColor: appBarColor), + leading: Builder(builder: (context) { + return IconButton( + onPressed: () { + if (Navigator.canPop(context) && + ModalRoute.of(context)?.settings.name == '/luxmeter') { + Navigator.popUntil(context, ModalRoute.withName('/luxmeter')); + } else { + Navigator.pushNamedAndRemoveUntil( + context, + '/luxmeter', + (route) => route.isFirst, + ); + } + }, + icon: Icon( + Icons.arrow_back, + color: appBarContentColor, + ), + ); + }), + backgroundColor: primaryRed, + title: Text( + luxmeterConfigurations, + style: TextStyle( + color: appBarContentColor, + fontSize: 15, + ), + ), + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Consumer( + builder: (context, provider, child) { + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ConfigInputItem( + title: updatePeriod, + value: '${provider.config.updatePeriod} $ms', + controller: _updatePeriodController, + onChanged: (value) { + final intValue = int.tryParse(value); + if (intValue != null && + intValue >= 100 && + intValue <= 1000) { + provider.updateUpdatePeriod(intValue); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + updatePeriodErrorMessage, + style: TextStyle(color: snackBarContentColor), + ), + backgroundColor: snackBarBackgroundColor), + ); + } + }, + hint: updatePeriodHint, + ), + ConfigInputItem( + title: highLimit, + value: '${provider.config.highLimit} $lx', + controller: _highLimitController, + onChanged: (value) { + final intValue = int.tryParse(value); + if (intValue != null && + intValue >= 10 && + intValue <= 10000) { + provider.updateHighLimit(intValue); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + highLimitErrorMessage, + style: TextStyle(color: snackBarContentColor), + ), + backgroundColor: snackBarBackgroundColor), + ); + } + }, + hint: highLimitHint, + ), + ConfigDropdownItem( + title: activeSensor, + selectedValue: provider.config.activeSensor, + options: [ + ConfigOption( + value: 'In-built Sensor', + displayName: inBuiltSensor), + ConfigOption(value: 'BH1750', displayName: 'BH1750'), + ConfigOption(value: 'TSL2561', displayName: 'TSL2561'), + ], + onChanged: (value) { + provider.updateActiveSensor(value); + }, + ), + ConfigInputItem( + title: sensorGain, + value: provider.config.sensorGain.toString(), + controller: _sensorGainController, + onChanged: (value) { + final intValue = int.tryParse(value); + if (intValue != null) { + provider.updateSensorGain(intValue); + } + }, + hint: sensorGainHint, + ), + ConfigCheckboxItem( + title: locationData, + subtitle: locationDataHint, + value: provider.config.includeLocationData, + onChanged: (value) { + provider.updateIncludeLocationData(value); + }, + ), + ], + ), + ); + }, + ), + ), + ), + ); + } +} diff --git a/lib/view/luxmeter_screen.dart b/lib/view/luxmeter_screen.dart index d534f2d07..d035313c5 100644 --- a/lib/view/luxmeter_screen.dart +++ b/lib/view/luxmeter_screen.dart @@ -2,10 +2,12 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:pslab/constants.dart'; import 'package:pslab/providers/luxmeter_state_provider.dart'; +import 'package:pslab/providers/luxmeter_config_provider.dart'; import 'package:pslab/view/widgets/common_scaffold_widget.dart'; import 'package:pslab/view/widgets/guide_widget.dart'; import 'package:pslab/view/widgets/luxmeter_card.dart'; import 'package:fl_chart/fl_chart.dart'; +import 'package:pslab/view/luxmeter_config_screen.dart'; import '../theme/colors.dart'; @@ -17,6 +19,7 @@ class LuxMeterScreen extends StatefulWidget { class _LuxMeterScreenState extends State { late LuxMeterStateProvider _provider; + late LuxMeterConfigProvider _configProvider; bool _showGuide = false; static const imagePath = 'assets/images/bh1750_schematic.png'; void _showInstrumentGuide() { @@ -49,12 +52,61 @@ class _LuxMeterScreenState extends State { ]; } + void _showOptionsMenu() { + showMenu( + context: context, + position: RelativeRect.fromLTRB( + MediaQuery.of(context).size.width, + 0, + 0, + MediaQuery.of(context).size.height, + ), + items: [ + PopupMenuItem( + value: 'show_logged_data', + child: Text(showLoggedData), + ), + PopupMenuItem( + value: 'lux_meter_config', + child: Text(showLuxmeterConfig), + ), + ], + elevation: 8, + ).then((value) { + if (value != null) { + switch (value) { + case 'show_logged_data': + // TODO + break; + case 'lux_meter_config': + _navigateToConfig(); + break; + } + } + }); + } + + void _navigateToConfig() { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + ChangeNotifierProvider.value( + value: _configProvider, + child: const LuxMeterConfigScreen(), + ), + ), + ); + } + @override void initState() { super.initState(); _provider = LuxMeterStateProvider(); + _configProvider = LuxMeterConfigProvider(); WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { + _provider.setConfigProvider(_configProvider); _provider.initializeSensors(onError: _showSensorErrorSnackbar); } }); @@ -64,6 +116,7 @@ class _LuxMeterScreenState extends State { void dispose() { _provider.disposeSensors(); _provider.dispose(); + _configProvider.dispose(); super.dispose(); } @@ -85,11 +138,16 @@ class _LuxMeterScreenState extends State { @override Widget build(BuildContext context) { - return ChangeNotifierProvider.value( - value: _provider, + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: _provider), + ChangeNotifierProvider.value( + value: _configProvider), + ], child: Stack(children: [ CommonScaffold( title: luxMeterTitle, + onOptionsPressed: _showOptionsMenu, onGuidePressed: _showInstrumentGuide, body: SafeArea(child: LayoutBuilder(builder: (context, constraints) { final isLargeScreen = constraints.maxWidth > 900; diff --git a/lib/view/widgets/common_scaffold_widget.dart b/lib/view/widgets/common_scaffold_widget.dart index 15494d13e..a5181941c 100644 --- a/lib/view/widgets/common_scaffold_widget.dart +++ b/lib/view/widgets/common_scaffold_widget.dart @@ -9,14 +9,15 @@ class CommonScaffold extends StatefulWidget { final Key? scaffoldKey; final List? actions; final VoidCallback? onGuidePressed; - const CommonScaffold({ - super.key, - required this.body, - required this.title, - this.scaffoldKey, - this.actions, - this.onGuidePressed, - }); + final VoidCallback? onOptionsPressed; + const CommonScaffold( + {super.key, + required this.body, + required this.title, + this.scaffoldKey, + this.actions, + this.onGuidePressed, + this.onOptionsPressed}); @override State createState() => _CommonScaffoldState(); } @@ -62,9 +63,17 @@ class _CommonScaffoldState extends State { if (widget.onGuidePressed != null) IconButton( onPressed: widget.onGuidePressed, - icon: const Icon( + icon: Icon( Icons.info, - color: Colors.white, + color: appBarContentColor, + ), + ), + if (widget.onOptionsPressed != null) + IconButton( + onPressed: widget.onOptionsPressed, + icon: Icon( + Icons.more_vert, + color: appBarContentColor, ), ), if (widget.actions != null) ...widget.actions!, diff --git a/lib/view/widgets/config_widgets.dart b/lib/view/widgets/config_widgets.dart new file mode 100644 index 000000000..29fc0f59e --- /dev/null +++ b/lib/view/widgets/config_widgets.dart @@ -0,0 +1,242 @@ +import 'package:flutter/material.dart'; +import 'package:pslab/constants.dart'; +import 'package:pslab/theme/colors.dart'; + +class ConfigInputItem extends StatelessWidget { + final String title; + final String value; + final TextEditingController controller; + final Function(String) onChanged; + final String? hint; + + const ConfigInputItem({ + super.key, + required this.title, + required this.value, + required this.controller, + required this.onChanged, + this.hint, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text( + title, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + subtitle: Text( + value, + style: TextStyle( + fontSize: 14, + color: hintTextColor, + ), + ), + onTap: () => + _showInputDialog(context, title, controller, onChanged, hint), + contentPadding: EdgeInsets.zero, + ); + } + + void _showInputDialog( + BuildContext context, + String title, + TextEditingController controller, + Function(String) onChanged, + String? hint) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text(title), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (hint != null) + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: Text( + hint, + style: TextStyle( + fontSize: 14, + color: hintTextColor, + ), + ), + ), + TextField( + controller: controller, + keyboardType: TextInputType.numberWithOptions(decimal: false), + decoration: InputDecoration( + border: const UnderlineInputBorder(), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: primaryRed), + ), + ), + autofocus: true, + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text( + cancel, + style: TextStyle(color: primaryRed), + ), + ), + TextButton( + onPressed: () { + onChanged(controller.text); + Navigator.of(context).pop(); + }, + child: Text( + ok, + style: TextStyle(color: primaryRed), + ), + ), + ], + ); + }, + ); + } +} + +class ConfigDropdownItem extends StatelessWidget { + final String title; + final String selectedValue; + final List options; + final Function(String) onChanged; + + const ConfigDropdownItem({ + super.key, + required this.title, + required this.selectedValue, + required this.options, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text( + title, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + subtitle: Text( + selectedValue, + style: TextStyle( + fontSize: 14, + color: hintTextColor, + ), + ), + onTap: () => _showDropdownDialog(context), + contentPadding: EdgeInsets.zero, + ); + } + + void _showDropdownDialog(BuildContext context) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text(title), + content: Column( + mainAxisSize: MainAxisSize.min, + children: options.map((option) { + return RadioListTile( + title: Text(option.displayName), + value: option.value, + groupValue: selectedValue, + onChanged: (String? value) { + if (value != null) { + onChanged(value); + Navigator.of(context).pop(); + } + }, + activeColor: primaryRed, + contentPadding: EdgeInsets.zero, + ); + }).toList(), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text( + cancel, + style: TextStyle(color: primaryRed), + ), + ), + ], + ); + }, + ); + } +} + +class ConfigCheckboxItem extends StatelessWidget { + final String title; + final String subtitle; + final bool value; + final Function(bool) onChanged; + + const ConfigCheckboxItem({ + super.key, + required this.title, + required this.subtitle, + required this.value, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text( + title, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + subtitle: Text( + subtitle, + style: TextStyle( + fontSize: 14, + color: hintTextColor, + ), + ), + trailing: Checkbox( + value: value, + onChanged: (bool? newValue) { + if (newValue != null) { + onChanged(newValue); + } + }, + activeColor: checkBoxActiveColor, + ), + onTap: () { + onChanged(!value); + }, + contentPadding: EdgeInsets.zero, + ); + } +} + +class ConfigOption { + final String value; + final String displayName; + + const ConfigOption({ + required this.value, + required this.displayName, + }); +} diff --git a/pubspec.yaml b/pubspec.yaml index 55d0459f4..2e5b7b3cf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,6 +60,7 @@ dependencies: light: ^4.1.0 connectivity_plus: ^6.1.4 vibration: ^3.1.3 + shared_preferences: ^2.5.3 dev_dependencies: