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
|
||||
|
||||
# runs app locally (web | not really supported)
|
||||
run-linux:
|
||||
run-web:
|
||||
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)
|
||||
run-android:
|
||||
@ -42,7 +42,7 @@ fix:
|
||||
dart fix --apply
|
||||
|
||||
gen:
|
||||
flutter pub run build_runner build
|
||||
dart run build_runner build
|
||||
|
||||
# run `make run` in another terminal (or another variant of flutter run)
|
||||
autoreload:
|
||||
@ -63,4 +63,7 @@ clean:
|
||||
upgrade:
|
||||
flutter 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
|
||||
|
||||
- [ ] Message List
|
||||
- [x] Message List
|
||||
* [ ] CRUD
|
||||
- [ ] Message Big-View
|
||||
- [ ] Search/Filter Messages
|
||||
- [ ] Channel List
|
||||
* [ ] Show subs
|
||||
- [x] Message Big-View
|
||||
- [x] Search/Filter Messages
|
||||
- [x] Channel List
|
||||
* [x] Show subs
|
||||
* [ ] CRUD
|
||||
* [ ] what about unsubbed foreign channels? - thex should still be visible (or should they, do i still get the messages?)
|
||||
- [ ] Sub List
|
||||
* [ ] Sub/Unsub/Accept/Deny
|
||||
- [ ] Debug List (Show logs, requests)
|
||||
- [x] Sub List
|
||||
* [x] Sub/Unsub/Accept/Deny
|
||||
- [x] Debug List (Show logs, requests)
|
||||
- [ ] Key List
|
||||
* [ ] CRUD
|
||||
- [ ] Auto R-only key for admin, use for QR+link+send
|
||||
|
@ -3,8 +3,8 @@
|
||||
# shellcheck disable=SC2002 # disable useless-cat warning
|
||||
|
||||
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 errtrace # Allow the above trap be inherited by all functions in the script. ( 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 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.
|
||||
|
||||
@ -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"
|
||||
exit 1
|
||||
fi
|
||||
@ -37,10 +37,21 @@ trap 'echo "reseived SIGNAL<SIGTERM> - exiting"; exit 0' SIGTERM
|
||||
trap 'echo "reseived SIGNAL<SIGQUIT> - exiting"; exit 0' SIGQUIT
|
||||
|
||||
echo ""
|
||||
blue "Listening for changes in lib/ directory - sending signals to ${pid}..."
|
||||
while IFS= read -r pid; do
|
||||
blue "Listening for changes in lib/ directory - sending signals to ${pid}..."
|
||||
done <<< "$pids"
|
||||
|
||||
echo ""
|
||||
|
||||
while true; do
|
||||
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';
|
||||
done
|
||||
while IFS= read -r pid; do
|
||||
{
|
||||
while true; do
|
||||
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';
|
||||
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/client.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/subscription.dart';
|
||||
import 'package:simplecloudnotifier/models/user.dart';
|
||||
@ -50,7 +51,8 @@ class MessageFilter {
|
||||
}
|
||||
|
||||
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>({
|
||||
required String name,
|
||||
@ -61,10 +63,11 @@ class APIClient {
|
||||
dynamic jsonBody,
|
||||
String? authToken,
|
||||
Map<String, String>? header,
|
||||
bool? nonAPI,
|
||||
}) async {
|
||||
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);
|
||||
|
||||
@ -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(
|
||||
name: 'getKeyTokenPreview',
|
||||
name: 'getKeyTokenPreviewByID',
|
||||
method: 'GET',
|
||||
relURL: 'preview/keys/$kid',
|
||||
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 {
|
||||
return await _request(
|
||||
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(
|
||||
name: 'subscribeToChannelbyID',
|
||||
method: 'POST',
|
||||
relURL: 'users/${auth.getUserID()}/subscriptions',
|
||||
query: {
|
||||
if (subscribeKey != null) 'chan_subscribe_key': [subscribeKey],
|
||||
},
|
||||
jsonBody: {
|
||||
'channel_id': channelID,
|
||||
},
|
||||
@ -458,4 +474,26 @@ class APIClient {
|
||||
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(
|
||||
channelID: this.channelID,
|
||||
ownerUserID: this.ownerUserID,
|
||||
internalName: this.internalName,
|
||||
displayName: this.displayName,
|
||||
descriptionName: this.descriptionName,
|
||||
messagesSent: this.messagesSent,
|
||||
subscription: sub,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -109,6 +111,8 @@ class ChannelPreview {
|
||||
final String internalName;
|
||||
final String displayName;
|
||||
final String? descriptionName;
|
||||
final int messagesSent;
|
||||
final Subscription? subscription;
|
||||
|
||||
const ChannelPreview({
|
||||
required this.channelID,
|
||||
@ -116,6 +120,8 @@ class ChannelPreview {
|
||||
required this.internalName,
|
||||
required this.displayName,
|
||||
required this.descriptionName,
|
||||
required this.messagesSent,
|
||||
required this.subscription,
|
||||
});
|
||||
|
||||
factory ChannelPreview.fromJson(Map<String, dynamic> json) {
|
||||
@ -125,6 +131,8 @@ class ChannelPreview {
|
||||
internalName: json['internal_name'] as String,
|
||||
displayName: json['display_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';
|
||||
|
||||
enum ScanResultMode { ChannelSubscribe, MessageSend, Channel }
|
||||
enum ScanResultMode { ChannelSubscribe, MessageSend, Channel, Error }
|
||||
|
||||
abstract class ScanResult {
|
||||
ScanResultMode get mode;
|
||||
@ -12,10 +12,10 @@ abstract class ScanResult {
|
||||
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']);
|
||||
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')) {
|
||||
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 != 4) return null;
|
||||
if (lines.length != 5) return null;
|
||||
|
||||
return ScanResultChannel(channelDisplayName: lines[2], ownerUserID: lines[3], channelID: lines[4]);
|
||||
}
|
||||
|
||||
return null;
|
||||
return ScanResultError(message: 'Invalid QR code');
|
||||
}
|
||||
|
||||
static String createChannelQR(Channel channel) {
|
||||
@ -44,8 +44,9 @@ abstract class ScanResult {
|
||||
class ScanResultMessageSend extends ScanResult {
|
||||
final String userID;
|
||||
final String? userKey;
|
||||
final String url;
|
||||
|
||||
ScanResultMessageSend({required this.userID, required this.userKey});
|
||||
ScanResultMessageSend({required this.userID, required this.userKey, required this.url});
|
||||
|
||||
@override
|
||||
ScanResultMode get mode => ScanResultMode.MessageSend;
|
||||
@ -73,3 +74,12 @@ class ScanResultChannelSubscribe extends ScanResult {
|
||||
@override
|
||||
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:simplecloudnotifier/api/api_client.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/application_log.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);
|
||||
|
||||
if (widget.preloadedData != null && usePreload) {
|
||||
channelPreview = widget.preloadedData!.$1.toPreview();
|
||||
channel = widget.preloadedData!.$1;
|
||||
subscription = widget.preloadedData!.$2;
|
||||
channelPreview = widget.preloadedData!.$1.toPreview(widget.preloadedData!.$2);
|
||||
} else {
|
||||
try {
|
||||
var p = await APIClient.getChannelPreview(userAcc, widget.channelID);
|
||||
channelPreview = p;
|
||||
setState(() {
|
||||
channelPreview = p;
|
||||
subscription = p.subscription;
|
||||
});
|
||||
|
||||
if (p.ownerUserID == userAcc.userID) {
|
||||
var r = await APIClient.getChannel(userAcc, widget.channelID);
|
||||
channel = r.channel;
|
||||
subscription = r.subscription;
|
||||
setState(() {
|
||||
channel = r.channel;
|
||||
subscription = r.subscription;
|
||||
});
|
||||
} else {
|
||||
channel = null;
|
||||
subscription = null; //TODO get own subscription on this channel, even though its foreign channel
|
||||
setState(() {
|
||||
channel = null;
|
||||
});
|
||||
}
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to load data: ' + exc.toString(), trace: trace);
|
||||
@ -97,32 +104,34 @@ class _ChannelViewPageState extends State<ChannelViewPage> {
|
||||
}
|
||||
}
|
||||
|
||||
this.loadingState = ChannelViewPageInitState.okay;
|
||||
setState(() {
|
||||
this.loadingState = ChannelViewPageInitState.okay;
|
||||
|
||||
assert(channelPreview != null);
|
||||
assert(channelPreview != null);
|
||||
|
||||
if (this.channelPreview!.ownerUserID == userAcc.userID) {
|
||||
if (this.channel != null && this.channel!.subscribeKey != null) {
|
||||
_futureSubscribeKey = ImmediateFuture<String?>.ofValue(this.channel!.subscribeKey);
|
||||
if (this.channelPreview!.ownerUserID == userAcc.userID) {
|
||||
if (this.channel != null && this.channel!.subscribeKey != null) {
|
||||
_futureSubscribeKey = ImmediateFuture<String?>.ofValue(this.channel!.subscribeKey);
|
||||
} else {
|
||||
_futureSubscribeKey = ImmediateFuture<String?>.ofFuture(_getSubscribeKey(userAcc));
|
||||
}
|
||||
_futureSubscriptions = ImmediateFuture<List<(Subscription, UserPreview?)>>.ofFuture(_listSubscriptions(userAcc));
|
||||
} else {
|
||||
_futureSubscribeKey = ImmediateFuture<String?>.ofFuture(_getSubscribeKey(userAcc));
|
||||
_futureSubscribeKey = ImmediateFuture<String?>.ofValue(null);
|
||||
_futureSubscriptions = ImmediateFuture<List<(Subscription, UserPreview?)>>.ofValue([]);
|
||||
}
|
||||
_futureSubscriptions = ImmediateFuture<List<(Subscription, UserPreview?)>>.ofFuture(_listSubscriptions(userAcc));
|
||||
} else {
|
||||
_futureSubscribeKey = ImmediateFuture<String?>.ofValue(null);
|
||||
_futureSubscriptions = ImmediateFuture<List<(Subscription, UserPreview?)>>.ofValue([]);
|
||||
}
|
||||
|
||||
if (this.channelPreview!.ownerUserID == userAcc.userID) {
|
||||
var cacheUser = userAcc.getUserOrNull();
|
||||
if (cacheUser != null) {
|
||||
_futureOwner = ImmediateFuture<UserPreview>.ofValue(cacheUser.toPreview());
|
||||
if (this.channelPreview!.ownerUserID == userAcc.userID) {
|
||||
var cacheUser = userAcc.getUserOrNull();
|
||||
if (cacheUser != null) {
|
||||
_futureOwner = ImmediateFuture<UserPreview>.ofValue(cacheUser.toPreview());
|
||||
} else {
|
||||
_futureOwner = ImmediateFuture<UserPreview>.ofFuture(_getOwner(userAcc));
|
||||
}
|
||||
} else {
|
||||
_futureOwner = ImmediateFuture<UserPreview>.ofFuture(_getOwner(userAcc));
|
||||
_futureOwner = ImmediateFuture<UserPreview>.ofFuture(APIClient.getUserPreview(userAcc, this.channelPreview!.ownerUserID));
|
||||
}
|
||||
} else {
|
||||
_futureOwner = ImmediateFuture<UserPreview>.ofFuture(APIClient.getUserPreview(userAcc, this.channelPreview!.ownerUserID));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -61,7 +61,7 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
||||
final msg = await APIClient.getMessage(acc, widget.messageID);
|
||||
|
||||
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 chn = await fut_chn;
|
||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
|
||||
@ -135,7 +136,7 @@ class _SendRootPageState extends State<SendRootPage> {
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri);
|
||||
} else {
|
||||
// TODO ("Cannot open URL");
|
||||
Toaster.error("Error", 'Cannot open URL on this system');
|
||||
}
|
||||
} catch (exc, 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()));
|
||||
}
|
||||
|
||||
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) {
|
||||
Provider.of<AppBarState>(context, listen: false).setLoadingIndeterminate(false);
|
||||
Provider.of<AppBarState>(context, listen: false).setShowSearchField(false);
|
||||
|
@ -4,7 +4,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
class UI {
|
||||
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 padding = big ? EdgeInsets.fromLTRB(8, 12, 8, 12) : null;
|
||||
|
||||
@ -12,6 +12,7 @@ class UI {
|
||||
textStyle: TextStyle(fontSize: fontSize),
|
||||
padding: padding,
|
||||
backgroundColor: color,
|
||||
foregroundColor: textColor,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(DefaultBorderRadius)),
|
||||
);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user