diff --git a/lib/view/pages/bluetooth/BluetoothDeviceModel.dart b/lib/view/pages/bluetooth/BluetoothDeviceModel.dart new file mode 100644 index 00000000..27a8e6b3 --- /dev/null +++ b/lib/view/pages/bluetooth/BluetoothDeviceModel.dart @@ -0,0 +1,62 @@ +import 'package:bluez/bluez.dart'; +import 'package:safe_change_notifier/safe_change_notifier.dart'; + +class BluetoothDeviceModel extends SafeChangeNotifier { + final BlueZDevice _device; + late bool connected; + late String name; + late int appearance; + late int deviceClass; + late String alias; + late bool blocked; + late String address; + late bool paired; + late String errorMessage; + + BluetoothDeviceModel(this._device) { + connected = _device.connected; + name = _device.name; + appearance = _device.appearance; + deviceClass = _device.deviceClass; + alias = _device.alias; + blocked = _device.blocked; + address = _device.address; + paired = _device.paired; + errorMessage = ''; + } + + void init() { + _device.propertiesChanged.listen((event) { + connected = _device.connected; + name = _device.name; + appearance = _device.appearance; + alias = _device.alias; + blocked = _device.blocked; + address = _device.address; + paired = _device.paired; + notifyListeners(); + }); + } + + Future connect() async { + if (!_device.paired) { + await _device.pair().catchError((ioError) { + errorMessage = ioError.toString(); + }); + notifyListeners(); + } + + await _device.connect().catchError((ioError) { + errorMessage = ioError.toString(); + }); + paired = _device.paired; + connected = _device.connected; + notifyListeners(); + } + + Future disconnect() async { + await _device.disconnect(); + connected = _device.connected; + notifyListeners(); + } +} diff --git a/lib/view/pages/bluetooth/bluetooth_device_row.dart b/lib/view/pages/bluetooth/bluetooth_device_row.dart index 0bae97bb..2a5f6e6e 100644 --- a/lib/view/pages/bluetooth/bluetooth_device_row.dart +++ b/lib/view/pages/bluetooth/bluetooth_device_row.dart @@ -1,17 +1,26 @@ import 'package:bluez/bluez.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:settings/view/pages/bluetooth/BluetoothDeviceModel.dart'; import 'package:settings/view/pages/bluetooth/bluetooth_device_types.dart'; -import 'package:settings/view/pages/bluetooth/bluetooth_model.dart'; -import 'package:yaru_icons/widgets/yaru_icons.dart'; import 'package:yaru_widgets/yaru_widgets.dart'; class BluetoothDeviceRow extends StatefulWidget { - const BluetoothDeviceRow( - {Key? key, required this.device, required this.model}) + const BluetoothDeviceRow({Key? key, required this.removeDevice}) : super(key: key); - final BlueZDevice device; - final BluetoothModel model; + final AsyncCallback removeDevice; + + static Widget create( + BuildContext context, BlueZDevice device, AsyncCallback removeDevice) { + return ChangeNotifierProvider( + create: (_) => BluetoothDeviceModel(device), + child: BluetoothDeviceRow( + removeDevice: removeDevice, + ), + ); + } @override State createState() => _BluetoothDeviceRowState(); @@ -22,25 +31,134 @@ class _BluetoothDeviceRowState extends State { @override void initState() { - status = widget.device.connected ? 'connected' : 'disconnected'; + final model = context.read(); + model.init(); super.initState(); } @override Widget build(BuildContext context) { - status = widget.device.connected ? 'connected' : 'disconnected'; + final model = context.watch(); return InkWell( borderRadius: BorderRadius.circular(4.0), onTap: () => setState(() { - showSimpleDeviceDialog(context); + showDialog( + context: context, + builder: (context) => StatefulBuilder(builder: (context, setState) { + return AlertDialog( + title: Padding( + padding: + const EdgeInsets.only(right: 8, left: 8, bottom: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: RichText( + text: TextSpan( + text: model.name, + style: Theme.of(context).textTheme.headline6), + maxLines: 10, + overflow: TextOverflow.ellipsis, + ), + ), + Padding( + padding: const EdgeInsets.only(left: 10), + child: Icon( + BluetoothDeviceTypes.getIconForAppearanceCode( + model.appearance)), + ) + ], + ), + ), + content: SizedBox( + height: model.errorMessage.isEmpty ? 270 : 320, + width: 300, + child: SingleChildScrollView( + child: Column( + children: [ + YaruRow( + trailingWidget: model.connected + ? const Text('Connected') + : const Text('Disconnected'), + actionWidget: Switch( + value: model.connected, + onChanged: (connectRequested) async { + connectRequested + ? await model.connect() + : await model.disconnect(); + setState(() {}); + })), + YaruRow( + trailingWidget: const Text('Paired'), + actionWidget: Padding( + padding: const EdgeInsets.only(right: 8), + child: Text(model.paired ? 'Yes' : 'No'), + )), + YaruRow( + trailingWidget: const Text('Address'), + actionWidget: Padding( + padding: const EdgeInsets.only(right: 8), + child: Text(model.address), + )), + YaruRow( + trailingWidget: const Text('Type'), + actionWidget: Padding( + padding: const EdgeInsets.only(right: 8), + child: Text(BluetoothDeviceTypes + .map[model.appearance] ?? + 'Unkown'), + )), + Padding( + padding: const EdgeInsets.only( + top: 16, bottom: 8, right: 8, left: 8), + child: SizedBox( + width: 300, + child: OutlinedButton( + onPressed: () { + if (BluetoothDeviceTypes.isMouse( + model.appearance)) { + // TODO: get route name from model + Navigator.of(context) + .pushNamed('routeName'); + } + }, + child: const Text('Open device settings')), + ), + ), + Padding( + padding: const EdgeInsets.all(8), + child: SizedBox( + width: 300, + child: TextButton( + onPressed: () async { + await model.disconnect(); + widget.removeDevice; + + Navigator.of(context).pop(); + }, + child: const Text('Remove device')), + ), + ), + if (model.errorMessage.isNotEmpty) + Text( + model.errorMessage, + style: TextStyle( + color: Theme.of(context).errorColor), + ) + ], + ), + ), + ), + ); + })); }), child: Padding( padding: const EdgeInsets.all(8.0), child: YaruRow( - trailingWidget: Text(widget.device.name), + trailingWidget: Text(model.name), actionWidget: Text( - widget.device.connected ? 'connected' : 'disconnected', + model.connected ? 'connected' : 'disconnected', style: TextStyle( color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7)), @@ -48,114 +166,4 @@ class _BluetoothDeviceRowState extends State { ), ); } - - void showSimpleDeviceDialog(BuildContext context) { - showDialog( - context: context, - builder: (context) => StatefulBuilder(builder: (context, setState) { - return AlertDialog( - title: Padding( - padding: const EdgeInsets.only(right: 8, left: 8, bottom: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: RichText( - text: TextSpan( - text: widget.device.name, - style: Theme.of(context).textTheme.headline6), - maxLines: 10, - overflow: TextOverflow.ellipsis, - ), - ), - Padding( - padding: const EdgeInsets.only(left: 10), - child: Icon( - BluetoothDeviceTypes.getIconForAppearanceCode( - widget.device.appearance)), - ) - ], - ), - ), - content: SizedBox( - height: 270, - width: 300, - child: SingleChildScrollView( - child: Column( - children: [ - YaruRow( - trailingWidget: widget.device.connected - ? const Text('Connected') - : const Text('Disconnected'), - actionWidget: Switch( - value: widget.device.connected, - onChanged: (newValue) async { - widget.device.connected - ? await widget.device.disconnect() - : await widget.device - .connect() - .catchError((ioError) => {}); - Navigator.of(context).pop(); - setState(() {}); - })), - YaruRow( - trailingWidget: widget.device.paired - ? const Text('Paired') - : const Text('Unpaired'), - actionWidget: Padding( - padding: const EdgeInsets.only(right: 8), - child: Text(widget.device.paired ? 'Yes' : 'No'), - )), - YaruRow( - trailingWidget: const Text('Address'), - actionWidget: Padding( - padding: const EdgeInsets.only(right: 8), - child: Text(widget.device.address), - )), - YaruRow( - trailingWidget: const Text('Type'), - actionWidget: Padding( - padding: const EdgeInsets.only(right: 8), - child: Text(BluetoothDeviceTypes - .map[widget.device.appearance] ?? - 'Unkown'), - )), - Padding( - padding: const EdgeInsets.only( - top: 16, bottom: 8, right: 8, left: 8), - child: SizedBox( - width: 300, - child: OutlinedButton( - onPressed: () { - if (BluetoothDeviceTypes.isMouse( - widget.device.appearance)) { - // TODO: get route name from model - Navigator.of(context) - .pushNamed('routeName'); - } - }, - child: const Text('Open device settings')), - ), - ), - Padding( - padding: const EdgeInsets.all(8), - child: SizedBox( - width: 300, - child: TextButton( - onPressed: () async { - await widget.device.disconnect().then( - (value) => widget.model - .removeDevice(widget.device)); - Navigator.of(context).pop(); - }, - child: const Text('Remove device')), - ), - ) - ], - ), - ), - ), - ); - })).then((value) => setState(() {})); - } } diff --git a/lib/view/pages/bluetooth/bluetooth_model.dart b/lib/view/pages/bluetooth/bluetooth_model.dart index 46f5e1ab..cf1b9cbc 100644 --- a/lib/view/pages/bluetooth/bluetooth_model.dart +++ b/lib/view/pages/bluetooth/bluetooth_model.dart @@ -8,13 +8,19 @@ class BluetoothModel extends SafeChangeNotifier { late StreamSubscription? _devicesAdded; late StreamSubscription? _devicesRemoved; + late BlueZAdapter? firstAdapter; BluetoothModel(this._client); void init() async { await _client.connect().then((value) { - for (var adapter in _client.adapters) { - adapter.startDiscovery(); + if (_client.adapters.isEmpty) { + _client.close(); + return; + } + firstAdapter = _client.adapters[0]; + if (!firstAdapter!.discovering) { + firstAdapter?.startDiscovery(); } _devicesAdded = _client.deviceAdded.listen((event) { notifyListeners(); @@ -30,21 +36,19 @@ class BluetoothModel extends SafeChangeNotifier { return _client.devices; } - void removeDevice(BlueZDevice device) { - for (var adapter in _client.adapters) { - adapter.removeDevice(device); - } + Future removeDevice(BlueZDevice device) async { + await firstAdapter?.removeDevice(device); notifyListeners(); } @override void dispose() { - for (var adapter in _client.adapters) { - adapter.stopDiscovery(); + if (firstAdapter!.discovering) { + firstAdapter?.stopDiscovery(); } - _devicesAdded!.cancel(); - _devicesRemoved!.cancel(); + _devicesAdded?.cancel(); + _devicesRemoved?.cancel(); super.dispose(); } } diff --git a/lib/view/pages/bluetooth/bluetooth_page.dart b/lib/view/pages/bluetooth/bluetooth_page.dart index 5c5c3f1b..613a0dc4 100644 --- a/lib/view/pages/bluetooth/bluetooth_page.dart +++ b/lib/view/pages/bluetooth/bluetooth_page.dart @@ -43,10 +43,10 @@ class _BluetoothPageState extends State { ListView.builder( shrinkWrap: true, itemCount: model.devices.length, - itemBuilder: (context, index) => BluetoothDeviceRow( - device: model.devices[index], - model: model, - ), + itemBuilder: (context, index) => BluetoothDeviceRow.create( + context, + model.devices[index], + () => model.removeDevice(model.devices[index])), ) ]), ],