Channel QR Code scanner [WIP]

This commit is contained in:
Mike Schwörer 2024-10-19 22:33:08 +02:00
parent 1cf14e65a9
commit cc672d2f20
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
11 changed files with 228 additions and 11 deletions

View File

@ -17,7 +17,17 @@ run-android:
adb connect 10.10.10.177:5555
flutter pub run build_runner build
_JAVA_OPTIONS="" flutter run -d 10.10.10.177:5555
install-release:
# Install on Pixel 7a
flutter build apk --release
flutter install --release -d 35221JEHN07157
build-release:
flutter build apk --release
flutter build appbundle --release
flutter build linux --release
test:
dart analyze

View File

@ -45,5 +45,10 @@
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>This app needs camera access to scan QR codes</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs photos access to get QR code from photo library</string>
</dict>
</plist>

View File

@ -0,0 +1,75 @@
import 'package:simplecloudnotifier/models/channel.dart';
enum ScanResultMode { ChannelSubscribe, MessageSend, Channel }
abstract class ScanResult {
ScanResultMode get mode;
static ScanResult? parse(String v) {
var lines = v.split('\n');
if (lines.length == 1 && lines[0].startsWith('https://simplecloudnotifier.de?')) {
final v = Uri.tryParse(lines[0]);
if (v != null && v.queryParameters.containsKey('preset_user_id') && v.queryParameters.containsKey('preset_user_key')) {
return ScanResultMessageSend(userID: v.queryParameters['preset_user_id']!, userKey: v.queryParameters['preset_user_key']);
}
if (v != null && v.queryParameters.containsKey('preset_user_id') && v.queryParameters.containsKey('preset_user_key')) {
return ScanResultMessageSend(userID: v.queryParameters['preset_user_id']!, userKey: null);
}
}
if (lines.length == 6 && lines[0] == '@scn.channel.subscribe' && lines[1] == 'v1') {
return ScanResultChannelSubscribe(channelDisplayName: lines[2], ownerUserID: lines[3], channelID: lines[4], subscribeKey: lines[5]);
}
if (lines.length == 5 && lines[0] == '@scn.channel' && lines[1] == 'v1') {
if (lines.length != 4) return null;
return ScanResultChannel(channelDisplayName: lines[2], ownerUserID: lines[3], channelID: lines[4]);
}
return null;
}
static String createChannelQR(Channel channel) {
return '@scn.channel' + '\n' + "v1" + '\n' + channel.displayName + '\n' + channel.ownerUserID + '\n' + channel.channelID;
}
static String createChannelSubscribeQR(Channel channel, String subscribeKey) {
return '@scn.channel.subscribe' + '\n' + "v1" + '\n' + channel.displayName + '\n' + channel.ownerUserID + '\n' + channel.channelID + '\n' + subscribeKey;
}
}
class ScanResultMessageSend extends ScanResult {
final String userID;
final String? userKey;
ScanResultMessageSend({required this.userID, required this.userKey});
@override
ScanResultMode get mode => ScanResultMode.MessageSend;
}
class ScanResultChannel extends ScanResult {
final String channelDisplayName;
final String ownerUserID;
final String channelID;
ScanResultChannel({required this.channelDisplayName, required this.ownerUserID, required this.channelID});
@override
ScanResultMode get mode => ScanResultMode.Channel;
}
class ScanResultChannelSubscribe extends ScanResult {
final String channelDisplayName;
final String ownerUserID;
final String channelID;
final String subscribeKey;
ScanResultChannelSubscribe({required this.channelDisplayName, required this.ownerUserID, required this.channelID, required this.subscribeKey});
@override
ScanResultMode get mode => ScanResultMode.ChannelSubscribe;
}

View File

@ -4,6 +4,7 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/pages/channel_list/channel_scanner.dart';
import 'package:simplecloudnotifier/state/app_bar_state.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
@ -168,7 +169,7 @@ class _ChannelRootPageState extends State<ChannelRootPage> with RouteAware {
floatingActionButton: FloatingActionButton(
heroTag: 'fab_channel_list_qr',
onPressed: () {
//TODO scan qr code to subscribe channel
Navi.push(context, () => ChannelScannerPage());
},
child: const Icon(FontAwesomeIcons.qrcode),
),

View File

@ -199,7 +199,7 @@ class _ChannelListItemState extends State<ChannelListItem> {
await APIClient.deleteSubscription(acc, widget.channel.channelID, widget.subscription!.subscriptionID);
widget.onChannelListReloadTrigger.call();
widget.onSubscriptionChanged?.call(widget.channel.channelID, null);
widget.onSubscriptionChanged.call(widget.channel.channelID, null);
Toaster.success("Success", 'Unsubscribed from channel');
} catch (exc, trace) {

View File

@ -0,0 +1,117 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/models/scan_result.dart';
import 'package:simplecloudnotifier/utils/ui.dart';
class ChannelScannerPage extends StatefulWidget {
const ChannelScannerPage({super.key});
@override
State<ChannelScannerPage> createState() => _ChannelScannerPageState();
}
class _ChannelScannerPageState extends State<ChannelScannerPage> {
final MobileScannerController _controller = MobileScannerController(
formats: const [BarcodeFormat.qrCode],
);
ScanResult? scanResult = null;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return SCNScaffold(
title: "Scanner",
showSearch: false,
showShare: false,
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
child: Column(
children: [
SizedBox(height: 16),
Center(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(UI.DefaultBorderRadius),
),
clipBehavior: Clip.hardEdge,
child: SizedBox(
height: 300,
width: 300,
child: MobileScanner(
fit: BoxFit.cover,
controller: _controller,
onDetect: _handleBarcode,
),
),
),
),
SizedBox(height: 16),
_buildScanResult(context),
],
),
),
),
);
}
void _handleBarcode(BarcodeCapture barcodes) {
setState(() {
if (barcodes.barcodes.isEmpty) {
scanResult = null;
} else {
print('parsed: ${barcodes.barcodes[0].rawValue}');
scanResult = ScanResult.parse(barcodes.barcodes[0].rawValue ?? '');
}
});
}
Widget _buildScanResult(BuildContext context) {
if (scanResult == null) {
return UI.box(
padding: EdgeInsets.fromLTRB(16, 2, 4, 2), //TODO
context: context,
child: Center(
child: Icon(FontAwesomeIcons.solidEmptySet, size: 64, color: Theme.of(context).colorScheme.onPrimaryContainer.withAlpha(128)),
),
);
}
if (scanResult! is ScanResultMessageSend) {
return UI.box(
padding: EdgeInsets.fromLTRB(16, 2, 4, 2),
context: context,
child: Text("TODO -- ScanResultMessageSend"), //TODO
);
}
if (scanResult! is ScanResultChannel) {
return UI.box(
padding: EdgeInsets.fromLTRB(16, 2, 4, 2),
context: context,
child: Text("TODO -- ScanResultChannel"), //TODO
);
}
if (scanResult! is ScanResultChannelSubscribe) {
return UI.box(
padding: EdgeInsets.fromLTRB(16, 2, 4, 2),
context: context,
child: Text("TODO -- ScanResultChannelSubscribe"), //TODO
);
}
return UI.box(
padding: EdgeInsets.fromLTRB(16, 2, 4, 2),
context: context,
child: Text("TODO -- ERROR"), //TODO
);
}
}

View File

@ -5,6 +5,7 @@ import 'package:share_plus/share_plus.dart';
import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/models/scan_result.dart';
import 'package:simplecloudnotifier/models/subscription.dart';
import 'package:simplecloudnotifier/models/user.dart';
import 'package:simplecloudnotifier/pages/channel_message_view/channel_message_view.dart';
@ -295,8 +296,8 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
return FutureBuilder(
future: _futureSubscribeKey.future,
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data != null) {
var text = '@scn.channel.subscribe' + '\n' + "v1" + '\n' + channel!.displayName + '\n' + channel!.ownerUserID + '\n' + channel!.channelID + '\n' + snapshot.data!;
if (snapshot.hasData) {
final text = (snapshot.data == null) ? ScanResult.createChannelQR(channel!) : ScanResult.createChannelSubscribeQR(channel!, snapshot.data!);
return GestureDetector(
onTap: () {
Share.share(text, subject: _displayNameOverride ?? channel!.displayName);
@ -317,12 +318,6 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
),
),
);
} else if (snapshot.hasData && snapshot.data == null) {
return const SizedBox(
width: 300.0,
height: 300.0,
child: Center(child: Icon(FontAwesomeIcons.solidSnake, size: 64)),
);
} else {
return const SizedBox(
width: 300.0,

View File

@ -72,6 +72,9 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
_channels = SCNDataCache().getChannelMap();
//TODO this is not 100% correct - the message-cache contains (which is right!) all messages, even from unsubscribed channels
//TODO what we should do is save another list in SCNDataCache, with the result of the last getMessageList call (page-1) and use that
//TODO this way we only get 1 page of data from cache, but its a weird behaviour anway that we loose data once _backgroundRefresh is finished
_pagingController.value = PagingState(nextPageKey: null, itemList: SCNDataCache().getMessagesSorted(), error: null);
_backgroundRefresh(true);

View File

@ -9,6 +9,7 @@ import device_info_plus
import firebase_core
import firebase_messaging
import flutter_local_notifications
import mobile_scanner
import package_info_plus
import path_provider_foundation
import share_plus
@ -20,6 +21,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))

View File

@ -612,6 +612,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.6"
mobile_scanner:
dependency: "direct main"
description:
name: mobile_scanner
sha256: e93461298494a3e5475dd2b41068012823b8fe2caf8d47ba545faca2aa3767d6
url: "https://pub.dev"
source: hosted
version: "6.0.1"
nested:
dependency: transitive
description:

View File

@ -37,6 +37,7 @@ dependencies:
path: any
mobile_scanner: ^6.0.1
dependency_overrides:
font_awesome_flutter:
path: deps/font_awesome_flutter