Channel QR Code scanner [WIP]
This commit is contained in:
parent
1cf14e65a9
commit
cc672d2f20
@ -18,6 +18,16 @@ run-android:
|
|||||||
flutter pub run build_runner build
|
flutter pub run build_runner build
|
||||||
_JAVA_OPTIONS="" flutter run -d 10.10.10.177:5555
|
_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:
|
test:
|
||||||
dart analyze
|
dart analyze
|
||||||
|
|
||||||
|
@ -45,5 +45,10 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<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>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
75
flutter/lib/models/scan_result.dart
Normal file
75
flutter/lib/models/scan_result.dart
Normal 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;
|
||||||
|
}
|
@ -4,6 +4,7 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||||
import 'package:simplecloudnotifier/models/channel.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/app_bar_state.dart';
|
||||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||||
@ -168,7 +169,7 @@ class _ChannelRootPageState extends State<ChannelRootPage> with RouteAware {
|
|||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
heroTag: 'fab_channel_list_qr',
|
heroTag: 'fab_channel_list_qr',
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
//TODO scan qr code to subscribe channel
|
Navi.push(context, () => ChannelScannerPage());
|
||||||
},
|
},
|
||||||
child: const Icon(FontAwesomeIcons.qrcode),
|
child: const Icon(FontAwesomeIcons.qrcode),
|
||||||
),
|
),
|
||||||
|
@ -199,7 +199,7 @@ class _ChannelListItemState extends State<ChannelListItem> {
|
|||||||
await APIClient.deleteSubscription(acc, widget.channel.channelID, widget.subscription!.subscriptionID);
|
await APIClient.deleteSubscription(acc, widget.channel.channelID, widget.subscription!.subscriptionID);
|
||||||
widget.onChannelListReloadTrigger.call();
|
widget.onChannelListReloadTrigger.call();
|
||||||
|
|
||||||
widget.onSubscriptionChanged?.call(widget.channel.channelID, null);
|
widget.onSubscriptionChanged.call(widget.channel.channelID, null);
|
||||||
|
|
||||||
Toaster.success("Success", 'Unsubscribed from channel');
|
Toaster.success("Success", 'Unsubscribed from channel');
|
||||||
} catch (exc, trace) {
|
} catch (exc, trace) {
|
||||||
|
117
flutter/lib/pages/channel_list/channel_scanner.dart
Normal file
117
flutter/lib/pages/channel_list/channel_scanner.dart
Normal 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@ import 'package:share_plus/share_plus.dart';
|
|||||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||||
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
|
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
|
||||||
import 'package:simplecloudnotifier/models/channel.dart';
|
import 'package:simplecloudnotifier/models/channel.dart';
|
||||||
|
import 'package:simplecloudnotifier/models/scan_result.dart';
|
||||||
import 'package:simplecloudnotifier/models/subscription.dart';
|
import 'package:simplecloudnotifier/models/subscription.dart';
|
||||||
import 'package:simplecloudnotifier/models/user.dart';
|
import 'package:simplecloudnotifier/models/user.dart';
|
||||||
import 'package:simplecloudnotifier/pages/channel_message_view/channel_message_view.dart';
|
import 'package:simplecloudnotifier/pages/channel_message_view/channel_message_view.dart';
|
||||||
@ -295,8 +296,8 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
|||||||
return FutureBuilder(
|
return FutureBuilder(
|
||||||
future: _futureSubscribeKey.future,
|
future: _futureSubscribeKey.future,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.hasData && snapshot.data != null) {
|
if (snapshot.hasData) {
|
||||||
var text = '@scn.channel.subscribe' + '\n' + "v1" + '\n' + channel!.displayName + '\n' + channel!.ownerUserID + '\n' + channel!.channelID + '\n' + snapshot.data!;
|
final text = (snapshot.data == null) ? ScanResult.createChannelQR(channel!) : ScanResult.createChannelSubscribeQR(channel!, snapshot.data!);
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Share.share(text, subject: _displayNameOverride ?? channel!.displayName);
|
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 {
|
} else {
|
||||||
return const SizedBox(
|
return const SizedBox(
|
||||||
width: 300.0,
|
width: 300.0,
|
||||||
|
@ -72,6 +72,9 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
|
|||||||
|
|
||||||
_channels = SCNDataCache().getChannelMap();
|
_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);
|
_pagingController.value = PagingState(nextPageKey: null, itemList: SCNDataCache().getMessagesSorted(), error: null);
|
||||||
|
|
||||||
_backgroundRefresh(true);
|
_backgroundRefresh(true);
|
||||||
|
@ -9,6 +9,7 @@ import device_info_plus
|
|||||||
import firebase_core
|
import firebase_core
|
||||||
import firebase_messaging
|
import firebase_messaging
|
||||||
import flutter_local_notifications
|
import flutter_local_notifications
|
||||||
|
import mobile_scanner
|
||||||
import package_info_plus
|
import package_info_plus
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import share_plus
|
import share_plus
|
||||||
@ -20,6 +21,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||||
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
|
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
|
||||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||||
|
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
|
||||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||||
|
@ -612,6 +612,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.6"
|
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:
|
nested:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -37,6 +37,7 @@ dependencies:
|
|||||||
|
|
||||||
|
|
||||||
path: any
|
path: any
|
||||||
|
mobile_scanner: ^6.0.1
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
font_awesome_flutter:
|
font_awesome_flutter:
|
||||||
path: deps/font_awesome_flutter
|
path: deps/font_awesome_flutter
|
||||||
|
Loading…
Reference in New Issue
Block a user