Implement Scanner-View
This commit is contained in:
parent
c0b8a8a3f4
commit
95353735b0
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
.aider*
|
@ -14,9 +14,9 @@ run-linux:
|
|||||||
_JAVA_OPTIONS="" flutter run -d linux
|
_JAVA_OPTIONS="" flutter run -d linux
|
||||||
|
|
||||||
# runs app locally (web | not really supported)
|
# runs app locally (web | not really supported)
|
||||||
run-linux:
|
run-web:
|
||||||
dart run build_runner build
|
dart run build_runner build
|
||||||
_JAVA_OPTIONS="" flutter run -d web
|
_JAVA_OPTIONS="" flutter run -d chrome
|
||||||
|
|
||||||
# runs on android device (must have network adb enabled teh correct IP)
|
# runs on android device (must have network adb enabled teh correct IP)
|
||||||
run-android:
|
run-android:
|
||||||
@ -42,7 +42,7 @@ fix:
|
|||||||
dart fix --apply
|
dart fix --apply
|
||||||
|
|
||||||
gen:
|
gen:
|
||||||
flutter pub run build_runner build
|
dart run build_runner build
|
||||||
|
|
||||||
# run `make run` in another terminal (or another variant of flutter run)
|
# run `make run` in another terminal (or another variant of flutter run)
|
||||||
autoreload:
|
autoreload:
|
||||||
@ -64,3 +64,6 @@ upgrade:
|
|||||||
flutter upgrade
|
flutter upgrade
|
||||||
flutter pub upgrade
|
flutter pub upgrade
|
||||||
flutter doctor
|
flutter doctor
|
||||||
|
|
||||||
|
aider:
|
||||||
|
aider --model gemini-2.5-pro --no-auto-commits --no-dirty-commits --test-cmd "flutter build linux" --auto-test --subtree-only
|
@ -1,17 +1,17 @@
|
|||||||
|
|
||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
- [ ] Message List
|
- [x] Message List
|
||||||
* [ ] CRUD
|
* [ ] CRUD
|
||||||
- [ ] Message Big-View
|
- [x] Message Big-View
|
||||||
- [ ] Search/Filter Messages
|
- [x] Search/Filter Messages
|
||||||
- [ ] Channel List
|
- [x] Channel List
|
||||||
* [ ] Show subs
|
* [x] Show subs
|
||||||
* [ ] CRUD
|
* [ ] CRUD
|
||||||
* [ ] what about unsubbed foreign channels? - thex should still be visible (or should they, do i still get the messages?)
|
* [ ] what about unsubbed foreign channels? - thex should still be visible (or should they, do i still get the messages?)
|
||||||
- [ ] Sub List
|
- [x] Sub List
|
||||||
* [ ] Sub/Unsub/Accept/Deny
|
* [x] Sub/Unsub/Accept/Deny
|
||||||
- [ ] Debug List (Show logs, requests)
|
- [x] Debug List (Show logs, requests)
|
||||||
- [ ] Key List
|
- [ ] Key List
|
||||||
* [ ] CRUD
|
* [ ] CRUD
|
||||||
- [ ] Auto R-only key for admin, use for QR+link+send
|
- [ ] Auto R-only key for admin, use for QR+link+send
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
# shellcheck disable=SC2002 # disable useless-cat warning
|
# shellcheck disable=SC2002 # disable useless-cat warning
|
||||||
|
|
||||||
set -o nounset # disallow usage of unset vars ( set -u )
|
set -o nounset # disallow usage of unset vars ( set -u )
|
||||||
set -o errexit # Exit immediately if a pipeline returns non-zero. ( set -e )
|
#set -o errexit # Exit immediately if a pipeline returns non-zero. ( set -e )
|
||||||
set -o errtrace # Allow the above trap be inherited by all functions in the script. ( set -E )
|
#set -o errtrace # Allow the above trap be inherited by all functions in the script. ( set -E )
|
||||||
set -o pipefail # Return value of a pipeline is the value of the last (rightmost) command to exit with a non-zero status
|
set -o pipefail # Return value of a pipeline is the value of the last (rightmost) command to exit with a non-zero status
|
||||||
IFS=$'\n\t' # Set $IFS to only newline and tab.
|
IFS=$'\n\t' # Set $IFS to only newline and tab.
|
||||||
|
|
||||||
@ -24,9 +24,9 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
pid="$( pgrep -f 'flutter_tools\.[s]napshot run' || echo '' | tail -n 1 )"
|
pids="$( pgrep -f 'flutter_tools\.[s]napshot run' || echo '' )"
|
||||||
|
|
||||||
if [ -z "$pid" ]; then
|
if [ -z "$pids" ]; then
|
||||||
red "No [flutter run] process found - exiting"
|
red "No [flutter run] process found - exiting"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@ -37,10 +37,21 @@ trap 'echo "reseived SIGNAL<SIGTERM> - exiting"; exit 0' SIGTERM
|
|||||||
trap 'echo "reseived SIGNAL<SIGQUIT> - exiting"; exit 0' SIGQUIT
|
trap 'echo "reseived SIGNAL<SIGQUIT> - exiting"; exit 0' SIGQUIT
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
while IFS= read -r pid; do
|
||||||
blue "Listening for changes in lib/ directory - sending signals to ${pid}..."
|
blue "Listening for changes in lib/ directory - sending signals to ${pid}..."
|
||||||
|
done <<< "$pids"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
while IFS= read -r pid; do
|
||||||
|
{
|
||||||
while true; do
|
while true; do
|
||||||
find lib/ -name '*.dart' | entr -d -p sh -c "echo 'File(s) changed - Sending SIGUSR to $pid' ; kill -USR1 $pid";
|
find lib/ -name '*.dart' | entr -d -p sh -c "echo 'File(s) changed - Sending SIGUSR to $pid' ; kill -USR1 $pid";
|
||||||
yellow 'File list changed - restart';
|
yellow 'File list changed - restart';
|
||||||
done
|
done
|
||||||
|
} &
|
||||||
|
done <<< "$pids"
|
||||||
|
|
||||||
|
wait # wait for all background jobs to finish
|
||||||
|
|
||||||
|
echo "DONE."
|
@ -5,6 +5,7 @@ import 'package:simplecloudnotifier/api/api_exception.dart';
|
|||||||
import 'package:simplecloudnotifier/models/api_error.dart';
|
import 'package:simplecloudnotifier/models/api_error.dart';
|
||||||
import 'package:simplecloudnotifier/models/client.dart';
|
import 'package:simplecloudnotifier/models/client.dart';
|
||||||
import 'package:simplecloudnotifier/models/keytoken.dart';
|
import 'package:simplecloudnotifier/models/keytoken.dart';
|
||||||
|
import 'package:simplecloudnotifier/models/send_message_response.dart';
|
||||||
import 'package:simplecloudnotifier/models/sender_name_statistics.dart';
|
import 'package:simplecloudnotifier/models/sender_name_statistics.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';
|
||||||
@ -50,7 +51,8 @@ class MessageFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class APIClient {
|
class APIClient {
|
||||||
static const String _base = 'https://simplecloudnotifier.de/api/v2';
|
static const String _base = 'https://simplecloudnotifier.de';
|
||||||
|
static const String _prefix = '/api/v2';
|
||||||
|
|
||||||
static Future<T> _request<T>({
|
static Future<T> _request<T>({
|
||||||
required String name,
|
required String name,
|
||||||
@ -61,10 +63,11 @@ class APIClient {
|
|||||||
dynamic jsonBody,
|
dynamic jsonBody,
|
||||||
String? authToken,
|
String? authToken,
|
||||||
Map<String, String>? header,
|
Map<String, String>? header,
|
||||||
|
bool? nonAPI,
|
||||||
}) async {
|
}) async {
|
||||||
final t0 = DateTime.now();
|
final t0 = DateTime.now();
|
||||||
|
|
||||||
final uri = Uri.parse('$_base/$relURL').replace(queryParameters: query ?? {});
|
final uri = Uri.parse('$_base${(nonAPI ?? false) ? '' : _prefix}/$relURL').replace(queryParameters: query ?? {});
|
||||||
|
|
||||||
final req = http.Request(method, uri);
|
final req = http.Request(method, uri);
|
||||||
|
|
||||||
@ -380,9 +383,9 @@ class APIClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<KeyTokenPreview> getKeyTokenPreview(TokenSource auth, String kid) async {
|
static Future<KeyTokenPreview> getKeyTokenPreviewByID(TokenSource auth, String kid) async {
|
||||||
return await _request(
|
return await _request(
|
||||||
name: 'getKeyTokenPreview',
|
name: 'getKeyTokenPreviewByID',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
relURL: 'preview/keys/$kid',
|
relURL: 'preview/keys/$kid',
|
||||||
fn: KeyTokenPreview.fromJson,
|
fn: KeyTokenPreview.fromJson,
|
||||||
@ -390,6 +393,16 @@ class APIClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<KeyTokenPreview> getKeyTokenPreviewByToken(TokenSource auth, String tok) async {
|
||||||
|
return await _request(
|
||||||
|
name: 'getKeyTokenPreviewByToken',
|
||||||
|
method: 'GET',
|
||||||
|
relURL: 'preview/keys/$tok',
|
||||||
|
fn: KeyTokenPreview.fromJson,
|
||||||
|
authToken: auth.getToken(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
static Future<KeyToken> getKeyTokenByToken(String userid, String token) async {
|
static Future<KeyToken> getKeyTokenByToken(String userid, String token) async {
|
||||||
return await _request(
|
return await _request(
|
||||||
name: 'getCurrentKeyToken',
|
name: 'getCurrentKeyToken',
|
||||||
@ -410,11 +423,14 @@ class APIClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Subscription> subscribeToChannelbyID(TokenSource auth, String channelID) async {
|
static Future<Subscription> subscribeToChannelbyID(TokenSource auth, String channelID, {String? subscribeKey}) async {
|
||||||
return await _request(
|
return await _request(
|
||||||
name: 'subscribeToChannelbyID',
|
name: 'subscribeToChannelbyID',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
relURL: 'users/${auth.getUserID()}/subscriptions',
|
relURL: 'users/${auth.getUserID()}/subscriptions',
|
||||||
|
query: {
|
||||||
|
if (subscribeKey != null) 'chan_subscribe_key': [subscribeKey],
|
||||||
|
},
|
||||||
jsonBody: {
|
jsonBody: {
|
||||||
'channel_id': channelID,
|
'channel_id': channelID,
|
||||||
},
|
},
|
||||||
@ -458,4 +474,26 @@ class APIClient {
|
|||||||
authToken: auth.getToken(),
|
authToken: auth.getToken(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<SendMessageResponse> sendMessage(String userid, String keytoken, String text, {String? channel, String? content, String? messageID, int? priority, String? senderName, DateTime? timestamp}) async {
|
||||||
|
return await _request(
|
||||||
|
name: 'sendMessage',
|
||||||
|
method: 'POST',
|
||||||
|
relURL: '/send',
|
||||||
|
nonAPI: true,
|
||||||
|
jsonBody: {
|
||||||
|
'user_id': userid,
|
||||||
|
'key': keytoken,
|
||||||
|
'title': text,
|
||||||
|
if (channel != null) 'channel': channel,
|
||||||
|
if (content != null) 'content': content,
|
||||||
|
if (priority != null) 'priority': priority,
|
||||||
|
if (messageID != null) 'msg_id': messageID,
|
||||||
|
if (timestamp != null) 'timestamp': (timestamp.microsecondsSinceEpoch / 1000).toInt(),
|
||||||
|
if (senderName != null) 'sender_name': senderName,
|
||||||
|
},
|
||||||
|
fn: SendMessageResponse.fromJson,
|
||||||
|
authToken: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,13 +71,15 @@ class Channel extends HiveObject implements FieldDebuggable {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
ChannelPreview toPreview() {
|
ChannelPreview toPreview(Subscription? sub) {
|
||||||
return ChannelPreview(
|
return ChannelPreview(
|
||||||
channelID: this.channelID,
|
channelID: this.channelID,
|
||||||
ownerUserID: this.ownerUserID,
|
ownerUserID: this.ownerUserID,
|
||||||
internalName: this.internalName,
|
internalName: this.internalName,
|
||||||
displayName: this.displayName,
|
displayName: this.displayName,
|
||||||
descriptionName: this.descriptionName,
|
descriptionName: this.descriptionName,
|
||||||
|
messagesSent: this.messagesSent,
|
||||||
|
subscription: sub,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -109,6 +111,8 @@ class ChannelPreview {
|
|||||||
final String internalName;
|
final String internalName;
|
||||||
final String displayName;
|
final String displayName;
|
||||||
final String? descriptionName;
|
final String? descriptionName;
|
||||||
|
final int messagesSent;
|
||||||
|
final Subscription? subscription;
|
||||||
|
|
||||||
const ChannelPreview({
|
const ChannelPreview({
|
||||||
required this.channelID,
|
required this.channelID,
|
||||||
@ -116,6 +120,8 @@ class ChannelPreview {
|
|||||||
required this.internalName,
|
required this.internalName,
|
||||||
required this.displayName,
|
required this.displayName,
|
||||||
required this.descriptionName,
|
required this.descriptionName,
|
||||||
|
required this.messagesSent,
|
||||||
|
required this.subscription,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory ChannelPreview.fromJson(Map<String, dynamic> json) {
|
factory ChannelPreview.fromJson(Map<String, dynamic> json) {
|
||||||
@ -125,6 +131,8 @@ class ChannelPreview {
|
|||||||
internalName: json['internal_name'] as String,
|
internalName: json['internal_name'] as String,
|
||||||
displayName: json['display_name'] as String,
|
displayName: json['display_name'] as String,
|
||||||
descriptionName: json['description_name'] as String?,
|
descriptionName: json['description_name'] as String?,
|
||||||
|
messagesSent: json['messages_sent'] as int,
|
||||||
|
subscription: json['subscription'] == null ? null : Subscription.fromJson(json['subscription'] as Map<String, dynamic>),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import 'package:simplecloudnotifier/models/channel.dart';
|
import 'package:simplecloudnotifier/models/channel.dart';
|
||||||
|
|
||||||
enum ScanResultMode { ChannelSubscribe, MessageSend, Channel }
|
enum ScanResultMode { ChannelSubscribe, MessageSend, Channel, Error }
|
||||||
|
|
||||||
abstract class ScanResult {
|
abstract class ScanResult {
|
||||||
ScanResultMode get mode;
|
ScanResultMode get mode;
|
||||||
@ -12,10 +12,10 @@ abstract class ScanResult {
|
|||||||
final v = Uri.tryParse(lines[0]);
|
final v = Uri.tryParse(lines[0]);
|
||||||
|
|
||||||
if (v != null && v.queryParameters.containsKey('preset_user_id') && v.queryParameters.containsKey('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: v.queryParameters['preset_user_key']);
|
return ScanResultMessageSend(userID: v.queryParameters['preset_user_id']!, userKey: v.queryParameters['preset_user_key'], url: lines[0]);
|
||||||
}
|
}
|
||||||
if (v != null && v.queryParameters.containsKey('preset_user_id') && v.queryParameters.containsKey('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);
|
return ScanResultMessageSend(userID: v.queryParameters['preset_user_id']!, userKey: null, url: lines[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,12 +24,12 @@ abstract class ScanResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (lines.length == 5 && lines[0] == '@scn.channel' && lines[1] == 'v1') {
|
if (lines.length == 5 && lines[0] == '@scn.channel' && lines[1] == 'v1') {
|
||||||
if (lines.length != 4) return null;
|
if (lines.length != 5) return null;
|
||||||
|
|
||||||
return ScanResultChannel(channelDisplayName: lines[2], ownerUserID: lines[3], channelID: lines[4]);
|
return ScanResultChannel(channelDisplayName: lines[2], ownerUserID: lines[3], channelID: lines[4]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return ScanResultError(message: 'Invalid QR code');
|
||||||
}
|
}
|
||||||
|
|
||||||
static String createChannelQR(Channel channel) {
|
static String createChannelQR(Channel channel) {
|
||||||
@ -44,8 +44,9 @@ abstract class ScanResult {
|
|||||||
class ScanResultMessageSend extends ScanResult {
|
class ScanResultMessageSend extends ScanResult {
|
||||||
final String userID;
|
final String userID;
|
||||||
final String? userKey;
|
final String? userKey;
|
||||||
|
final String url;
|
||||||
|
|
||||||
ScanResultMessageSend({required this.userID, required this.userKey});
|
ScanResultMessageSend({required this.userID, required this.userKey, required this.url});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ScanResultMode get mode => ScanResultMode.MessageSend;
|
ScanResultMode get mode => ScanResultMode.MessageSend;
|
||||||
@ -73,3 +74,12 @@ class ScanResultChannelSubscribe extends ScanResult {
|
|||||||
@override
|
@override
|
||||||
ScanResultMode get mode => ScanResultMode.ChannelSubscribe;
|
ScanResultMode get mode => ScanResultMode.ChannelSubscribe;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ScanResultError extends ScanResult {
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
ScanResultError({required this.message});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ScanResultMode get mode => ScanResultMode.Error;
|
||||||
|
}
|
||||||
|
55
flutter/lib/models/send_message_response.dart
Normal file
55
flutter/lib/models/send_message_response.dart
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
class SendMessageResponse {
|
||||||
|
final bool success;
|
||||||
|
final int errorID;
|
||||||
|
final int errorHighlight;
|
||||||
|
final String message;
|
||||||
|
final bool suppressSend;
|
||||||
|
final int messageCount;
|
||||||
|
final int quota;
|
||||||
|
final bool isPro;
|
||||||
|
final int quotaMax;
|
||||||
|
final String scnMessageID;
|
||||||
|
|
||||||
|
SendMessageResponse({
|
||||||
|
required this.success,
|
||||||
|
required this.errorID,
|
||||||
|
required this.errorHighlight,
|
||||||
|
required this.message,
|
||||||
|
required this.suppressSend,
|
||||||
|
required this.messageCount,
|
||||||
|
required this.quota,
|
||||||
|
required this.isPro,
|
||||||
|
required this.quotaMax,
|
||||||
|
required this.scnMessageID,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory SendMessageResponse.fromJson(Map<String, dynamic> json) {
|
||||||
|
return SendMessageResponse(
|
||||||
|
success: json['success'] as bool,
|
||||||
|
errorID: json['error'] as int,
|
||||||
|
errorHighlight: json['errhighlight'] as int,
|
||||||
|
message: json['message'] as String,
|
||||||
|
suppressSend: json['suppress_send'] as bool,
|
||||||
|
messageCount: json['messagecount'] as int,
|
||||||
|
quota: json['quota'] as int,
|
||||||
|
isPro: json['is_pro'] as bool,
|
||||||
|
quotaMax: json['quota_max'] as int,
|
||||||
|
scnMessageID: json['scn_msg_id'] as String,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'success': success,
|
||||||
|
'error': errorID,
|
||||||
|
'errhighlight': errorHighlight,
|
||||||
|
'message': message,
|
||||||
|
'suppress_send': suppressSend,
|
||||||
|
'messagecount': messageCount,
|
||||||
|
'quota': quota,
|
||||||
|
'is_pro': isPro,
|
||||||
|
'quota_max': quotaMax,
|
||||||
|
'scn_msg_id': scnMessageID,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +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/pages/channel_scanner/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';
|
||||||
|
@ -1,117 +0,0 @@
|
|||||||
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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
159
flutter/lib/pages/channel_scanner/channel_scanner.dart
Normal file
159
flutter/lib/pages/channel_scanner/channel_scanner.dart
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
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/pages/channel_scanner/channel_scanner_result_channelsubscribe.dart';
|
||||||
|
import 'package:simplecloudnotifier/pages/channel_scanner/channel_scanner_result_channelview.dart';
|
||||||
|
import 'package:simplecloudnotifier/pages/channel_scanner/channel_scanner_result_messagesend.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),
|
||||||
|
if (scanResult == null) ...[
|
||||||
|
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),
|
||||||
|
],
|
||||||
|
Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(maxWidth: 300, minWidth: 300, minHeight: 200),
|
||||||
|
child: _buildScanResult(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleBarcode(BarcodeCapture barcodes) {
|
||||||
|
setState(() {
|
||||||
|
if (barcodes.barcodes.isEmpty) {
|
||||||
|
scanResult = null;
|
||||||
|
} else {
|
||||||
|
scanResult = ScanResult.parse(barcodes.barcodes[0].rawValue ?? '');
|
||||||
|
print('parsed: ${jsonEncode(barcodes.barcodes[0].rawValue)} as ${scanResult.runtimeType.toString()}');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildScanResult(BuildContext context) {
|
||||||
|
if (scanResult == null) {
|
||||||
|
return UI.box(
|
||||||
|
padding: EdgeInsets.fromLTRB(16, 32, 16, 8),
|
||||||
|
context: context,
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
spacing: 32,
|
||||||
|
children: [
|
||||||
|
Icon(FontAwesomeIcons.solidEmptySet, size: 64, color: Theme.of(context).colorScheme.onPrimaryContainer.withAlpha(48)),
|
||||||
|
Text("Please scan a Channel QR Code to subscribe to it", style: TextStyle(fontStyle: FontStyle.italic), textAlign: TextAlign.center),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scanResult! is ScanResultMessageSend) {
|
||||||
|
return UI.box(
|
||||||
|
padding: EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||||
|
context: context,
|
||||||
|
child: ChannelScannerResultMessageSend(value: scanResult! as ScanResultMessageSend),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scanResult! is ScanResultChannel) {
|
||||||
|
return UI.box(
|
||||||
|
padding: EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||||
|
context: context,
|
||||||
|
child: ChannelScannerResultChannelView(value: scanResult! as ScanResultChannel),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scanResult! is ScanResultChannelSubscribe) {
|
||||||
|
return UI.box(
|
||||||
|
padding: EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||||
|
context: context,
|
||||||
|
child: ChannelScannerResultChannelSubscribe(value: scanResult! as ScanResultChannelSubscribe),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scanResult! is ScanResultError) {
|
||||||
|
return UI.box(
|
||||||
|
padding: EdgeInsets.fromLTRB(16, 32, 16, 8),
|
||||||
|
context: context,
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
spacing: 32,
|
||||||
|
children: [
|
||||||
|
Icon(FontAwesomeIcons.solidTriangleExclamation, size: 64, color: Colors.red[900]),
|
||||||
|
Text((scanResult! as ScanResultError).message, textAlign: TextAlign.center),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return UI.box(
|
||||||
|
padding: EdgeInsets.fromLTRB(16, 32, 16, 8),
|
||||||
|
context: context,
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
spacing: 32,
|
||||||
|
children: [
|
||||||
|
Icon(FontAwesomeIcons.solidTriangleExclamation, size: 64, color: Colors.red[900]),
|
||||||
|
Text("Please scan a Channel QR Code to subscribe to it", style: TextStyle(fontStyle: FontStyle.italic), textAlign: TextAlign.center),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,198 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:simplecloudnotifier/api/api_client.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_view/channel_view.dart';
|
||||||
|
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||||
|
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||||
|
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||||
|
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||||
|
import 'package:simplecloudnotifier/utils/ui.dart';
|
||||||
|
|
||||||
|
class ChannelScannerResultChannelSubscribe extends StatefulWidget {
|
||||||
|
final ScanResultChannelSubscribe value;
|
||||||
|
|
||||||
|
const ChannelScannerResultChannelSubscribe({required this.value}) : super();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ChannelScannerResultChannelSubscribe> createState() => _ChannelScannerResultChannelSubscribeState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChannelScannerResultChannelSubscribeState extends State<ChannelScannerResultChannelSubscribe> {
|
||||||
|
Future<(ChannelPreview, UserPreview)?> _fetchDataFuture;
|
||||||
|
|
||||||
|
_ChannelScannerResultChannelSubscribeState() : _fetchDataFuture = Future.value(null); // Initial dummy future
|
||||||
|
|
||||||
|
Subscription? overrideSubscription = null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
final auth = Provider.of<AppAuth>(context, listen: false);
|
||||||
|
setState(() {
|
||||||
|
_fetchDataFuture = _fetchData(auth);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<(ChannelPreview, UserPreview)?> _fetchData(AppAuth auth) async {
|
||||||
|
ChannelPreview? channel = null;
|
||||||
|
try {
|
||||||
|
channel = await APIClient.getChannelPreview(auth, widget.value.channelID);
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
Toaster.error("Error", 'Failed to fetch channel preview: ${e.toString()}');
|
||||||
|
ApplicationLog.error('Failed to fetch channel (preview) for ${widget.value.channelID}', trace: stackTrace);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
UserPreview? user = null;
|
||||||
|
try {
|
||||||
|
user = await APIClient.getUserPreview(auth, widget.value.ownerUserID);
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
Toaster.error("Error", 'Failed to fetch user preview: ${e.toString()}');
|
||||||
|
ApplicationLog.error('Failed to fetch user (preview) for ${widget.value.ownerUserID}', trace: stackTrace);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (channel, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FutureBuilder<(ChannelPreview, UserPreview)?>(
|
||||||
|
future: _fetchDataFuture,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final auth = Provider.of<AppAuth>(context, listen: false);
|
||||||
|
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.hasError) {
|
||||||
|
return Text('Error: ${snapshot.error}'); //TODO better error display
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.data == null) {
|
||||||
|
return Column(
|
||||||
|
spacing: 32,
|
||||||
|
children: [
|
||||||
|
Icon(FontAwesomeIcons.solidTriangleExclamation, size: 64, color: Colors.red[900]),
|
||||||
|
Text("Failed to parse QR", textAlign: TextAlign.center),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final (channel, user) = snapshot.data!;
|
||||||
|
|
||||||
|
final sub = overrideSubscription ?? channel.subscription;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Text("SCN Channel", style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20), textAlign: TextAlign.center),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(child: Text("Name: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||||
|
Expanded(child: SingleChildScrollView(child: Text(channel.displayName), scrollDirection: Axis.horizontal)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(child: Text("InternalName: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||||
|
Expanded(child: SingleChildScrollView(child: Text(channel.internalName, style: const TextStyle(fontStyle: FontStyle.italic)), scrollDirection: Axis.horizontal)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(child: Text("ChannelID: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||||
|
Expanded(child: SingleChildScrollView(child: Text(channel.channelID, style: const TextStyle(fontStyle: FontStyle.italic)), scrollDirection: Axis.horizontal)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(child: Text("Messages: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||||
|
Expanded(child: SingleChildScrollView(child: Text(channel.messagesSent.toString()), scrollDirection: Axis.horizontal)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (channel.descriptionName != null && channel.descriptionName!.isNotEmpty)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(child: Text("Description:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||||
|
Expanded(child: SingleChildScrollView(child: Text(channel.descriptionName!), scrollDirection: Axis.horizontal)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(child: Text("Owner:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||||
|
Expanded(child: SingleChildScrollView(child: Text((user.username ?? user.userID) + ((auth.userID != null && auth.userID! == user.userID) ? "\n(you)" : "")), scrollDirection: Axis.horizontal)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(child: Text("UserID:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||||
|
Expanded(child: SingleChildScrollView(child: Text(user.userID, style: const TextStyle(fontStyle: FontStyle.italic)), scrollDirection: Axis.horizontal)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(child: Text("Status:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||||
|
Expanded(child: SingleChildScrollView(child: Text(_formatSubscriptionStatus(sub)), scrollDirection: Axis.horizontal)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 48),
|
||||||
|
if (sub == null)
|
||||||
|
UI.button(
|
||||||
|
text: 'Request Subscription',
|
||||||
|
onPressed: _onSubscribe,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
if (sub != null && sub.confirmed)
|
||||||
|
UI.button(
|
||||||
|
text: 'Go to channel',
|
||||||
|
onPressed: () {
|
||||||
|
Navi.pushOnRoot(context, () => ChannelViewPage(channelID: widget.value.channelID, preloadedData: null, needsReload: null));
|
||||||
|
},
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSubscribe() async {
|
||||||
|
final auth = Provider.of<AppAuth>(context, listen: false);
|
||||||
|
try {
|
||||||
|
var sub = await APIClient.subscribeToChannelbyID(auth, widget.value.channelID, subscribeKey: widget.value.subscribeKey);
|
||||||
|
if (sub.confirmed) {
|
||||||
|
Toaster.success("Success", "Subscription request sent and auto-confirmed");
|
||||||
|
} else {
|
||||||
|
Toaster.success("Success", "Subscription request sent - pending confirmation");
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
overrideSubscription = sub;
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
Toaster.error("Error", 'Failed to send subscription-request: ${e.toString()}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatSubscriptionStatus(Subscription? sub) {
|
||||||
|
if (sub == null) {
|
||||||
|
return "Not Subscribed";
|
||||||
|
} else if (sub.confirmed) {
|
||||||
|
return "Already Subscribed";
|
||||||
|
} else {
|
||||||
|
return "Unconfirmed Subscription";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,157 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:simplecloudnotifier/api/api_client.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/state/app_auth.dart';
|
||||||
|
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||||
|
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||||
|
|
||||||
|
class ChannelScannerResultChannelView extends StatefulWidget {
|
||||||
|
final ScanResultChannel value;
|
||||||
|
|
||||||
|
const ChannelScannerResultChannelView({required this.value}) : super();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ChannelScannerResultChannelView> createState() => _ChannelScannerResultChannelViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChannelScannerResultChannelViewState extends State<ChannelScannerResultChannelView> {
|
||||||
|
Future<(ChannelPreview, UserPreview)?> _fetchDataFuture;
|
||||||
|
|
||||||
|
_ChannelScannerResultChannelViewState() : _fetchDataFuture = Future.value(null); // Initial dummy future
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
final auth = Provider.of<AppAuth>(context, listen: false);
|
||||||
|
setState(() {
|
||||||
|
_fetchDataFuture = _fetchData(auth);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<(ChannelPreview, UserPreview)?> _fetchData(AppAuth auth) async {
|
||||||
|
ChannelPreview? channel = null;
|
||||||
|
try {
|
||||||
|
channel = await APIClient.getChannelPreview(auth, widget.value.channelID);
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
Toaster.error("Error", 'Failed to fetch channel preview: ${e.toString()}');
|
||||||
|
ApplicationLog.error('Failed to fetch channel (preview) for ${widget.value.channelID}', trace: stackTrace);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
UserPreview? user = null;
|
||||||
|
try {
|
||||||
|
user = await APIClient.getUserPreview(auth, widget.value.ownerUserID);
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
Toaster.error("Error", 'Failed to fetch user preview: ${e.toString()}');
|
||||||
|
ApplicationLog.error('Failed to fetch user (preview) for ${widget.value.ownerUserID}', trace: stackTrace);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (channel, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FutureBuilder<(ChannelPreview, UserPreview)?>(
|
||||||
|
future: _fetchDataFuture,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.hasError) {
|
||||||
|
return Text('Error: ${snapshot.error}'); //TODO better error display
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.data == null) {
|
||||||
|
return Column(
|
||||||
|
spacing: 32,
|
||||||
|
children: [
|
||||||
|
Icon(FontAwesomeIcons.solidTriangleExclamation, size: 64, color: Colors.red[900]),
|
||||||
|
Text("Failed to parse QR", textAlign: TextAlign.center),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final (channel, user) = snapshot.data!;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Text("SCN Channel", style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20), textAlign: TextAlign.center),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(child: Text("Name: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||||
|
Expanded(child: SingleChildScrollView(child: Text(channel.displayName), scrollDirection: Axis.horizontal)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(child: Text("InternalName: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||||
|
Expanded(child: SingleChildScrollView(child: Text(channel.internalName, style: const TextStyle(fontStyle: FontStyle.italic)), scrollDirection: Axis.horizontal)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(child: Text("ChannelID: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||||
|
Expanded(child: SingleChildScrollView(child: Text(channel.channelID, style: const TextStyle(fontStyle: FontStyle.italic)), scrollDirection: Axis.horizontal)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(child: Text("Messages: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||||
|
Expanded(child: SingleChildScrollView(child: Text(channel.messagesSent.toString()), scrollDirection: Axis.horizontal)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (channel.descriptionName != null && channel.descriptionName!.isNotEmpty)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(child: Text("Description:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||||
|
Expanded(child: SingleChildScrollView(child: Text(channel.descriptionName!), scrollDirection: Axis.horizontal)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(child: Text("Owner:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||||
|
Expanded(child: SingleChildScrollView(child: Text(user.username ?? user.userID), scrollDirection: Axis.horizontal)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(child: Text("UserID:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||||
|
Expanded(child: SingleChildScrollView(child: Text(user.userID, style: const TextStyle(fontStyle: FontStyle.italic)), scrollDirection: Axis.horizontal)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(child: Text("Status:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||||
|
Expanded(child: SingleChildScrollView(child: Text(_formatSubscriptionStatus(channel.subscription)), scrollDirection: Axis.horizontal)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 48),
|
||||||
|
Text('QR Code contains no subscription-key\nCannot subscribe to channel', textAlign: TextAlign.center, style: const TextStyle(fontStyle: FontStyle.italic)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatSubscriptionStatus(Subscription? sub) {
|
||||||
|
if (sub == null) {
|
||||||
|
return "Not Subscribed";
|
||||||
|
} else if (sub.confirmed) {
|
||||||
|
return "Already Subscribed";
|
||||||
|
} else {
|
||||||
|
return "Unconfirmed Subscription";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,239 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||||
|
import 'package:simplecloudnotifier/models/channel.dart';
|
||||||
|
import 'package:simplecloudnotifier/models/keytoken.dart';
|
||||||
|
import 'package:simplecloudnotifier/models/scan_result.dart';
|
||||||
|
import 'package:simplecloudnotifier/models/user.dart';
|
||||||
|
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||||
|
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||||
|
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||||
|
import 'package:simplecloudnotifier/utils/ui.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
|
class ChannelScannerResultMessageSend extends StatefulWidget {
|
||||||
|
final ScanResultMessageSend value;
|
||||||
|
|
||||||
|
const ChannelScannerResultMessageSend({required this.value}) : super();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ChannelScannerResultMessageSend> createState() => _ChannelScannerResultMessageSendState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChannelScannerResultMessageSendState extends State<ChannelScannerResultMessageSend> {
|
||||||
|
Future<(UserPreview, KeyTokenPreview?)?> _fetchDataFuture;
|
||||||
|
|
||||||
|
_ChannelScannerResultMessageSendState() : _fetchDataFuture = Future.value(null); // Initial dummy future
|
||||||
|
|
||||||
|
late TextEditingController _ctrlMessage;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
_ctrlMessage = TextEditingController();
|
||||||
|
|
||||||
|
final auth = Provider.of<AppAuth>(context, listen: false);
|
||||||
|
setState(() {
|
||||||
|
_fetchDataFuture = _fetchData(auth);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_ctrlMessage.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<(UserPreview, KeyTokenPreview?)?> _fetchData(AppAuth auth) async {
|
||||||
|
UserPreview? user = null;
|
||||||
|
try {
|
||||||
|
user = await APIClient.getUserPreview(auth, widget.value.userID);
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
Toaster.error("Error", 'Failed to fetch user preview: ${e.toString()}');
|
||||||
|
ApplicationLog.error('Failed to fetch user (preview) for ${widget.value.userID}', trace: stackTrace);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyTokenPreview? key = null;
|
||||||
|
if (widget.value.userKey != null) {
|
||||||
|
try {
|
||||||
|
key = await APIClient.getKeyTokenPreviewByToken(auth, widget.value.userKey!);
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
Toaster.error("Error", 'Failed to fetch keytoken preview: ${e.toString()}');
|
||||||
|
ApplicationLog.error('Failed to fetch keytoken (preview) for ${widget.value.userID}', trace: stackTrace);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (user, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FutureBuilder<(UserPreview, KeyTokenPreview?)?>(
|
||||||
|
future: _fetchDataFuture,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.hasError) {
|
||||||
|
return Text('Error: ${snapshot.error}'); //TODO better error display
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.data == null) {
|
||||||
|
return Column(
|
||||||
|
spacing: 32,
|
||||||
|
children: [
|
||||||
|
Icon(FontAwesomeIcons.solidTriangleExclamation, size: 64, color: Colors.red[900]),
|
||||||
|
Text("Failed to parse QR", textAlign: TextAlign.center),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final (user, key) = snapshot.data!;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Text((widget.value.userKey == null) ? "SCN User" : "SCN User & Key", style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20), textAlign: TextAlign.center),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
if (user.username != null)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(child: Text("Name: ", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||||
|
Expanded(child: SingleChildScrollView(child: Text(user.username!), scrollDirection: Axis.horizontal)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(child: Text("UserID:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||||
|
Expanded(child: SingleChildScrollView(child: Text(user.userID, style: const TextStyle(fontStyle: FontStyle.italic)), scrollDirection: Axis.horizontal)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
if (key != null) ...[
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(child: Text("KeyID:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||||
|
Expanded(child: SingleChildScrollView(child: Text(key.keytokenID, style: const TextStyle(fontStyle: FontStyle.italic)), scrollDirection: Axis.horizontal)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(child: Text("KeyName:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||||
|
Expanded(child: SingleChildScrollView(child: Text(key.name), scrollDirection: Axis.horizontal)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ConstrainedBox(child: Text("Permissions:", style: const TextStyle(fontWeight: FontWeight.bold)), constraints: const BoxConstraints(minWidth: 100)),
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Text(_formatPermissions(key.permissions) + "\n" + (key.allChannels ? "(all channels)" : '(${key.channels.length} channels)')),
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
if (widget.value.userKey == null)
|
||||||
|
Text(
|
||||||
|
'QR Code contains no key\nCannot send messages',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(fontStyle: FontStyle.italic),
|
||||||
|
),
|
||||||
|
if (widget.value.userKey != null) ..._buildSend(context),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildSend(BuildContext context) {
|
||||||
|
return [
|
||||||
|
FractionallySizedBox(
|
||||||
|
widthFactor: 1.0,
|
||||||
|
child: TextField(
|
||||||
|
controller: _ctrlMessage,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
labelText: 'Text',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: UI.button(
|
||||||
|
text: 'Send Message',
|
||||||
|
onPressed: _onSend,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
UI.button(
|
||||||
|
text: 'Web',
|
||||||
|
onPressed: _onOpenWeb,
|
||||||
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
|
textColor: Theme.of(context).colorScheme.onSecondary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSend() async {
|
||||||
|
if (_ctrlMessage.text.isEmpty) {
|
||||||
|
Toaster.error("Error", 'Please enter a message');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widget.value.userKey == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await APIClient.sendMessage(widget.value.userID, widget.value.userKey!, _ctrlMessage.text);
|
||||||
|
Toaster.success("Success", 'Message sent');
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
Toaster.error("Error", 'Failed to send message: ${e.toString()}');
|
||||||
|
ApplicationLog.error('Failed to send message', trace: stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onOpenWeb() async {
|
||||||
|
try {
|
||||||
|
final Uri uri = Uri.parse(widget.value.url);
|
||||||
|
|
||||||
|
ApplicationLog.debug('Opening URL: [ ${uri.toString()} ]');
|
||||||
|
|
||||||
|
if (await canLaunchUrl(uri)) {
|
||||||
|
await launchUrl(uri);
|
||||||
|
} else {
|
||||||
|
Toaster.error("Error", 'Cannot open URL on this system');
|
||||||
|
}
|
||||||
|
} catch (exc, trace) {
|
||||||
|
ApplicationLog.error('Failed to open URL: ' + exc.toString(), additional: 'URL: ${widget.value.url}', trace: trace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatPermissions(String v) {
|
||||||
|
var splt = v.split(';');
|
||||||
|
|
||||||
|
if (splt.length == 0) return "None";
|
||||||
|
|
||||||
|
List<String> result = [];
|
||||||
|
|
||||||
|
if (splt.contains("A")) result.add(" - Admin");
|
||||||
|
if (splt.contains("UR")) result.add(" - Read Account");
|
||||||
|
if (splt.contains("CR")) result.add(" - Read Messages");
|
||||||
|
if (splt.contains("CS")) result.add(" - Send Messages");
|
||||||
|
|
||||||
|
return result.join("\n");
|
||||||
|
}
|
||||||
|
}
|
@ -73,20 +73,27 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
|||||||
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
final userAcc = Provider.of<AppAuth>(context, listen: false);
|
||||||
|
|
||||||
if (widget.preloadedData != null && usePreload) {
|
if (widget.preloadedData != null && usePreload) {
|
||||||
channelPreview = widget.preloadedData!.$1.toPreview();
|
|
||||||
channel = widget.preloadedData!.$1;
|
channel = widget.preloadedData!.$1;
|
||||||
subscription = widget.preloadedData!.$2;
|
subscription = widget.preloadedData!.$2;
|
||||||
|
channelPreview = widget.preloadedData!.$1.toPreview(widget.preloadedData!.$2);
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
var p = await APIClient.getChannelPreview(userAcc, widget.channelID);
|
var p = await APIClient.getChannelPreview(userAcc, widget.channelID);
|
||||||
|
setState(() {
|
||||||
channelPreview = p;
|
channelPreview = p;
|
||||||
|
subscription = p.subscription;
|
||||||
|
});
|
||||||
|
|
||||||
if (p.ownerUserID == userAcc.userID) {
|
if (p.ownerUserID == userAcc.userID) {
|
||||||
var r = await APIClient.getChannel(userAcc, widget.channelID);
|
var r = await APIClient.getChannel(userAcc, widget.channelID);
|
||||||
|
setState(() {
|
||||||
channel = r.channel;
|
channel = r.channel;
|
||||||
subscription = r.subscription;
|
subscription = r.subscription;
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
|
setState(() {
|
||||||
channel = null;
|
channel = null;
|
||||||
subscription = null; //TODO get own subscription on this channel, even though its foreign channel
|
});
|
||||||
}
|
}
|
||||||
} catch (exc, trace) {
|
} catch (exc, trace) {
|
||||||
ApplicationLog.error('Failed to load data: ' + exc.toString(), trace: trace);
|
ApplicationLog.error('Failed to load data: ' + exc.toString(), trace: trace);
|
||||||
@ -97,6 +104,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
this.loadingState = ChannelViewPageInitState.okay;
|
this.loadingState = ChannelViewPageInitState.okay;
|
||||||
|
|
||||||
assert(channelPreview != null);
|
assert(channelPreview != null);
|
||||||
@ -123,6 +131,7 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
|||||||
} else {
|
} else {
|
||||||
_futureOwner = ImmediateFuture<UserPreview>.ofFuture(APIClient.getUserPreview(userAcc, this.channelPreview!.ownerUserID));
|
_futureOwner = ImmediateFuture<UserPreview>.ofFuture(APIClient.getUserPreview(userAcc, this.channelPreview!.ownerUserID));
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -61,7 +61,7 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
|||||||
final msg = await APIClient.getMessage(acc, widget.messageID);
|
final msg = await APIClient.getMessage(acc, widget.messageID);
|
||||||
|
|
||||||
final fut_chn = APIClient.getChannelPreview(acc, msg.channelID);
|
final fut_chn = APIClient.getChannelPreview(acc, msg.channelID);
|
||||||
final fut_key = APIClient.getKeyTokenPreview(acc, msg.usedKeyID);
|
final fut_key = APIClient.getKeyTokenPreviewByID(acc, msg.usedKeyID);
|
||||||
final fut_usr = APIClient.getUserPreview(acc, msg.senderUserID);
|
final fut_usr = APIClient.getUserPreview(acc, msg.senderUserID);
|
||||||
|
|
||||||
final chn = await fut_chn;
|
final chn = await fut_chn;
|
||||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:qr_flutter/qr_flutter.dart';
|
import 'package:qr_flutter/qr_flutter.dart';
|
||||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||||
|
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||||
|
|
||||||
@ -135,7 +136,7 @@ class _SendRootPageState extends State<SendRootPage> {
|
|||||||
if (await canLaunchUrl(uri)) {
|
if (await canLaunchUrl(uri)) {
|
||||||
await launchUrl(uri);
|
await launchUrl(uri);
|
||||||
} else {
|
} else {
|
||||||
// TODO ("Cannot open URL");
|
Toaster.error("Error", 'Cannot open URL on this system');
|
||||||
}
|
}
|
||||||
} catch (exc, trace) {
|
} catch (exc, trace) {
|
||||||
ApplicationLog.error('Failed to open URL: ' + exc.toString(), additional: 'URL: ${url}', trace: trace);
|
ApplicationLog.error('Failed to open URL: ' + exc.toString(), additional: 'URL: ${url}', trace: trace);
|
||||||
|
@ -13,6 +13,15 @@ class Navi {
|
|||||||
Navigator.push(context, MaterialPageRoute<T>(builder: (context) => builder()));
|
Navigator.push(context, MaterialPageRoute<T>(builder: (context) => builder()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void pushOnRoot<T extends Widget>(BuildContext context, T Function() builder) {
|
||||||
|
Provider.of<AppBarState>(context, listen: false).setLoadingIndeterminate(false);
|
||||||
|
Provider.of<AppBarState>(context, listen: false).setShowSearchField(false);
|
||||||
|
|
||||||
|
Navigator.popUntil(context, (route) => route.isFirst);
|
||||||
|
|
||||||
|
Navigator.push(context, MaterialPageRoute<T>(builder: (context) => builder()));
|
||||||
|
}
|
||||||
|
|
||||||
static void popToRoot(BuildContext context) {
|
static void popToRoot(BuildContext context) {
|
||||||
Provider.of<AppBarState>(context, listen: false).setLoadingIndeterminate(false);
|
Provider.of<AppBarState>(context, listen: false).setLoadingIndeterminate(false);
|
||||||
Provider.of<AppBarState>(context, listen: false).setShowSearchField(false);
|
Provider.of<AppBarState>(context, listen: false).setShowSearchField(false);
|
||||||
|
@ -4,7 +4,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|||||||
class UI {
|
class UI {
|
||||||
static const double DefaultBorderRadius = 4;
|
static const double DefaultBorderRadius = 4;
|
||||||
|
|
||||||
static Widget button({required String text, required void Function() onPressed, bool big = false, Color? color = null, bool tonal = false, IconData? icon = null}) {
|
static Widget button({required String text, required void Function() onPressed, bool big = false, Color? color = null, Color? textColor = null, bool tonal = false, IconData? icon = null}) {
|
||||||
final double fontSize = big ? 24 : 14;
|
final double fontSize = big ? 24 : 14;
|
||||||
final padding = big ? EdgeInsets.fromLTRB(8, 12, 8, 12) : null;
|
final padding = big ? EdgeInsets.fromLTRB(8, 12, 8, 12) : null;
|
||||||
|
|
||||||
@ -12,6 +12,7 @@ class UI {
|
|||||||
textStyle: TextStyle(fontSize: fontSize),
|
textStyle: TextStyle(fontSize: fontSize),
|
||||||
padding: padding,
|
padding: padding,
|
||||||
backgroundColor: color,
|
backgroundColor: color,
|
||||||
|
foregroundColor: textColor,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(DefaultBorderRadius)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(DefaultBorderRadius)),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user