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