Skip to content

Commit 4f1e311

Browse files
authored
Add Bluetooth page (#139)
1 parent de9db19 commit 4f1e311

File tree

7 files changed

+360
-2
lines changed

7 files changed

+360
-2
lines changed

lib/main.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:bluez/bluez.dart';
12
import 'package:flutter/material.dart';
23
import 'package:gsettings/gsettings.dart';
34
import 'package:nm/nm.dart';
@@ -57,6 +58,10 @@ void main() async {
5758
create: (_) => UPowerClient(),
5859
dispose: (_, client) => client.close(),
5960
),
61+
Provider<BlueZClient>(
62+
create: (_) => BlueZClient(),
63+
dispose: (_, client) => client.close(),
64+
)
6065
],
6166
child: const UbuntuSettingsApp(),
6267
),
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import 'package:bluez/bluez.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:settings/view/pages/bluetooth/bluetooth_device_types.dart';
4+
import 'package:settings/view/pages/bluetooth/bluetooth_model.dart';
5+
import 'package:yaru_icons/widgets/yaru_icons.dart';
6+
import 'package:yaru_widgets/yaru_widgets.dart';
7+
8+
class BluetoothDeviceRow extends StatefulWidget {
9+
const BluetoothDeviceRow(
10+
{Key? key, required this.device, required this.model})
11+
: super(key: key);
12+
13+
final BlueZDevice device;
14+
final BluetoothModel model;
15+
16+
@override
17+
State<BluetoothDeviceRow> createState() => _BluetoothDeviceRowState();
18+
}
19+
20+
class _BluetoothDeviceRowState extends State<BluetoothDeviceRow> {
21+
late String status;
22+
23+
@override
24+
void initState() {
25+
status = widget.device.connected ? 'connected' : 'disconnected';
26+
27+
super.initState();
28+
}
29+
30+
@override
31+
Widget build(BuildContext context) {
32+
status = widget.device.connected ? 'connected' : 'disconnected';
33+
return InkWell(
34+
borderRadius: BorderRadius.circular(4.0),
35+
onTap: () => setState(() {
36+
showSimpleDeviceDialog(context);
37+
}),
38+
child: Padding(
39+
padding: const EdgeInsets.all(8.0),
40+
child: YaruRow(
41+
trailingWidget: Text(widget.device.name),
42+
actionWidget: Text(
43+
widget.device.connected ? 'connected' : 'disconnected',
44+
style: TextStyle(
45+
color:
46+
Theme.of(context).colorScheme.onSurface.withOpacity(0.7)),
47+
)),
48+
),
49+
);
50+
}
51+
52+
void showSimpleDeviceDialog(BuildContext context) {
53+
showDialog(
54+
context: context,
55+
builder: (context) => StatefulBuilder(builder: (context, setState) {
56+
return AlertDialog(
57+
title: Padding(
58+
padding: const EdgeInsets.only(right: 8, left: 8, bottom: 8),
59+
child: Row(
60+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
61+
children: [
62+
Flexible(
63+
child: RichText(
64+
text: TextSpan(
65+
text: widget.device.name,
66+
style: Theme.of(context).textTheme.headline6),
67+
maxLines: 10,
68+
overflow: TextOverflow.ellipsis,
69+
),
70+
),
71+
Padding(
72+
padding: const EdgeInsets.only(left: 10),
73+
child: Icon(
74+
BluetoothDeviceTypes.getIconForAppearanceCode(
75+
widget.device.appearance)),
76+
)
77+
],
78+
),
79+
),
80+
content: SizedBox(
81+
height: 270,
82+
width: 300,
83+
child: SingleChildScrollView(
84+
child: Column(
85+
children: [
86+
YaruRow(
87+
trailingWidget: widget.device.connected
88+
? const Text('Connected')
89+
: const Text('Disconnected'),
90+
actionWidget: Switch(
91+
value: widget.device.connected,
92+
onChanged: (newValue) async {
93+
widget.device.connected
94+
? await widget.device.disconnect()
95+
: await widget.device
96+
.connect()
97+
.catchError((ioError) => {});
98+
Navigator.of(context).pop();
99+
setState(() {});
100+
})),
101+
YaruRow(
102+
trailingWidget: widget.device.paired
103+
? const Text('Paired')
104+
: const Text('Unpaired'),
105+
actionWidget: Padding(
106+
padding: const EdgeInsets.only(right: 8),
107+
child: Text(widget.device.paired ? 'Yes' : 'No'),
108+
)),
109+
YaruRow(
110+
trailingWidget: const Text('Address'),
111+
actionWidget: Padding(
112+
padding: const EdgeInsets.only(right: 8),
113+
child: Text(widget.device.address),
114+
)),
115+
YaruRow(
116+
trailingWidget: const Text('Type'),
117+
actionWidget: Padding(
118+
padding: const EdgeInsets.only(right: 8),
119+
child: Text(BluetoothDeviceTypes
120+
.map[widget.device.appearance] ??
121+
'Unkown'),
122+
)),
123+
Padding(
124+
padding: const EdgeInsets.only(
125+
top: 16, bottom: 8, right: 8, left: 8),
126+
child: SizedBox(
127+
width: 300,
128+
child: OutlinedButton(
129+
onPressed: () {
130+
if (BluetoothDeviceTypes.isMouse(
131+
widget.device.appearance)) {
132+
// TODO: get route name from model
133+
Navigator.of(context)
134+
.pushNamed('routeName');
135+
}
136+
},
137+
child: const Text('Open device settings')),
138+
),
139+
),
140+
Padding(
141+
padding: const EdgeInsets.all(8),
142+
child: SizedBox(
143+
width: 300,
144+
child: TextButton(
145+
onPressed: () async {
146+
await widget.device.disconnect().then(
147+
(value) => widget.model
148+
.removeDevice(widget.device));
149+
Navigator.of(context).pop();
150+
},
151+
child: const Text('Remove device')),
152+
),
153+
)
154+
],
155+
),
156+
),
157+
),
158+
);
159+
})).then((value) => setState(() {}));
160+
}
161+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:yaru_icons/widgets/yaru_icons.dart';
3+
4+
class BluetoothDeviceTypes {
5+
static const map = <int, String>{
6+
0: 'unknown',
7+
64: 'generic phone',
8+
128: 'generic computer',
9+
192: 'generic watch',
10+
193: 'watch sports watch',
11+
256: 'generic clock',
12+
320: 'generic display',
13+
384: 'generic remote control',
14+
448: 'generic eye glasses',
15+
512: 'generic tag',
16+
576: 'generic keyring',
17+
640: 'generic media player',
18+
704: 'generic barcode scanner',
19+
768: 'generic thermometer',
20+
769: 'thermometer ear',
21+
832: 'generic heart rate sensor',
22+
833: 'heart rate sensor heart rate belt ',
23+
896: 'generic blood pressure',
24+
897: 'blood pressure arm',
25+
898: 'blood pressure wrist',
26+
960: 'generic hid',
27+
961: 'hid keyboard',
28+
962: 'hid mouse',
29+
963: 'hid joystick',
30+
964: 'hid gamepad',
31+
965: 'hid digitizersubtype',
32+
966: 'hid card reader',
33+
967: 'hid digital pen',
34+
968: 'hid barcode',
35+
1024: 'generic glucose meter',
36+
1088: 'generic running walking sensor',
37+
1089: 'running walking sensor in shoe',
38+
1090: 'running walking sensor on shoe',
39+
1091: 'running walking sensor on hip',
40+
1152: 'generic cycling',
41+
1153: 'cycling cycling computer',
42+
1154: 'cycling speed sensor',
43+
1155: 'cycling cadence sensor',
44+
1156: 'cycling power sensor',
45+
1157: 'cycling speed cadence sensor',
46+
3136: 'generic pulse oximeter',
47+
3137: 'pulse oximeter fingertip',
48+
3138: 'pulse oximeter wrist worn',
49+
3200: 'generic weight scale',
50+
5184: 'generic outdoor sports act',
51+
5185: 'outdoor sports act loc disp',
52+
5186: 'outdoor sports act loc and nav disp',
53+
5187: 'outdoor sports act loc pod',
54+
5188: 'outdoor sports act loc and nav pod',
55+
};
56+
57+
static bool isMouse(int appearanceCode) =>
58+
appearanceCode == 962 ? true : false;
59+
60+
static bool isKeyboard(int appearanceCode) =>
61+
appearanceCode == 961 ? true : false;
62+
63+
static bool isGamePad(int appearanceCode) =>
64+
appearanceCode == 964 ? true : false;
65+
66+
static bool isJoyStick(int appearanceCode) =>
67+
appearanceCode == 963 ? true : false;
68+
69+
static bool isMediaPlayer(int appearanceCode) =>
70+
appearanceCode == 640 ? true : false;
71+
72+
static IconData getIconForAppearanceCode(int appearanceCode) {
73+
if (isMouse(appearanceCode)) {
74+
return YaruIcons.input_mouse;
75+
} else if (isKeyboard(appearanceCode)) {
76+
return YaruIcons.input_keyboard;
77+
} else if (isGamePad(appearanceCode) || isJoyStick(appearanceCode)) {
78+
return YaruIcons.input_gaming;
79+
} else if (isMediaPlayer(appearanceCode)) {
80+
return YaruIcons.headphones;
81+
} else {}
82+
83+
return YaruIcons.question;
84+
}
85+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import 'dart:async';
2+
3+
import 'package:bluez/bluez.dart';
4+
import 'package:safe_change_notifier/safe_change_notifier.dart';
5+
6+
class BluetoothModel extends SafeChangeNotifier {
7+
final BlueZClient _client;
8+
9+
late StreamSubscription<BlueZDevice>? _devicesAdded;
10+
late StreamSubscription<BlueZDevice>? _devicesRemoved;
11+
12+
BluetoothModel(this._client);
13+
14+
void init() async {
15+
await _client.connect().then((value) {
16+
for (var adapter in _client.adapters) {
17+
adapter.startDiscovery();
18+
}
19+
_devicesAdded = _client.deviceAdded.listen((event) {
20+
notifyListeners();
21+
});
22+
_devicesRemoved = _client.deviceRemoved.listen((event) {
23+
notifyListeners();
24+
});
25+
notifyListeners();
26+
});
27+
}
28+
29+
List<BlueZDevice> get devices {
30+
return _client.devices;
31+
}
32+
33+
void removeDevice(BlueZDevice device) {
34+
for (var adapter in _client.adapters) {
35+
adapter.removeDevice(device);
36+
}
37+
38+
notifyListeners();
39+
}
40+
41+
@override
42+
void dispose() {
43+
for (var adapter in _client.adapters) {
44+
adapter.stopDiscovery();
45+
}
46+
_devicesAdded!.cancel();
47+
_devicesRemoved!.cancel();
48+
super.dispose();
49+
}
50+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import 'package:bluez/bluez.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:provider/provider.dart';
4+
import 'package:settings/view/pages/bluetooth/bluetooth_device_row.dart';
5+
import 'package:settings/view/pages/bluetooth/bluetooth_model.dart';
6+
import 'package:yaru_widgets/yaru_widgets.dart';
7+
8+
class BluetoothPage extends StatefulWidget {
9+
const BluetoothPage({Key? key}) : super(key: key);
10+
11+
static Widget create(BuildContext context) {
12+
return ChangeNotifierProvider(
13+
create: (_) => BluetoothModel(context.read<BlueZClient>()),
14+
child: const BluetoothPage(),
15+
);
16+
}
17+
18+
@override
19+
State<BluetoothPage> createState() => _BluetoothPageState();
20+
}
21+
22+
class _BluetoothPageState extends State<BluetoothPage> {
23+
@override
24+
void initState() {
25+
final model = context.read<BluetoothModel>();
26+
model.init();
27+
super.initState();
28+
}
29+
30+
@override
31+
Widget build(BuildContext context) {
32+
final model = context.watch<BluetoothModel>();
33+
return Column(
34+
children: [
35+
YaruSection(
36+
headerWidget: const SizedBox(
37+
height: 15,
38+
width: 15,
39+
child: CircularProgressIndicator(),
40+
),
41+
headline: 'Bluetooth devices',
42+
children: [
43+
ListView.builder(
44+
shrinkWrap: true,
45+
itemCount: model.devices.length,
46+
itemBuilder: (context, index) => BluetoothDeviceRow(
47+
device: model.devices[index],
48+
model: model,
49+
),
50+
)
51+
]),
52+
],
53+
);
54+
}
55+
}

lib/view/pages/page_items.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
22
import 'package:flutter/widgets.dart';
33
import 'package:settings/view/pages/accessibility/accessibility_page.dart';
44
import 'package:settings/view/pages/appearance/appearance_page.dart';
5+
import 'package:settings/view/pages/bluetooth/bluetooth_page.dart';
56
import 'package:settings/view/pages/info/info_page.dart';
67
import 'package:settings/view/pages/keyboard_shortcuts/keyboard_shortcuts_page.dart';
78
import 'package:settings/view/pages/mouse_and_touchpad/mouse_and_touchpad_page.dart';
@@ -20,10 +21,10 @@ final pageItems = <YaruPageItem>[
2021
iconData: YaruIcons.network,
2122
builder: ConnectionsPage.create,
2223
),
23-
YaruPageItem(
24+
const YaruPageItem(
2425
title: 'Bluetooth',
2526
iconData: YaruIcons.bluetooth,
26-
builder: (_) => const Text('Bluetooth'),
27+
builder: BluetoothPage.create,
2728
),
2829
const YaruPageItem(
2930
title: 'Wallpaper',

pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ dependencies:
2121
gsettings: ^0.1.2+1
2222
linux_system_info: ^0.0.7
2323
mime: ^1.0.0
24+
bluez: ^0.7.4
2425
flex_color_picker: ^2.1.2
2526
nm: ^0.4.0
2627
open_file: ^3.2.1

0 commit comments

Comments
 (0)