From f6df6302c52f428b753dcc64d1afc61612991cc2 Mon Sep 17 00:00:00 2001 From: habibialireza Date: Wed, 17 Jun 2026 11:39:02 +0200 Subject: [PATCH] microphone gain control added to app --- .../device_detail/device_detail_page.dart | 9 + .../microphone_gain_controls.dart | 352 ++++++++++++++++++ .../lib/widgets/devices/devices_page.dart | 17 + .../sensor_configuration_detail_view.dart | 42 +-- .../sensor_configuration_device_row.dart | 74 ++-- .../sensor_configuration_value_row.dart | 57 ++- open_wearable/pubspec.lock | 7 +- open_wearable/pubspec.yaml | 5 +- 8 files changed, 455 insertions(+), 108 deletions(-) create mode 100644 open_wearable/lib/widgets/devices/device_detail/microphone_gain_controls.dart diff --git a/open_wearable/lib/widgets/devices/device_detail/device_detail_page.dart b/open_wearable/lib/widgets/devices/device_detail/device_detail_page.dart index 88ee4fd2..e5c2bee2 100644 --- a/open_wearable/lib/widgets/devices/device_detail/device_detail_page.dart +++ b/open_wearable/lib/widgets/devices/device_detail/device_detail_page.dart @@ -14,6 +14,7 @@ import 'package:open_wearable/widgets/app_toast.dart'; import 'package:open_wearable/widgets/common/app_section_card.dart'; import 'package:open_wearable/widgets/devices/device_detail/audio_mode_widget.dart'; import 'package:open_wearable/widgets/devices/device_detail/device_detail_shared_widgets.dart'; +import 'package:open_wearable/widgets/devices/device_detail/microphone_gain_controls.dart'; import 'package:open_wearable/widgets/devices/device_detail/power_saving_mode_widget.dart'; import 'package:open_wearable/widgets/devices/device_status_pills.dart'; import 'package:open_wearable/widgets/devices/wearable_icon.dart'; @@ -242,6 +243,14 @@ class _DeviceDetailPageState extends State { ), ), ), + if (widget.device.hasCapability()) + Card( + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: MicrophoneGainControls(device: widget.device), + ), + ), if (widget.device.hasCapability()) Card( margin: EdgeInsets.zero, diff --git a/open_wearable/lib/widgets/devices/device_detail/microphone_gain_controls.dart b/open_wearable/lib/widgets/devices/device_detail/microphone_gain_controls.dart new file mode 100644 index 00000000..5de6c424 --- /dev/null +++ b/open_wearable/lib/widgets/devices/device_detail/microphone_gain_controls.dart @@ -0,0 +1,352 @@ +import 'package:flutter/material.dart'; +import 'package:open_earable_flutter/open_earable_flutter.dart'; + +class MicrophoneGainControls extends StatefulWidget { + final Wearable? device; + final Wearable? pairedDevice; + + const MicrophoneGainControls({ + super.key, + required this.device, + this.pairedDevice, + }); + + @override + State createState() => _MicrophoneGainControlsState(); +} + +class _MicrophoneGainControlsState extends State { + int _leftRegister = MicrophoneGain.defaultRegister; + int _rightRegister = MicrophoneGain.defaultRegister; + int _lastLeftRegister = MicrophoneGain.defaultRegister; + int _lastRightRegister = MicrophoneGain.defaultRegister; + bool _linked = true; + bool _muted = false; + bool _loading = true; + bool _writing = false; + String? _error; + + MicrophoneGainManager? get _manager => + widget.device?.getCapability(); + + MicrophoneGainManager? get _pairedManager => + widget.pairedDevice?.getCapability(); + + bool get _hasPairedTarget => + widget.pairedDevice != null && + widget.pairedDevice?.deviceId != widget.device?.deviceId && + _pairedManager != null; + + @override + void initState() { + super.initState(); + _readGain(); + } + + @override + void didUpdateWidget(covariant MicrophoneGainControls oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.device?.deviceId != widget.device?.deviceId || + oldWidget.pairedDevice?.deviceId != widget.pairedDevice?.deviceId) { + _readGain(); + } + } + + @override + Widget build(BuildContext context) { + final manager = _manager; + if (manager == null) { + return const SizedBox.shrink(); + } + + final colorScheme = Theme.of(context).colorScheme; + final disabled = _loading || _writing || _muted; + + return Container( + width: double.infinity, + padding: const EdgeInsets.fromLTRB(10, 10, 10, 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: colorScheme.outlineVariant.withValues(alpha: 0.45), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Expanded( + child: Text( + 'Microphone Gain', + style: Theme.of( + context, + ).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w700), + ), + ), + IconButton( + tooltip: 'Refresh', + onPressed: _loading || _writing ? null : _readGain, + icon: const Icon(Icons.refresh_rounded, size: 18), + ), + ], + ), + const SizedBox(height: 2), + Row( + children: [ + Checkbox.adaptive( + value: _linked, + onChanged: _loading || _writing + ? null + : (value) => _setLinked(value ?? false), + ), + Expanded( + child: Text( + 'Link channels', + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w600), + ), + ), + FilledButton.tonalIcon( + onPressed: _loading || _writing ? null : _toggleMute, + icon: Icon( + _muted ? Icons.volume_up_rounded : Icons.volume_off_rounded, + size: 18, + ), + label: Text(_muted ? 'Unmute' : 'Mute'), + ), + ], + ), + if (_loading) ...[ + const SizedBox(height: 8), + const LinearProgressIndicator(minHeight: 2), + ] else ...[ + const SizedBox(height: 8), + _GainSlider( + label: 'Left', + register: _leftRegister, + fallbackRegister: _lastLeftRegister, + enabled: !disabled, + onChanged: (db) => _updateGain(left: true, db: db), + onChangeEnd: (_) => _writeGain(), + ), + _GainSlider( + label: 'Right', + register: _rightRegister, + fallbackRegister: _lastRightRegister, + enabled: !disabled && !_linked, + onChanged: (db) => _updateGain(left: false, db: db), + onChangeEnd: (_) => _writeGain(), + ), + ], + if (_error != null) ...[ + const SizedBox(height: 6), + Text( + _error!, + style: Theme.of( + context, + ).textTheme.labelSmall?.copyWith(color: colorScheme.error), + ), + ], + ], + ), + ); + } + + Future _readGain() async { + final manager = _manager; + if (manager == null) { + if (!mounted) return; + setState(() { + _loading = false; + _error = null; + }); + return; + } + + setState(() { + _loading = true; + _error = null; + }); + + try { + final gain = await manager.getMicrophoneGain(); + if (!mounted) return; + setState(() { + _leftRegister = gain.leftRegister; + _rightRegister = gain.rightRegister; + _muted = gain.isMuted; + _linked = gain.leftRegister == gain.rightRegister; + if (!gain.isMuted) { + _lastLeftRegister = gain.leftRegister; + _lastRightRegister = gain.rightRegister; + } + _loading = false; + }); + } catch (_) { + if (!mounted) return; + setState(() { + _loading = false; + _error = 'Could not read microphone gain.'; + }); + } + } + + void _setLinked(bool linked) { + setState(() { + _linked = linked; + if (linked) { + _rightRegister = _leftRegister; + _lastRightRegister = _lastLeftRegister; + } + }); + if (linked && !_muted) { + _writeGain(); + } + } + + void _updateGain({required bool left, required double db}) { + final register = MicrophoneGain.dbToRegister(db); + setState(() { + if (_linked) { + _leftRegister = register; + _rightRegister = register; + _lastLeftRegister = register; + _lastRightRegister = register; + } else if (left) { + _leftRegister = register; + _lastLeftRegister = register; + } else { + _rightRegister = register; + _lastRightRegister = register; + } + }); + } + + Future _toggleMute() async { + setState(() { + if (_muted) { + _leftRegister = _lastLeftRegister; + _rightRegister = _linked ? _lastLeftRegister : _lastRightRegister; + _muted = false; + } else { + _lastLeftRegister = _leftRegister == MicrophoneGain.muteRegister + ? MicrophoneGain.defaultRegister + : _leftRegister; + _lastRightRegister = _rightRegister == MicrophoneGain.muteRegister + ? MicrophoneGain.defaultRegister + : _rightRegister; + _leftRegister = MicrophoneGain.muteRegister; + _rightRegister = MicrophoneGain.muteRegister; + _muted = true; + } + }); + await _writeGain(); + } + + Future _writeGain() async { + final manager = _manager; + if (manager == null) { + return; + } + + setState(() { + _writing = true; + _error = null; + }); + + try { + final gain = MicrophoneGain( + leftRegister: _leftRegister, + rightRegister: _rightRegister, + ); + await manager.setMicrophoneGain(gain); + if (_hasPairedTarget) { + await _pairedManager!.setMicrophoneGain(gain); + } + } catch (_) { + if (!mounted) return; + setState(() { + _error = _hasPairedTarget + ? 'Could not write microphone gain to both devices.' + : 'Could not write microphone gain.'; + }); + } finally { + if (mounted) { + setState(() { + _writing = false; + }); + } + } + } +} + +class _GainSlider extends StatelessWidget { + final String label; + final int register; + final int fallbackRegister; + final bool enabled; + final ValueChanged onChanged; + final ValueChanged onChangeEnd; + + const _GainSlider({ + required this.label, + required this.register, + required this.fallbackRegister, + required this.enabled, + required this.onChanged, + required this.onChangeEnd, + }); + + @override + Widget build(BuildContext context) { + final displayRegister = + register == MicrophoneGain.muteRegister ? fallbackRegister : register; + final db = MicrophoneGain.registerToDb(displayRegister) ?? + MicrophoneGain.registerToDb(MicrophoneGain.defaultRegister)!; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Expanded( + child: Text( + label, + style: Theme.of( + context, + ).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.w600), + ), + ), + Text( + register == MicrophoneGain.muteRegister + ? 'Muted' + : '${_formatDb(db)} (${_formatRegister(register)})', + style: Theme.of(context).textTheme.labelMedium, + ), + ], + ), + Slider.adaptive( + min: MicrophoneGain.minGainDb, + max: MicrophoneGain.maxGainDb, + divisions: MicrophoneGain.minGainRegister, + value: db, + label: _formatDb(db), + onChanged: enabled ? onChanged : null, + onChangeEnd: enabled ? onChangeEnd : null, + ), + ], + ); + } + + String _formatDb(double db) { + final value = db.abs() < 0.001 ? 0.0 : db; + final sign = value > 0 ? '+' : ''; + return '$sign${value.toStringAsFixed(2)} dB'; + } + + String _formatRegister(int register) { + return '0x${register.toRadixString(16).padLeft(2, '0').toUpperCase()}'; + } +} diff --git a/open_wearable/lib/widgets/devices/devices_page.dart b/open_wearable/lib/widgets/devices/devices_page.dart index e47973ca..37ca79b0 100644 --- a/open_wearable/lib/widgets/devices/devices_page.dart +++ b/open_wearable/lib/widgets/devices/devices_page.dart @@ -11,6 +11,7 @@ import 'package:open_wearable/widgets/connector_activity_indicator.dart'; import 'package:open_wearable/widgets/devices/connect_devices_page.dart'; import 'package:open_wearable/widgets/devices/device_detail/audio_mode_widget.dart'; import 'package:open_wearable/widgets/devices/device_detail/device_detail_page.dart'; +import 'package:open_wearable/widgets/devices/device_detail/microphone_gain_controls.dart'; import 'package:open_wearable/widgets/devices/device_detail/microphone_selection_widget.dart'; import 'package:open_wearable/widgets/devices/device_detail/power_saving_mode_widget.dart'; import 'package:open_wearable/widgets/devices/device_detail/stereo_pair_option_selector.dart'; @@ -687,6 +688,11 @@ class _PairedDeviceSheet extends StatelessWidget { return null; } + bool _supportsStereoMicrophoneGain() { + return leftDevice.hasCapability() && + rightDevice.hasCapability(); + } + bool _supportsStereoPowerSavingMode(Wearable device) { return device.hasCapability() && device.hasCapability(); @@ -707,6 +713,7 @@ class _PairedDeviceSheet extends StatelessWidget { final theme = Theme.of(context); final listeningModeDevice = _resolveListeningModeDevice(); final microphoneSelectionDevice = _resolveMicrophoneSelectionDevice(); + final supportsMicrophoneGain = _supportsStereoMicrophoneGain(); final powerSavingModeDevice = _resolvePowerSavingModeDevice(); return DraggableScrollableSheet( @@ -792,6 +799,16 @@ class _PairedDeviceSheet extends StatelessWidget { ), ), ], + if (supportsMicrophoneGain) ...[ + const SizedBox(height: 12), + MicrophoneGainControls( + key: ValueKey( + 'pair_microphone_gain_${leftDevice.deviceId}_${rightDevice.deviceId}', + ), + device: leftDevice, + pairedDevice: rightDevice, + ), + ], if (microphoneSelectionDevice != null) ...[ const SizedBox(height: 12), MicrophoneSelectionWidget( diff --git a/open_wearable/lib/widgets/sensors/configuration/sensor_configuration_detail_view.dart b/open_wearable/lib/widgets/sensors/configuration/sensor_configuration_detail_view.dart index c46baed5..faffff49 100644 --- a/open_wearable/lib/widgets/sensors/configuration/sensor_configuration_detail_view.dart +++ b/open_wearable/lib/widgets/sensors/configuration/sensor_configuration_detail_view.dart @@ -23,8 +23,9 @@ class SensorConfigurationDetailView extends StatelessWidget { Widget build(BuildContext context) { const sensorOnGreen = Color(0xFF2E7D32); final sensorConfigNotifier = context.watch(); - final selectedValue = - sensorConfigNotifier.getSelectedConfigurationValue(sensorConfiguration); + final selectedValue = sensorConfigNotifier.getSelectedConfigurationValue( + sensorConfiguration, + ); final isApplied = sensorConfigNotifier.isConfigurationApplied( sensorConfiguration, ); @@ -32,8 +33,10 @@ class SensorConfigurationDetailView extends StatelessWidget { .getSensorConfigurationValues(sensorConfiguration, distinct: true) .where((value) => _isVisibleValue(value, selectedValue)) .toList(growable: false); - final dropdownSelection = - _resolveSelection(selectableValues, selectedValue); + final dropdownSelection = _resolveSelection( + selectableValues, + selectedValue, + ); final colorScheme = Theme.of(context).colorScheme; final accentColor = isApplied ? sensorOnGreen : colorScheme.primary; final targetOptions = sensorConfiguration is ConfigurableSensorConfiguration @@ -47,8 +50,9 @@ class SensorConfigurationDetailView extends StatelessWidget { } // If on iOS, hide 'microphone' + 'stream' combination - final isMic = - sensorConfiguration.name.toLowerCase().contains('microphone'); + final isMic = sensorConfiguration.name.toLowerCase().contains( + 'microphone', + ); final isStream = option is StreamSensorConfigOption; return !(isMic && isStream); }).toList(growable: false) @@ -60,9 +64,9 @@ class SensorConfigurationDetailView extends StatelessWidget { if (targetOptions.isNotEmpty) ...[ Text( 'Data Targets', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w700, - ), + style: Theme.of( + context, + ).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w700), ), const SizedBox(height: 3), Text( @@ -79,9 +83,7 @@ class SensorConfigurationDetailView extends StatelessWidget { option: targetOptions[i], accentColor: accentColor, selected: sensorConfigNotifier - .getSelectedConfigurationOptions( - sensorConfiguration, - ) + .getSelectedConfigurationOptions(sensorConfiguration) .contains(targetOptions[i]), onChanged: (enabled) { _updatePrimaryAndPair( @@ -110,9 +112,9 @@ class SensorConfigurationDetailView extends StatelessWidget { ], Text( 'Sampling Rate', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w700, - ), + style: Theme.of( + context, + ).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w700), ), const SizedBox(height: 3), Text( @@ -200,8 +202,9 @@ class SensorConfigurationDetailView extends StatelessWidget { return; } - final selectedPrimaryValue = - primaryProvider.getSelectedConfigurationValue(sensorConfiguration); + final selectedPrimaryValue = primaryProvider.getSelectedConfigurationValue( + sensorConfiguration, + ); if (selectedPrimaryValue == null) { return; } @@ -422,10 +425,7 @@ class _OptionToggleTile extends StatelessWidget { (String, String?) _copyForOption(SensorConfigurationOption option) { if (option is StreamSensorConfigOption) { - return ( - 'Live stream to phone', - 'Send to app via Bluetooth.', - ); + return ('Live stream to phone', 'Send to app via Bluetooth.'); } if (option is RecordSensorConfigOption) { return ( diff --git a/open_wearable/lib/widgets/sensors/configuration/sensor_configuration_device_row.dart b/open_wearable/lib/widgets/sensors/configuration/sensor_configuration_device_row.dart index 4845f87a..564d0de9 100644 --- a/open_wearable/lib/widgets/sensors/configuration/sensor_configuration_device_row.dart +++ b/open_wearable/lib/widgets/sensors/configuration/sensor_configuration_device_row.dart @@ -108,10 +108,10 @@ class _SensorConfigurationDeviceRowState fit: FlexFit.loose, child: PlatformText( title, - style: - Theme.of(context).textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.bold, - ), + style: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith(fontWeight: FontWeight.bold), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -173,8 +173,9 @@ class _SensorConfigurationDeviceRowState ); } - final firmwareVersion = - await _readFirmwareVersionForProfiles(widget.device); + final firmwareVersion = await _readFirmwareVersionForProfiles( + widget.device, + ); return DeviceProfileScopeMatch.forDevice( deviceName: _profileDeviceName(), firmwareVersion: firmwareVersion, @@ -436,9 +437,7 @@ class _SensorConfigurationDeviceRowState const Divider(), const Padding( padding: EdgeInsets.all(12), - child: Text( - 'Could not load saved profiles. Please try again.', - ), + child: Text('Could not load saved profiles. Please try again.'), ), Padding( padding: const EdgeInsets.fromLTRB(12, 0, 12, 12), @@ -475,9 +474,7 @@ class _SensorConfigurationDeviceRowState uniqueNameScope: scopeMatch.nameScope, reservedProfileNames: const {_builtInOffProfileTitle}, reservedProfilesByName: { - _builtInOffProfileTitle: _buildBuiltInOffProfileConfig( - widget.device, - ), + _builtInOffProfileTitle: _buildBuiltInOffProfileConfig(widget.device), }, onSaved: _refreshProfiles, ), @@ -496,10 +493,7 @@ class _SensorConfigurationDeviceRowState } else { content.addAll( profileKeys.map( - (key) => _buildProfileTile( - key, - scopeMatch: scopeMatch, - ), + (key) => _buildProfileTile(key, scopeMatch: scopeMatch), ), ); } @@ -520,10 +514,7 @@ class _SensorConfigurationDeviceRowState final title = switch ((isBuiltIn, matchedScope)) { (true, _) => _builtInOffProfileTitle, (false, final scope?) => - SensorConfigurationStorage.displayNameFromScopedKey( - key, - scope: scope, - ), + SensorConfigurationStorage.displayNameFromScopedKey(key, scope: scope), _ => key, }; @@ -590,10 +581,7 @@ class _SensorConfigurationDeviceRowState ? colorScheme.onSurfaceVariant : stateColor, ), - title: PlatformText( - title, - style: titleStyle, - ), + title: PlatformText(title, style: titleStyle), subtitle: PlatformText(subtitle), onTap: () => _loadProfile(key: key, title: title), trailing: Row( @@ -603,10 +591,8 @@ class _SensorConfigurationDeviceRowState ProfileApplicationBadge(state: state), PlatformIconButton( icon: const Icon(Icons.more_horiz), - onPressed: () => _showProfileActions( - key: key, - title: title, - ), + onPressed: () => + _showProfileActions(key: key, title: title), ), ], ), @@ -790,10 +776,7 @@ class _SensorConfigurationDeviceRowState _updateContent(); } - void _showProfileActions({ - required String key, - required String title, - }) { + void _showProfileActions({required String key, required String title}) { final isBuiltIn = _isBuiltInProfileKey(key); final actions = _buildProfileActions( key: key, @@ -982,9 +965,7 @@ class _SensorConfigurationDeviceRowState style: Theme.of(sheetContext) .textTheme .titleMedium - ?.copyWith( - fontWeight: FontWeight.w700, - ), + ?.copyWith(fontWeight: FontWeight.w700), ), ], ), @@ -1010,9 +991,8 @@ class _SensorConfigurationDeviceRowState : ListView.builder( padding: const EdgeInsets.fromLTRB(12, 10, 12, 14), itemCount: details.length, - itemBuilder: (context, index) => ProfileDetailCard( - entry: details[index], - ), + itemBuilder: (context, index) => + ProfileDetailCard(entry: details[index]), ), ), ], @@ -1029,10 +1009,13 @@ class _SensorConfigurationDeviceRowState required SensorConfigurationValue profileValue, required SensorConfigurationProvider provider, }) { - final resolved = - SensorProfileService.describeSensorConfigurationValue(profileValue); - final selectedMatches = - provider.selectedMatchesConfigurationValue(sensorConfig, profileValue); + final resolved = SensorProfileService.describeSensorConfigurationValue( + profileValue, + ); + final selectedMatches = provider.selectedMatchesConfigurationValue( + sensorConfig, + profileValue, + ); final applied = selectedMatches && provider.isConfigurationApplied(sensorConfig); final status = switch ((selectedMatches, applied)) { @@ -1186,12 +1169,7 @@ class _SensorConfigurationDeviceRowState AppToastType type = AppToastType.info, IconData? icon, }) { - AppToast.show( - context, - message: message, - type: type, - icon: icon, - ); + AppToast.show(context, message: message, type: type, icon: icon); } } diff --git a/open_wearable/lib/widgets/sensors/configuration/sensor_configuration_value_row.dart b/open_wearable/lib/widgets/sensors/configuration/sensor_configuration_value_row.dart index 1ed81ef6..fa36db98 100644 --- a/open_wearable/lib/widgets/sensors/configuration/sensor_configuration_value_row.dart +++ b/open_wearable/lib/widgets/sensors/configuration/sensor_configuration_value_row.dart @@ -30,10 +30,7 @@ class SensorConfigurationValueRow extends StatelessWidget { final primaryProvider = context.watch(); final secondaryProvider = pairedProvider; if (secondaryProvider == null) { - return _buildRow( - context, - primaryProvider: primaryProvider, - ); + return _buildRow(context, primaryProvider: primaryProvider); } return ListenableBuilder( @@ -53,14 +50,13 @@ class SensorConfigurationValueRow extends StatelessWidget { }) { const sensorOnGreen = Color(0xFF2E7D32); final colorScheme = Theme.of(context).colorScheme; - final selectedValue = - primaryProvider.getSelectedConfigurationValue(sensorConfiguration); + final selectedValue = primaryProvider.getSelectedConfigurationValue( + sensorConfiguration, + ); final selectedOptions = sensorConfiguration is ConfigurableSensorConfiguration ? primaryProvider - .getSelectedConfigurationOptions( - sensorConfiguration, - ) + .getSelectedConfigurationOptions(sensorConfiguration) .toList(growable: false) : const []; @@ -73,12 +69,16 @@ class SensorConfigurationValueRow extends StatelessWidget { if (mirroredConfig == null) { isMixed = true; } else { - final mirroredValue = - secondaryProvider.getSelectedConfigurationValue(mirroredConfig); - final mirroredApplied = - secondaryProvider.isConfigurationApplied(mirroredConfig); - final valuesMatch = - _configurationValuesMatchNullable(selectedValue, mirroredValue); + final mirroredValue = secondaryProvider.getSelectedConfigurationValue( + mirroredConfig, + ); + final mirroredApplied = secondaryProvider.isConfigurationApplied( + mirroredConfig, + ); + final valuesMatch = _configurationValuesMatchNullable( + selectedValue, + mirroredValue, + ); final applyStateMatches = isApplied == mirroredApplied; if (!valuesMatch || !applyStateMatches) { isMixed = true; @@ -208,9 +208,7 @@ class SensorConfigurationValueRow extends StatelessWidget { style: Theme.of(modalContext) .textTheme .titleMedium - ?.copyWith( - fontWeight: FontWeight.w700, - ), + ?.copyWith(fontWeight: FontWeight.w700), ), const SizedBox(height: 2), Text( @@ -219,9 +217,9 @@ class SensorConfigurationValueRow extends StatelessWidget { .textTheme .bodySmall ?.copyWith( - color: Theme.of(modalContext) - .colorScheme - .onSurfaceVariant, + color: Theme.of( + modalContext, + ).colorScheme.onSurfaceVariant, ), ), ], @@ -354,9 +352,7 @@ class _OptionsCompactBadge extends StatelessWidget { decoration: BoxDecoration( color: colorScheme.surface, borderRadius: BorderRadius.circular(999), - border: Border.all( - color: accentColor.withValues(alpha: 0.38), - ), + border: Border.all(color: accentColor.withValues(alpha: 0.38)), ), child: Row( mainAxisSize: MainAxisSize.min, @@ -391,10 +387,7 @@ class _SamplingRatePill extends StatelessWidget { final String label; final Color foreground; - const _SamplingRatePill({ - required this.label, - required this.foreground, - }); + const _SamplingRatePill({required this.label, required this.foreground}); @override Widget build(BuildContext context) { @@ -408,9 +401,7 @@ class _SamplingRatePill extends StatelessWidget { decoration: BoxDecoration( color: colorScheme.surface, borderRadius: BorderRadius.circular(999), - border: Border.all( - color: foreground.withValues(alpha: 0.42), - ), + border: Border.all(color: foreground.withValues(alpha: 0.42)), ), child: ConstrainedBox( constraints: const BoxConstraints(minWidth: 38), @@ -442,9 +433,7 @@ class _MixedStatePill extends StatelessWidget { decoration: BoxDecoration( color: colorScheme.error.withValues(alpha: 0.10), borderRadius: BorderRadius.circular(999), - border: Border.all( - color: colorScheme.error.withValues(alpha: 0.38), - ), + border: Border.all(color: colorScheme.error.withValues(alpha: 0.38)), ), child: Text( 'Mixed', diff --git a/open_wearable/pubspec.lock b/open_wearable/pubspec.lock index 660c3da9..a509bca9 100644 --- a/open_wearable/pubspec.lock +++ b/open_wearable/pubspec.lock @@ -531,10 +531,9 @@ packages: open_earable_flutter: dependency: "direct main" description: - name: open_earable_flutter - sha256: d7a2e491fa589ea14093101fa37b182d748240ac26fe9cafe9938371f6256b67 - url: "https://pub.dev" - source: hosted + path: "E:/teco/microphone-gain/open_earable_flutter" + relative: false + source: path version: "2.3.10" open_file: dependency: "direct main" diff --git a/open_wearable/pubspec.yaml b/open_wearable/pubspec.yaml index 9b723337..4da9cb86 100644 --- a/open_wearable/pubspec.yaml +++ b/open_wearable/pubspec.yaml @@ -35,7 +35,10 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.9 open_file: ^3.5.11 - open_earable_flutter: ^2.3.10 + open_earable_flutter: + git: + url: https://github.com/OpenEarable/open_earable_flutter.git + ref: microphone-gain-fix universal_ble: ^2.0.2 flutter_platform_widgets: ^10.0.1 provider: ^6.1.5+1