Merge pull request #182 from jonasbark/copilot/fix-ble-device-reconnection

Persist ignored BLE devices across app restarts
This commit is contained in:
jonasbark
2025-11-15 09:04:53 +01:00
committed by GitHub
4 changed files with 158 additions and 8 deletions

View File

@@ -52,8 +52,6 @@ class Connection {
Timer? _gamePadSearchTimer;
final _dontAllowReconnectDevices = <String>{};
void initialize() {
actionStream.listen((log) {
lastLogEntries.add((date: DateTime.now(), entry: log.toString()));
@@ -211,9 +209,20 @@ class Connection {
}
void addDevices(List<BaseDevice> dev) {
final newDevices = dev
.where((device) => !devices.contains(device) && !_dontAllowReconnectDevices.contains(device.name))
.toList();
final ignoredDevices = settings.getIgnoredDevices();
final ignoredDeviceIds = ignoredDevices.map((d) => d.id).toSet();
final newDevices = dev.where((device) {
if (devices.contains(device)) return false;
// Check if device is in the ignored list
if (device is BluetoothDevice) {
if (ignoredDeviceIds.contains(device.device.deviceId)) {
return false;
}
}
return true;
}).toList();
devices.addAll(newDevices);
_connectionQueue.addAll(newDevices);
@@ -258,7 +267,7 @@ class Connection {
device.isConnected = state;
_connectionStreams.add(device);
if (!device.isConnected) {
disconnect(device, forget: true);
disconnect(device, forget: false);
// try reconnect
performScanning();
}
@@ -332,13 +341,26 @@ class Connection {
if (device.isConnected) {
await device.disconnect();
}
if (!forget && device is BluetoothDevice) {
if (device is BluetoothDevice) {
if (forget) {
// Add device to ignored list when forgetting
await settings.addIgnoredDevice(device.device.deviceId, device.name);
_actionStreams.add(LogNotification('Device ignored: ${device.name}'));
}
// Clean up subscriptions and scan results for reconnection
_lastScanResult.removeWhere((b) => b.deviceId == device.device.deviceId);
_streamSubscriptions[device]?.cancel();
_streamSubscriptions.remove(device);
_connectionSubscriptions[device]?.cancel();
_connectionSubscriptions.remove(device);
// Remove device from the list
devices.remove(device);
hasDevices.value = devices.isNotEmpty;
}
signalChange(device);
}

View File

@@ -182,4 +182,48 @@ class Settings {
Future<void> setMiuiWarningDismissed(bool dismissed) async {
await prefs.setBool('miui_warning_dismissed', dismissed);
}
List<String> _getIgnoredDeviceIds() {
return prefs.getStringList('ignored_device_ids') ?? [];
}
List<String> _getIgnoredDeviceNames() {
return prefs.getStringList('ignored_device_names') ?? [];
}
Future<void> addIgnoredDevice(String deviceId, String deviceName) async {
final ids = _getIgnoredDeviceIds();
final names = _getIgnoredDeviceNames();
if (!ids.contains(deviceId)) {
ids.add(deviceId);
names.add(deviceName);
await prefs.setStringList('ignored_device_ids', ids);
await prefs.setStringList('ignored_device_names', names);
}
}
Future<void> removeIgnoredDevice(String deviceId) async {
final ids = _getIgnoredDeviceIds();
final names = _getIgnoredDeviceNames();
final index = ids.indexOf(deviceId);
if (index != -1) {
ids.removeAt(index);
names.removeAt(index);
await prefs.setStringList('ignored_device_ids', ids);
await prefs.setStringList('ignored_device_names', names);
}
}
List<({String id, String name})> getIgnoredDevices() {
final ids = _getIgnoredDeviceIds();
final names = _getIgnoredDeviceNames();
final result = <({String id, String name})>[];
for (int i = 0; i < ids.length && i < names.length; i++) {
result.add((id: ids[i], name: names[i]));
}
return result;
}
}

View File

@@ -0,0 +1,75 @@
import 'package:flutter/material.dart';
import 'package:swift_control/main.dart';
class IgnoredDevicesDialog extends StatefulWidget {
const IgnoredDevicesDialog({super.key});
@override
State<IgnoredDevicesDialog> createState() => _IgnoredDevicesDialogState();
}
class _IgnoredDevicesDialogState extends State<IgnoredDevicesDialog> {
List<({String id, String name})> _ignoredDevices = [];
@override
void initState() {
super.initState();
_loadIgnoredDevices();
}
void _loadIgnoredDevices() {
setState(() {
_ignoredDevices = settings.getIgnoredDevices();
});
}
Future<void> _removeDevice(String deviceId) async {
await settings.removeIgnoredDevice(deviceId);
_loadIgnoredDevices();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Ignored Devices'),
content: SizedBox(
width: double.maxFinite,
child: _ignoredDevices.isEmpty
? Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'No ignored devices.',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
)
: ListView.builder(
shrinkWrap: true,
itemCount: _ignoredDevices.length,
itemBuilder: (context, index) {
final device = _ignoredDevices[index];
return ListTile(
title: Text(device.name),
subtitle: Text(
device.id,
style: TextStyle(fontSize: 12),
),
trailing: IconButton(
icon: Icon(Icons.delete_outline),
tooltip: 'Remove from ignored list',
onPressed: () => _removeDevice(device.id),
),
);
},
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Close'),
),
],
);
}
}

View File

@@ -13,6 +13,7 @@ import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart';
import '../pages/device.dart';
import 'ignored_devices_dialog.dart';
List<Widget> buildMenuButtons() {
return [
@@ -174,7 +175,15 @@ class MenuButton extends StatelessWidget {
Navigator.push(context, MaterialPageRoute(builder: (c) => MarkdownPage(assetPath: 'CHANGELOG.md')));
},
),
PopupMenuItem(
child: Text('Ignored Devices'),
onTap: () {
showDialog(
context: context,
builder: (context) => IgnoredDevicesDialog(),
);
},
),
PopupMenuItem(
child: Text('License'),
onTap: () {