diff --git a/flutter/Makefile b/flutter/Makefile index 67b2a5a..2785d5a 100644 --- a/flutter/Makefile +++ b/flutter/Makefile @@ -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 diff --git a/flutter/ios/Runner/Info.plist b/flutter/ios/Runner/Info.plist index 2bfd360..be61666 100644 --- a/flutter/ios/Runner/Info.plist +++ b/flutter/ios/Runner/Info.plist @@ -45,5 +45,10 @@ UIApplicationSupportsIndirectInputEvents + NSCameraUsageDescription + This app needs camera access to scan QR codes + + NSPhotoLibraryUsageDescription + This app needs photos access to get QR code from photo library diff --git a/flutter/lib/models/scan_result.dart b/flutter/lib/models/scan_result.dart new file mode 100644 index 0000000..afc6abb --- /dev/null +++ b/flutter/lib/models/scan_result.dart @@ -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; +} diff --git a/flutter/lib/pages/channel_list/channel_list.dart b/flutter/lib/pages/channel_list/channel_list.dart index 93d075e..3b19b55 100644 --- a/flutter/lib/pages/channel_list/channel_list.dart +++ b/flutter/lib/pages/channel_list/channel_list.dart @@ -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 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), ), diff --git a/flutter/lib/pages/channel_list/channel_list_item.dart b/flutter/lib/pages/channel_list/channel_list_item.dart index 731fc11..61a10fb 100644 --- a/flutter/lib/pages/channel_list/channel_list_item.dart +++ b/flutter/lib/pages/channel_list/channel_list_item.dart @@ -199,7 +199,7 @@ class _ChannelListItemState extends State { 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) { diff --git a/flutter/lib/pages/channel_list/channel_scanner.dart b/flutter/lib/pages/channel_list/channel_scanner.dart new file mode 100644 index 0000000..126eefb --- /dev/null +++ b/flutter/lib/pages/channel_list/channel_scanner.dart @@ -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 createState() => _ChannelScannerPageState(); +} + +class _ChannelScannerPageState extends State { + 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 + ); + } +} diff --git a/flutter/lib/pages/channel_view/channel_view.dart b/flutter/lib/pages/channel_view/channel_view.dart index cd397b4..2680bbf 100644 --- a/flutter/lib/pages/channel_view/channel_view.dart +++ b/flutter/lib/pages/channel_view/channel_view.dart @@ -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 { 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 { ), ), ); - } 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, diff --git a/flutter/lib/pages/message_list/message_list.dart b/flutter/lib/pages/message_list/message_list.dart index 708c521..2126611 100644 --- a/flutter/lib/pages/message_list/message_list.dart +++ b/flutter/lib/pages/message_list/message_list.dart @@ -72,6 +72,9 @@ class _MessageListPageState extends State 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); diff --git a/flutter/macos/Flutter/GeneratedPluginRegistrant.swift b/flutter/macos/Flutter/GeneratedPluginRegistrant.swift index 04d1315..1be595f 100644 --- a/flutter/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/flutter/macos/Flutter/GeneratedPluginRegistrant.swift @@ -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")) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index d51d959..8a79670 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -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: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index c6eb1bb..878d350 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -37,6 +37,7 @@ dependencies: path: any + mobile_scanner: ^6.0.1 dependency_overrides: font_awesome_flutter: path: deps/font_awesome_flutter