Finish implementing send page

This commit is contained in:
Mike Schwörer 2025-04-13 01:51:52 +02:00
parent 95353735b0
commit e96be86314
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
7 changed files with 300 additions and 85 deletions

View File

@ -21,7 +21,7 @@
- [ ] read + migrate old SharedPrefs (or not? - who uses SCN even??) - [ ] read + migrate old SharedPrefs (or not? - who uses SCN even??)
- [ ] Account-Page - [ ] Account-Page
- [ ] Logout - [ ] Logout
- [ ] Send-page - [x] Send-page
- [ ] Still @ERROR on scn-init, but no logs? - better persist error (write in SharedPrefs at error_$date=txt ?), also perhaps print first error line in scn-init notification? - [ ] Still @ERROR on scn-init, but no logs? - better persist error (write in SharedPrefs at error_$date=txt ?), also perhaps print first error line in scn-init notification?

View File

@ -178,11 +178,12 @@ class _ChannelScannerResultMessageSendState extends State<ChannelScannerResultMe
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
UI.button( UI.buttonIconOnly(
text: 'Web', icon: FontAwesomeIcons.earthAmericas,
onPressed: _onOpenWeb, onPressed: _onOpenWeb,
square: true,
color: Theme.of(context).colorScheme.secondary, color: Theme.of(context).colorScheme.secondary,
textColor: Theme.of(context).colorScheme.onSecondary, iconColor: Theme.of(context).colorScheme.onSecondary,
), ),
], ],
), ),
@ -200,6 +201,9 @@ class _ChannelScannerResultMessageSendState extends State<ChannelScannerResultMe
try { try {
await APIClient.sendMessage(widget.value.userID, widget.value.userKey!, _ctrlMessage.text); await APIClient.sendMessage(widget.value.userID, widget.value.userKey!, _ctrlMessage.text);
Toaster.success("Success", 'Message sent'); Toaster.success("Success", 'Message sent');
setState(() {
_ctrlMessage.clear();
});
} catch (e, stackTrace) { } catch (e, stackTrace) {
Toaster.error("Error", 'Failed to send message: ${e.toString()}'); Toaster.error("Error", 'Failed to send message: ${e.toString()}');
ApplicationLog.error('Failed to send message', trace: stackTrace); ApplicationLog.error('Failed to send message', trace: stackTrace);

View File

@ -1,13 +1,19 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.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/api/api_client.dart';
import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/globals.dart';
import 'package:simplecloudnotifier/utils/toaster.dart'; import 'package:simplecloudnotifier/utils/toaster.dart';
import 'package:simplecloudnotifier/utils/ui.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';
class SendRootPage extends StatefulWidget { class SendRootPage extends StatefulWidget {
const SendRootPage({super.key, required bool isVisiblePage}); const SendRootPage({super.key, required this.isVisiblePage});
final bool isVisiblePage;
@override @override
State<SendRootPage> createState() => _SendRootPageState(); State<SendRootPage> createState() => _SendRootPageState();
@ -16,18 +22,28 @@ class SendRootPage extends StatefulWidget {
class _SendRootPageState extends State<SendRootPage> { class _SendRootPageState extends State<SendRootPage> {
late TextEditingController _msgTitle; late TextEditingController _msgTitle;
late TextEditingController _msgContent; late TextEditingController _msgContent;
late TextEditingController _channelName;
late TextEditingController _senderName;
int _priority = 0;
bool _expanded = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_msgTitle = TextEditingController(); _msgTitle = TextEditingController();
_msgContent = TextEditingController(); _msgContent = TextEditingController();
_channelName = TextEditingController();
_senderName = TextEditingController();
} }
@override @override
void dispose() { void dispose() {
_msgTitle.dispose(); _msgTitle.dispose();
_msgContent.dispose(); _msgContent.dispose();
_channelName.dispose();
_senderName.dispose();
super.dispose(); super.dispose();
} }
@ -38,7 +54,15 @@ class _SendRootPageState extends State<SendRootPage> {
return SingleChildScrollView( return SingleChildScrollView(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: _expanded ? _buildExpanded(context, acc) : _buildSimple(context, acc),
),
);
},
);
}
Widget _buildSimple(BuildContext context, AppAuth acc) {
return Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
_buildQRCode(context, acc), _buildQRCode(context, acc),
@ -68,22 +92,124 @@ class _SendRootPageState extends State<SendRootPage> {
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
FilledButton( Row(
style: FilledButton.styleFrom(textStyle: const TextStyle(fontSize: 20)), children: [
onPressed: _send, Expanded(
child: const Text('Send'), child: UI.button(
text: 'Send',
onPressed: () {
_sendSimple(acc);
},
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
),
),
const SizedBox(width: 8),
UI.buttonIconOnly(
icon: FontAwesomeIcons.layerPlus,
onPressed: _openExpanded,
square: true,
color: Theme.of(context).colorScheme.secondary,
iconColor: Theme.of(context).colorScheme.onSecondary,
),
],
), ),
const SizedBox(height: 32), const SizedBox(height: 32),
], ],
),
),
);
},
); );
} }
void _send() { Widget _buildExpanded(BuildContext context, AppAuth acc) {
//... return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FractionallySizedBox(
widthFactor: 1.0,
child: TextField(
controller: _channelName,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Channel',
),
),
),
const SizedBox(height: 16),
FractionallySizedBox(
widthFactor: 1.0,
child: TextField(
controller: _msgTitle,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Title',
),
),
),
const SizedBox(height: 16),
FractionallySizedBox(
widthFactor: 1.0,
child: TextField(
controller: _senderName,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'SenderName',
),
),
),
const SizedBox(height: 16),
SegmentedButton<int>(
showSelectedIcon: false,
segments: const <ButtonSegment<int>>[
ButtonSegment<int>(value: 0, label: Text('Low Priority')),
ButtonSegment<int>(value: 1, label: Text('Normal')),
ButtonSegment<int>(value: 2, label: Text('High Priority')),
],
selected: {_priority},
onSelectionChanged: (Set<int> newSelection) {
setState(() {
_priority = newSelection.isEmpty ? 1 : newSelection.first;
});
},
),
const SizedBox(height: 16),
FractionallySizedBox(
widthFactor: 1.0,
child: TextField(
controller: _msgContent,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Text',
),
minLines: 6,
maxLines: null,
keyboardType: TextInputType.multiline,
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: UI.button(
text: 'Send',
onPressed: () {
_sendExpanded(acc);
},
color: Theme.of(context).colorScheme.primary,
textColor: Theme.of(context).colorScheme.onPrimary,
),
),
const SizedBox(width: 8),
UI.buttonIconOnly(
icon: FontAwesomeIcons.squareDashed,
onPressed: _closeExpanded,
square: true,
color: Theme.of(context).colorScheme.secondary,
iconColor: Theme.of(context).colorScheme.onSecondary,
),
],
),
const SizedBox(height: 32),
],
);
} }
Widget _buildQRCode(BuildContext context, AppAuth acc) { Widget _buildQRCode(BuildContext context, AppAuth acc) {
@ -94,11 +220,22 @@ class _SendRootPageState extends State<SendRootPage> {
return FutureBuilder( return FutureBuilder(
future: acc.loadUser(force: false), future: acc.loadUser(force: false),
builder: ((context, snapshot) { builder: ((context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.active || snapshot.connectionState == ConnectionState.waiting) {
return const SizedBox(
width: 300.0,
height: 300.0,
child: Center(child: CircularProgressIndicator()),
);
}
if (snapshot.hasError) { if (snapshot.hasError) {
return Text('Error: ${snapshot.error}'); //TODO better error display return Text('Error: ${snapshot.error}'); //TODO better error display
} }
if (snapshot.connectionState != ConnectionState.done) {
return Text('...'); //?
}
var url = (acc.tokenSend == null) ? 'https://simplecloudnotifier.de?preset_user_id=${acc.userID}' : 'https://simplecloudnotifier.de?preset_user_id=${acc.userID}&preset_user_key=${acc.tokenSend}'; var url = (acc.tokenSend == null) ? 'https://simplecloudnotifier.de?preset_user_id=${acc.userID}' : 'https://simplecloudnotifier.de?preset_user_id=${acc.userID}&preset_user_key=${acc.tokenSend}';
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
_openWeb(url); _openWeb(url);
@ -117,16 +254,48 @@ class _SendRootPageState extends State<SendRootPage> {
), ),
), ),
); );
}
return const SizedBox(
width: 300.0,
height: 300.0,
child: Center(child: CircularProgressIndicator()),
);
}), }),
); );
} }
void _sendSimple(AppAuth acc) async {
if (!acc.isAuth()) {
Toaster.error("Error", 'Must be logged in to send messages');
return;
}
try {
await APIClient.sendMessage(acc.userID!, acc.tokenSend!, _msgContent.text);
Toaster.success("Success", 'Message sent');
setState(() {
_msgTitle.clear();
_msgContent.clear();
});
} catch (e, stackTrace) {
Toaster.error("Error", 'Failed to send message: ${e.toString()}');
ApplicationLog.error('Failed to send message', trace: stackTrace);
}
}
void _sendExpanded(AppAuth acc) async {
if (!acc.isAuth()) {
Toaster.error("Error", 'Must be logged in to send messages');
return;
}
try {
await APIClient.sendMessage(acc.userID!, acc.tokenSend!, _msgContent.text, channel: _channelName.text, senderName: _senderName.text, priority: _priority);
Toaster.success("Success", 'Message sent');
setState(() {
_msgTitle.clear();
_msgContent.clear();
});
} catch (e, stackTrace) {
Toaster.error("Error", 'Failed to send message: ${e.toString()}');
ApplicationLog.error('Failed to send message', trace: stackTrace);
}
}
void _openWeb(String url) async { void _openWeb(String url) async {
try { try {
final Uri uri = Uri.parse(url); final Uri uri = Uri.parse(url);
@ -142,4 +311,24 @@ class _SendRootPageState extends State<SendRootPage> {
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);
} }
} }
void _closeExpanded() {
setState(() {
_expanded = false;
_channelName.clear();
_priority = 1;
_senderName.clear();
});
}
void _openExpanded() {
final userAcc = Provider.of<AppAuth>(context, listen: false);
setState(() {
_expanded = true;
_channelName.text = userAcc.getUserOrNull()?.defaultChannel ?? 'main';
_priority = 1;
_senderName.text = Globals().deviceName;
});
}
} }

View File

@ -11,7 +11,7 @@ class _SettingsRootPageState extends State<SettingsRootPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Center( return Center(
child: Text('Settings'), child: Text('(coming soon...)'), //TODO
); );
} }
} }

View File

@ -34,7 +34,7 @@ class AppAuth extends ChangeNotifier implements TokenSource {
} }
bool isAuth() { bool isAuth() {
return _userID != null && _tokenAdmin != null; return _userID != null && _tokenAdmin != null && _tokenSend != null;
} }
void set(User user, Client client, String tokenAdmin, String tokenSend) { void set(User user, Client client, String tokenAdmin, String tokenSend) {

View File

@ -25,6 +25,7 @@ class Globals {
String hostname = ''; String hostname = '';
String clientType = ''; String clientType = '';
String deviceModel = ''; String deviceModel = '';
String deviceName = '';
late SharedPreferences sharedPrefs; late SharedPreferences sharedPrefs;
@ -48,18 +49,23 @@ class Globals {
if (Platform.isAndroid) { if (Platform.isAndroid) {
this.clientType = 'ANDROID'; this.clientType = 'ANDROID';
this.deviceModel = (await DeviceInfoPlugin().androidInfo).model; this.deviceModel = (await DeviceInfoPlugin().androidInfo).model;
this.deviceName = (await DeviceInfoPlugin().androidInfo).name;
} else if (Platform.isIOS) { } else if (Platform.isIOS) {
this.clientType = 'IOS'; this.clientType = 'IOS';
this.deviceModel = (await DeviceInfoPlugin().iosInfo).model; this.deviceModel = (await DeviceInfoPlugin().iosInfo).model;
this.deviceName = (await DeviceInfoPlugin().iosInfo).name;
} else if (Platform.isLinux) { } else if (Platform.isLinux) {
this.clientType = 'LINUX'; this.clientType = 'LINUX';
this.deviceModel = (await DeviceInfoPlugin().linuxInfo).prettyName; this.deviceModel = (await DeviceInfoPlugin().linuxInfo).prettyName;
this.deviceName = (await DeviceInfoPlugin().linuxInfo).name;
} else if (Platform.isWindows) { } else if (Platform.isWindows) {
this.clientType = 'WINDOWS'; this.clientType = 'WINDOWS';
this.deviceModel = (await DeviceInfoPlugin().windowsInfo).productName; this.deviceModel = (await DeviceInfoPlugin().windowsInfo).productName;
this.deviceName = (await DeviceInfoPlugin().windowsInfo).computerName;
} else if (Platform.isMacOS) { } else if (Platform.isMacOS) {
this.clientType = 'MACOS'; this.clientType = 'MACOS';
this.deviceModel = (await DeviceInfoPlugin().macOsInfo).model; this.deviceModel = (await DeviceInfoPlugin().macOsInfo).model;
this.deviceName = (await DeviceInfoPlugin().macOsInfo).computerName;
} else { } else {
this.clientType = '?'; this.clientType = '?';
} }

View File

@ -49,20 +49,36 @@ class UI {
} }
} }
static Widget buttonIconOnly({ static Widget buttonIconOnly({required void Function() onPressed, required IconData icon, double? iconSize = null, bool? square, Color? color = null, Color? iconColor = null}) {
required void Function() onPressed, final style = ButtonStyle(
required IconData icon, tapTargetSize: MaterialTapTargetSize.shrinkWrap,
double? iconSize = null, backgroundColor: (color != null) ? WidgetStateProperty.resolveWith<Color?>((states) => color) : null,
}) { padding: (square ?? false) ? WidgetStateProperty.resolveWith<EdgeInsetsGeometry?>((states) => EdgeInsets.all(10)) : null,
shape: (square ?? false) ? WidgetStateProperty.resolveWith<OutlinedBorder?>((states) => RoundedRectangleBorder(borderRadius: BorderRadius.circular(DefaultBorderRadius))) : null,
);
if (color != null) {
return IconButton.filled(
icon: FaIcon(icon),
iconSize: iconSize ?? 18,
padding: EdgeInsets.all(4),
constraints: BoxConstraints(),
style: style,
onPressed: onPressed,
color: iconColor,
);
} else {
return IconButton( return IconButton(
icon: FaIcon(icon), icon: FaIcon(icon),
iconSize: iconSize ?? 18, iconSize: iconSize ?? 18,
padding: EdgeInsets.all(4), padding: EdgeInsets.all(4),
constraints: BoxConstraints(), constraints: BoxConstraints(),
style: ButtonStyle(tapTargetSize: MaterialTapTargetSize.shrinkWrap), style: style,
onPressed: onPressed, onPressed: onPressed,
color: iconColor,
); );
} }
}
static Widget buttonCard({required BuildContext context, required Widget child, required void Function() onTap, EdgeInsets? margin = null}) { static Widget buttonCard({required BuildContext context, required Widget child, required void Function() onTap, EdgeInsets? margin = null}) {
return Card.filled( return Card.filled(