summaryrefslogtreecommitdiff
path: root/lib/widgets
diff options
context:
space:
mode:
Diffstat (limited to 'lib/widgets')
-rw-r--r--lib/widgets/badge_app.dart50
-rw-r--r--lib/widgets/device_details.dart101
-rw-r--r--lib/widgets/device_scan_select.dart86
-rw-r--r--lib/widgets/notifying_list_widget.dart27
-rw-r--r--lib/widgets/scan_page.dart142
5 files changed, 406 insertions, 0 deletions
diff --git a/lib/widgets/badge_app.dart b/lib/widgets/badge_app.dart
new file mode 100644
index 0000000..68cfce9
--- /dev/null
+++ b/lib/widgets/badge_app.dart
@@ -0,0 +1,50 @@
+// Copyright (C) 2025, uvok cheetah
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import 'package:uvok_epaper_badge/control/mock_scanner_controller.dart';
+import 'package:uvok_epaper_badge/widgets/scan_page.dart';
+import 'package:flutter/material.dart';
+
+class BadgeApp extends StatelessWidget {
+ const BadgeApp({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final selectedScanner = MockScannerController();
+
+ return MaterialApp(
+ title: 'Scanner',
+ theme: ThemeData(
+ // This is the theme of your application.
+ //
+ // TRY THIS: Try running your application with "flutter run". You'll see
+ // the application has a purple toolbar. Then, without quitting the app,
+ // try changing the seedColor in the colorScheme below to Colors.green
+ // and then invoke "hot reload" (save your changes or press the "hot
+ // reload" button in a Flutter-supported IDE, or press "r" if you used
+ // the command line to start the app).
+ //
+ // Notice that the counter didn't reset back to zero; the application
+ // state is not lost during the reload. To reset the state, use hot
+ // restart instead.
+ //
+ // This works for code too, not just values: Most code changes can be
+ // tested with just a hot reload.
+ colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
+ ),
+ home: ScanPage(title: 'Badge Scanner', deviceScanner: selectedScanner),
+ );
+ }
+}
diff --git a/lib/widgets/device_details.dart b/lib/widgets/device_details.dart
new file mode 100644
index 0000000..f3754d3
--- /dev/null
+++ b/lib/widgets/device_details.dart
@@ -0,0 +1,101 @@
+// Copyright (C) 2025, uvok cheetah
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import 'package:uvok_epaper_badge/model/device.dart';
+import 'package:flutter/material.dart';
+import 'package:logger/logger.dart';
+import 'package:uvok_epaper_badge/model/device_connection.dart';
+
+var logger = Logger();
+
+class DeviceDetailsScreen extends StatefulWidget {
+ final Device device;
+ final DeviceConnection deviceConnection;
+
+ const DeviceDetailsScreen({
+ super.key,
+ required this.device,
+ required this.deviceConnection,
+ });
+
+ @override
+ State<StatefulWidget> createState() {
+ return DeviceDetailsState();
+ }
+}
+
+class DeviceDetailsState extends State<DeviceDetailsScreen> {
+ String connectStatus = "<Status>";
+
+ /// Whether the back button should be active.
+ bool backActive = false;
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ backgroundColor: Theme.of(context).colorScheme.inversePrimary,
+ title: Text("Device details"),
+ ),
+ body: Center(
+ child: Column(
+ spacing: 20,
+ children: [
+ Text(connectStatus),
+ ElevatedButton(
+ onPressed: backActive ? backClick : null,
+ child: Text("Back"),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ _doConnect();
+ }
+
+ void backClick() {
+ Navigator.pop(context);
+ }
+
+ @override
+ void deactivate() {
+ super.deactivate();
+ logger.i("Closing state");
+ // widget.device.disconnect().ignore();
+ }
+
+ void _doConnect() async {
+ final dev = widget.device;
+
+ try {
+ logger.i("Try to connect...");
+ await widget.deviceConnection.connect();
+ } catch (e) {
+ logger.e(e);
+ await widget.deviceConnection.disconnect();
+ connectStatus = e.toString();
+ } finally {
+ backActive = true;
+ if (mounted) {
+ setState(() {});
+ }
+ }
+ }
+}
diff --git a/lib/widgets/device_scan_select.dart b/lib/widgets/device_scan_select.dart
new file mode 100644
index 0000000..c89d3cf
--- /dev/null
+++ b/lib/widgets/device_scan_select.dart
@@ -0,0 +1,86 @@
+// Copyright (C) 2025, uvok cheetah
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import 'package:uvok_epaper_badge/model/device.dart';
+import 'package:flutter/material.dart';
+import 'package:uvok_epaper_badge/widgets/notifying_list_widget.dart';
+
+class DeviceScanSelection extends NotifyingListWidget<Device> {
+ const DeviceScanSelection({
+ super.key,
+ required super.items,
+ required super.onItemSelected,
+ });
+
+ @override
+ State<DeviceScanSelection> createState() => _DeviceScanSelectionState();
+}
+
+class _DeviceScanSelectionState extends State<DeviceScanSelection> {
+ int selectedResult = -1;
+
+ @override
+ Widget build(BuildContext context) {
+ return Expanded(
+ child: ListView.separated(
+ itemCount: widget.items.length,
+ itemBuilder: (context, index) {
+ if (index >= widget.items.length) return null;
+ final Device result = widget.items[index];
+ final String name = result.name ?? "???";
+
+ return ListTile(
+ title: Text(name),
+ subtitle: Text(result.address ?? "???"),
+ trailing: Text('RSSI: ${result.rssi}'),
+ selectedTileColor: Colors.amber,
+ selectedColor: Colors.black,
+ onTap: () {
+ setState(() {
+ selectedResult = index;
+ });
+ widget.onItemSelected(result);
+ },
+ selected: selectedResult == index,
+ );
+ },
+ separatorBuilder: (BuildContext context, int index) {
+ return Divider();
+ },
+ ),
+ );
+ }
+
+ @override
+ void didUpdateWidget(covariant DeviceScanSelection oldWidget) {
+ super.didUpdateWidget(oldWidget);
+
+ if (!_deviceListEqual(oldWidget.items, widget.items)) {
+ setState(() {
+ selectedResult = -1;
+ });
+ }
+ }
+
+ bool _deviceListEqual(List<Device> oldList, List<Device> newList) {
+ if (oldList.length != newList.length) return false;
+ for (int i = 0; i < oldList.length; i++) {
+ if (oldList[i].address != newList[i].address) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/lib/widgets/notifying_list_widget.dart b/lib/widgets/notifying_list_widget.dart
new file mode 100644
index 0000000..7fdc2b1
--- /dev/null
+++ b/lib/widgets/notifying_list_widget.dart
@@ -0,0 +1,27 @@
+// Copyright (C) 2025, uvok cheetah
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import 'package:flutter/material.dart';
+
+abstract class NotifyingListWidget<T> extends StatefulWidget {
+ final List<T> items;
+ final ValueChanged<T?> onItemSelected;
+
+ const NotifyingListWidget({
+ super.key,
+ required this.items,
+ required this.onItemSelected,
+ });
+}
diff --git a/lib/widgets/scan_page.dart b/lib/widgets/scan_page.dart
new file mode 100644
index 0000000..c557657
--- /dev/null
+++ b/lib/widgets/scan_page.dart
@@ -0,0 +1,142 @@
+// Copyright (C) 2025, uvok cheetah
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+import 'package:uvok_epaper_badge/control/scanner_controller.dart';
+import 'package:uvok_epaper_badge/widgets/device_details.dart';
+import 'package:uvok_epaper_badge/widgets/device_scan_select.dart';
+import 'package:uvok_epaper_badge/model/device.dart';
+import 'package:flutter/material.dart';
+import 'package:logger/logger.dart';
+import 'package:permission_handler/permission_handler.dart';
+import 'package:uvok_epaper_badge/model/device_connection.dart';
+import 'package:uvok_epaper_badge/model/device_connection_factory.dart';
+
+var logger = Logger();
+
+class ScanPage extends StatefulWidget {
+ const ScanPage({super.key, required this.title, required this.deviceScanner});
+
+ // Original doc: Fields in a Widget subclass are always marked "final".
+
+ final String title;
+ final ScannerController deviceScanner;
+
+ @override
+ State<ScanPage> createState() => _ScanPageState();
+}
+
+class _ScanPageState extends State<ScanPage> {
+ Device? selectedDevice;
+
+ void _doConnect() async {
+ final Device? dev = selectedDevice;
+ if (dev == null) return;
+
+ final DeviceConnection connection =
+ DeviceConnectionFactory.createConnection(dev);
+ //???
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) =>
+ DeviceDetailsScreen(device: dev, deviceConnection: connection),
+ ),
+ );
+ }
+
+ void _doScan() async {
+ setState(() {
+ selectedDevice = null;
+ });
+
+ // ...
+ await widget.deviceScanner.startScan();
+ }
+
+ Future getPermissions() async {
+ try {
+ await Permission.bluetooth.request();
+ } catch (e) {
+ logger.e(e.toString());
+ }
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ getPermissions();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return StreamBuilder(
+ stream: widget.deviceScanner.statusStream,
+ initialData: ScanStatus.idle,
+ builder: (context, asyncSnapshot) {
+ bool isScanning = asyncSnapshot.data == ScanStatus.scanning;
+ return Scaffold(
+ appBar: AppBar(
+ backgroundColor: Theme.of(context).colorScheme.inversePrimary,
+ title: Text(widget.title),
+ ),
+ body: Center(
+ child: Column(
+ // TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
+ // action in the IDE, or press "p" in the console), to see the
+ // wireframe for each widget.
+ mainAxisAlignment: MainAxisAlignment.center,
+ spacing: 24,
+ children: <Widget>[
+ SizedBox(height: 15),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ spacing: 15.0,
+ children: [
+ ElevatedButton(
+ onPressed: isScanning ? null : _doScan,
+ child: isScanning
+ ? Text("Scanning...")
+ : Text("Start scan"),
+ ),
+ ElevatedButton(
+ onPressed: (selectedDevice == null || isScanning)
+ ? null
+ : _doConnect,
+ child: Text("Connect"),
+ ),
+ ],
+ ),
+ Expanded(
+ child: StreamBuilder(
+ stream: widget.deviceScanner.scanResultsStream,
+ initialData: <Device>[],
+ builder: (context, asyncSnapshot) {
+ return DeviceScanSelection(
+ items: asyncSnapshot.data ?? [],
+ onItemSelected: (item) {
+ setState(() => selectedDevice = item);
+ },
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ },
+ );
+ }
+}