Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 125 additions & 5 deletions open_wearable/lib/widgets/devices/connect_devices_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ class ConnectDevicesPage extends StatefulWidget {
class _ConnectDevicesPageState extends State<ConnectDevicesPage> {
bool _hasBlePermissions = false;
bool _hasMicPermission = true;
bool _showOnlyOpenEarableDevices = false;
String _deviceNameFilterText = 'OpenEarable';
final Map<String, bool> _connectingDevices = {};

late ConnectDevicesScanSnapshot _scanSnapshot;
Expand Down Expand Up @@ -134,12 +136,17 @@ class _ConnectDevicesPageState extends State<ConnectDevicesPage> {
.where((device) => !connectedDeviceIds.contains(device.id))
.toList();
final thisDeviceEntry = _thisDeviceEntry;
final availableDevices = [
final allAvailableDevices = [
if (thisDeviceEntry != null &&
!connectedDeviceIds.contains(thisDeviceEntry.id))
thisDeviceEntry,
...scannedDevices.where((device) => device.id != thisDeviceEntry?.id),
];
final availableDevices = _showOnlyOpenEarableDevices
? allAvailableDevices
.where((device) => device.name.contains(_deviceNameFilterText))
.toList()
: allAvailableDevices;

return PlatformScaffold(
appBar: PlatformAppBar(
Expand Down Expand Up @@ -249,16 +256,15 @@ class _ConnectDevicesPageState extends State<ConnectDevicesPage> {
context,
title: 'Available',
count: availableDevices.length,
trailing: _buildOpenEarableFilterToggle(context),
),
if (availableDevices.isEmpty)
_buildEmptyCard(
context,
title: _scanSnapshot.isScanning
? 'Scanning for devices...'
: 'No devices found yet',
subtitle: _scanSnapshot.isScanning
? 'Make sure your wearable is turned on and nearby.'
: 'Press scan again or pull to refresh.',
: 'No devices found',
subtitle: _emptyAvailableDevicesMessage(),
)
else
...availableDevices.map((device) {
Expand Down Expand Up @@ -368,6 +374,7 @@ class _ConnectDevicesPageState extends State<ConnectDevicesPage> {
BuildContext context, {
required String title,
required int count,
Widget? trailing,
}) {
return Padding(
padding: const EdgeInsets.fromLTRB(4, 0, 4, 8),
Expand All @@ -381,6 +388,18 @@ class _ConnectDevicesPageState extends State<ConnectDevicesPage> {
),
const SizedBox(width: 8),
_StatusPill(label: '$count'),
if (trailing != null) ...[
const SizedBox(width: 8),
Expanded(
child: Align(
alignment: Alignment.centerRight,
child: FittedBox(
fit: BoxFit.scaleDown,
child: trailing,
),
),
),
],
],
),
);
Expand All @@ -401,6 +420,98 @@ class _ConnectDevicesPageState extends State<ConnectDevicesPage> {
);
}

Widget _buildOpenEarableFilterToggle(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;

return Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Device name filter',
style: theme.textTheme.labelSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 6),
InkWell(
borderRadius: BorderRadius.circular(999),
onTap: _editDeviceNameFilterText,
child: Container(
constraints: const BoxConstraints(minWidth: 78, maxWidth: 128),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: BorderRadius.circular(999),
border: Border.all(
color: colorScheme.outlineVariant.withValues(alpha: 0.9),
),
),
child: Text(
_deviceNameFilterText,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: theme.textTheme.labelSmall?.copyWith(
color: colorScheme.onSurface,
fontWeight: FontWeight.w700,
),
),
),
),
const SizedBox(width: 2),
Transform.scale(
scale: 0.75,
child: Switch.adaptive(
value: _showOnlyOpenEarableDevices,
onChanged: (value) {
setState(() {
_showOnlyOpenEarableDevices = value;
});
},
),
),
],
);
}

Future<void> _editDeviceNameFilterText() async {
var editedText = _deviceNameFilterText;
final nextText = await showPlatformDialog<String>(
context: context,
builder: (dialogContext) => PlatformAlertDialog(
title: const Text('Device name filter'),
content: TextFormField(
initialValue: editedText,
autofocus: true,
decoration: const InputDecoration(labelText: 'Name contains'),
textInputAction: TextInputAction.done,
onChanged: (value) => editedText = value,
onFieldSubmitted: (value) => Navigator.of(dialogContext).pop(value),
),
actions: [
PlatformDialogAction(
onPressed: () => Navigator.of(dialogContext).pop(),
child: const Text('Cancel'),
),
PlatformDialogAction(
onPressed: () => Navigator.of(dialogContext).pop(editedText),
child: const Text('Apply'),
),
],
),
);

final trimmedText = nextText?.trim();
if (trimmedText == null || trimmedText.isEmpty || !mounted) {
return;
}

setState(() {
_deviceNameFilterText = trimmedText;
});
}

Widget _buildTrailingWidget(
DiscoveredDevice device, {
required VoidCallback onConnect,
Expand Down Expand Up @@ -446,6 +557,15 @@ class _ConnectDevicesPageState extends State<ConnectDevicesPage> {
return '${elapsed.inHours}h ago';
}

String _emptyAvailableDevicesMessage() {
if (_showOnlyOpenEarableDevices) {
return 'No devices match "$_deviceNameFilterText".';
}
return _scanSnapshot.isScanning
? 'Make sure your wearable is turned on and nearby.'
: 'Press scan again or pull to refresh.';
}

Future<void> _addThisDeviceToDiscovered() async {
if (_thisDeviceEntry != null) return;
final profile = await DeviceProfile.fetch();
Expand Down