Implement settings
Some checks failed
Build Docker and Deploy / Build Docker Container (push) Successful in 51s
Build Docker and Deploy / Run Unit-Tests (push) Failing after 11m17s
Build Docker and Deploy / Deploy to Server (push) Has been skipped

This commit is contained in:
2025-04-19 01:49:28 +02:00
parent 5417796f3f
commit b91ddc172d
33 changed files with 912 additions and 127 deletions

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/models/channel.dart';
@@ -9,6 +8,7 @@ import 'package:simplecloudnotifier/models/subscription.dart';
import 'package:simplecloudnotifier/pages/channel_message_view/channel_message_view.dart';
import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/state/app_settings.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/scn_data_cache.dart';
import 'package:simplecloudnotifier/utils/navi.dart';
@@ -20,8 +20,6 @@ enum ChannelListItemMode {
}
class ChannelListItem extends StatefulWidget {
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting
const ChannelListItem({
required this.channel,
required this.onChannelListReloadTrigger,
@@ -64,6 +62,8 @@ class _ChannelListItemState extends State<ChannelListItem> {
@override
Widget build(BuildContext context) {
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateFormat();
return Card.filled(
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
@@ -95,7 +95,7 @@ class _ChannelListItemState extends State<ChannelListItem> {
),
),
Text(
(widget.channel.timestampLastSent == null) ? '' : ChannelListItem._dateFormat.format(DateTime.parse(widget.channel.timestampLastSent!).toLocal()),
(widget.channel.timestampLastSent == null) ? '' : dateFormat.format(DateTime.parse(widget.channel.timestampLastSent!).toLocal()),
style: const TextStyle(fontSize: 14),
),
],

View File

@@ -6,7 +6,7 @@ import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/models/scn_message.dart';
import 'package:simplecloudnotifier/pages/message_list/message_list_item.dart';
import 'package:simplecloudnotifier/pages/message_view/message_view.dart';
import 'package:simplecloudnotifier/settings/app_settings.dart';
import 'package:simplecloudnotifier/state/app_settings.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/scn_data_cache.dart';

View File

@@ -1,7 +1,8 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/models/client.dart';
import 'package:simplecloudnotifier/state/app_settings.dart';
enum ClientListItemMode {
Messages,
@@ -9,8 +10,6 @@ enum ClientListItemMode {
}
class ClientListItem extends StatelessWidget {
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting
const ClientListItem({
required this.item,
super.key,
@@ -20,6 +19,8 @@ class ClientListItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateFormat();
return Card.filled(
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
@@ -43,7 +44,7 @@ class ClientListItem extends StatelessWidget {
),
),
Text(
ClientListItem._dateFormat.format(DateTime.parse(item.timestampCreated).toLocal()),
dateFormat.format(DateTime.parse(item.timestampCreated).toLocal()),
style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)),
),
],

View File

@@ -2,6 +2,7 @@ import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/state/app_settings.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/utils/notifier.dart';
@@ -67,8 +68,32 @@ class _DebugActionsPageState extends State<DebugActionsPage> {
SizedBox(height: 20),
UI.button(
big: false,
onPressed: () => Notifier.showLocalNotification('', 'TEST_CHANNEL', "Test Channel", "Channel for testing", "Hello World", "Local Notification test", null),
text: 'Show local notification',
onPressed: () => Notifier.showLocalNotification('', 'TEST_CHANNEL', "Test Channel", "Channel for testing", "Hello World", "Local Notification test", null, null),
text: 'Show local notification (generic)',
),
UI.button(
big: false,
onPressed: () => Notifier.showLocalNotification('', 'TEST_CHANNEL', "Test Channel", "Channel for testing", "Hello World", "Local Notification test", null, 0),
text: 'Show local notification (Prio = 0)',
),
UI.button(
big: false,
onPressed: () => Notifier.showLocalNotification('', 'TEST_CHANNEL', "Test Channel", "Channel for testing", "Hello World", "Local Notification test", null, 1),
text: 'Show local notification (Prio = 1)',
),
UI.button(
big: false,
onPressed: () => Notifier.showLocalNotification('', 'TEST_CHANNEL', "Test Channel", "Channel for testing", "Hello World", "Local Notification test", null, 2),
text: 'Show local notification (Prio = 2)',
),
SizedBox(height: 20),
UI.button(
big: false,
onPressed: () {
AppSettings().update((p) => p.reset());
Toaster.success("Success", "AppSettings reset to default");
},
text: 'Reset AppSettings to default',
),
],
),

View File

@@ -11,7 +11,7 @@ class DebugLogsPage extends StatefulWidget {
class _DebugLogsPageState extends State<DebugLogsPage> {
Box<SCNLog> logBox = Hive.box<SCNLog>('scn-logs');
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm');
@override
Widget build(BuildContext context) {

View File

@@ -6,7 +6,9 @@ class DebugSharedPrefPage extends StatelessWidget {
final SharedPreferences sharedPref;
final List<String> keys;
DebugSharedPrefPage({required this.sharedPref}) : keys = sharedPref.getKeys().toList();
DebugSharedPrefPage({required this.sharedPref}) : keys = sharedPref.getKeys().toList() {
keys.sort((a, b) => a.compareTo(b));
}
@override
Widget build(BuildContext context) {

View File

@@ -13,7 +13,7 @@ class DebugRequestsPage extends StatefulWidget {
class _DebugRequestsPageState extends State<DebugRequestsPage> {
Box<SCNRequest> requestsBox = Hive.box<SCNRequest>('scn-requests');
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm');
@override
Widget build(BuildContext context) {

View File

@@ -6,7 +6,7 @@ import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/models/scn_message.dart';
import 'package:simplecloudnotifier/pages/message_list/message_list_item.dart';
import 'package:simplecloudnotifier/pages/message_view/message_view.dart';
import 'package:simplecloudnotifier/settings/app_settings.dart';
import 'package:simplecloudnotifier/state/app_settings.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/scn_data_cache.dart';

View File

@@ -1,10 +1,12 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/models/keytoken.dart';
import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart';
import 'package:simplecloudnotifier/pages/keytoken_view/keytoken_view.dart';
import 'package:simplecloudnotifier/state/app_settings.dart';
import 'package:simplecloudnotifier/utils/navi.dart';
enum KeyTokenListItemMode {
@@ -13,8 +15,6 @@ enum KeyTokenListItemMode {
}
class KeyTokenListItem extends StatelessWidget {
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting
const KeyTokenListItem({
required this.item,
super.key,
@@ -24,6 +24,8 @@ class KeyTokenListItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateFormat();
return Card.filled(
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
@@ -51,7 +53,7 @@ class KeyTokenListItem extends StatelessWidget {
),
),
Text(
(item.timestampLastUsed == null) ? '' : KeyTokenListItem._dateFormat.format(DateTime.parse(item.timestampLastUsed!).toLocal()),
(item.timestampLastUsed == null) ? '' : dateFormat.format(DateTime.parse(item.timestampLastUsed!).toLocal()),
style: const TextStyle(fontSize: 14),
),
],

View File

@@ -1,5 +1,3 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:intl/intl.dart';
@@ -14,6 +12,7 @@ import 'package:simplecloudnotifier/pages/keytoken_view/keytoken_channel_modal.d
import 'package:simplecloudnotifier/pages/keytoken_view/keytoken_permission_modal.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/state/app_bar_state.dart';
import 'package:simplecloudnotifier/state/app_settings.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/scn_data_cache.dart';
import 'package:simplecloudnotifier/types/immediate_future.dart';
@@ -45,8 +44,6 @@ enum EditState { none, editing, saving }
enum KeyTokenViewPageInitState { loading, okay, error }
class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting
ImmediateFuture<UserPreview> _futureOwner = ImmediateFuture.ofPending();
ImmediateFuture<Map<String, ChannelPreview>> _futureAllChannels = ImmediateFuture.ofPending();
@@ -195,6 +192,8 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
}
Widget _buildOwnedKeyTokenView(BuildContext context, KeyToken keytoken) {
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateFormat();
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
@@ -217,13 +216,13 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
context: context,
icon: FontAwesomeIcons.solidClock,
title: 'Created',
values: [_KeyTokenViewPageState._dateFormat.format(DateTime.parse(keytoken.timestampCreated).toLocal())],
values: [dateFormat.format(DateTime.parse(keytoken.timestampCreated).toLocal())],
),
UI.metaCard(
context: context,
icon: FontAwesomeIcons.solidClockTwo,
title: 'Last Used',
values: [(keytoken.timestampLastUsed == null) ? 'Never' : _KeyTokenViewPageState._dateFormat.format(DateTime.parse(keytoken.timestampLastUsed!).toLocal())],
values: [(keytoken.timestampLastUsed == null) ? 'Never' : dateFormat.format(DateTime.parse(keytoken.timestampLastUsed!).toLocal())],
),
_buildOwnerCard(context, true),
UI.metaCard(
@@ -481,8 +480,6 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
}
void _editPermissions() async {
final acc = Provider.of<AppAuth>(context, listen: false);
if (keytokenUserAccAdmin == null || keytokenUserAccAdmin!.keytokenID == keytokenPreview!.keytokenID) {
Toaster.error("Error", "You cannot edit the currently used token");
return;
@@ -502,8 +499,6 @@ class _KeyTokenViewPageState extends State<KeyTokenViewPage> {
}
void _editChannels() async {
final acc = Provider.of<AppAuth>(context, listen: false);
if (keytokenUserAccAdmin == null || keytokenUserAccAdmin!.keytokenID == keytokenPreview!.keytokenID) {
Toaster.error("Error", "You cannot edit the currently used token");
return;

View File

@@ -6,7 +6,7 @@ import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/models/scn_message.dart';
import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.dart';
import 'package:simplecloudnotifier/pages/message_view/message_view.dart';
import 'package:simplecloudnotifier/settings/app_settings.dart';
import 'package:simplecloudnotifier/state/app_settings.dart';
import 'package:simplecloudnotifier/state/app_bar_state.dart';
import 'package:simplecloudnotifier/state/app_events.dart';
import 'package:simplecloudnotifier/state/application_log.dart';

View File

@@ -2,15 +2,14 @@ import 'dart:math';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/models/scn_message.dart';
import 'package:intl/intl.dart';
import 'package:simplecloudnotifier/state/app_settings.dart';
import 'package:simplecloudnotifier/utils/ui.dart';
class MessageListItem extends StatelessWidget {
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting
static final _lineCount = 3; //TODO setting
const MessageListItem({
required this.message,
required this.allChannels,
@@ -32,6 +31,9 @@ class MessageListItem extends StatelessWidget {
}
Card buildWithoutChannel(BuildContext context) {
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateFormat();
final previewLineCount = context.select<AppSettings, int>((v) => v.messagePreviewLength);
return Card.filled(
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
@@ -57,7 +59,7 @@ class MessageListItem extends StatelessWidget {
),
),
Text(
_dateFormat.format(DateTime.parse(message.timestamp).toLocal()),
dateFormat.format(DateTime.parse(message.timestamp).toLocal()),
style: const TextStyle(fontWeight: FontWeight.normal, fontSize: 11),
overflow: TextOverflow.clip,
maxLines: 1,
@@ -70,10 +72,10 @@ class MessageListItem extends StatelessWidget {
children: [
Expanded(
child: Text(
processContent(message.content),
processContent(message.content, previewLineCount),
style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)),
overflow: TextOverflow.ellipsis,
maxLines: _lineCount,
maxLines: previewLineCount,
),
),
if (message.priority == 2) SizedBox(width: 4),
@@ -90,6 +92,9 @@ class MessageListItem extends StatelessWidget {
}
Card buildWithChannel(BuildContext context) {
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateFormat();
final previewLineCount = context.select<AppSettings, int>((v) => v.messagePreviewLength);
return Card.filled(
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
@@ -113,7 +118,7 @@ class MessageListItem extends StatelessWidget {
),
Expanded(child: SizedBox()),
Text(
_dateFormat.format(DateTime.parse(message.timestamp).toLocal()),
dateFormat.format(DateTime.parse(message.timestamp).toLocal()),
style: const TextStyle(fontWeight: FontWeight.normal, fontSize: 11),
overflow: TextOverflow.clip,
maxLines: 1,
@@ -132,10 +137,10 @@ class MessageListItem extends StatelessWidget {
children: [
Expanded(
child: Text(
processContent(message.content),
processContent(message.content, previewLineCount),
style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)),
overflow: TextOverflow.ellipsis,
maxLines: _lineCount,
maxLines: previewLineCount,
),
),
if (message.priority == 2) SizedBox(width: 4),
@@ -151,7 +156,7 @@ class MessageListItem extends StatelessWidget {
);
}
String processContent(String? v) {
String processContent(String? v, int lineCount) {
if (v == null) {
return '';
}
@@ -161,7 +166,7 @@ class MessageListItem extends StatelessWidget {
return '';
}
return lines.sublist(0, min(_lineCount, lines.length)).join("\n").trim();
return lines.sublist(0, min(lineCount, lines.length)).join("\n").trim();
}
String processTitle(String? v) {

View File

@@ -5,6 +5,7 @@ import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:share_plus/share_plus.dart';
import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/components/error_display/error_display.dart';
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/models/keytoken.dart';
@@ -15,6 +16,7 @@ import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message
import 'package:simplecloudnotifier/pages/keytoken_view/keytoken_view.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/state/app_bar_state.dart';
import 'package:simplecloudnotifier/state/app_settings.dart';
import 'package:simplecloudnotifier/utils/navi.dart';
import 'package:simplecloudnotifier/utils/toaster.dart';
import 'package:simplecloudnotifier/utils/ui.dart';
@@ -37,8 +39,6 @@ class _MessageViewPageState extends State<MessageViewPage> {
late Future<(SCNMessage, ChannelPreview, KeyTokenPreview, UserPreview)>? mainFuture;
(SCNMessage, ChannelPreview, KeyTokenPreview, UserPreview)? mainFutureSnapshot = null;
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting
final ScrollController _controller = ScrollController();
bool _monospaceMode = false;
@@ -105,7 +105,7 @@ class _MessageViewPageState extends State<MessageViewPage> {
final (msg, chn, tok, usr) = snapshot.data!;
return _buildMessageView(context, msg, chn, tok, usr);
} else if (snapshot.hasError) {
return Center(child: Text('${snapshot.error}')); //TODO nice error page
return ErrorDisplay(errorMessage: '${snapshot.error}');
} else if (message != null && !this.message!.trimmed) {
return _buildMessageView(context, this.message!, null, null, null);
} else {
@@ -247,6 +247,8 @@ class _MessageViewPageState extends State<MessageViewPage> {
}
List<Widget> _buildMessageHeader(BuildContext context, SCNMessage message, ChannelPreview? channel) {
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateFormat();
return [
Row(
children: [
@@ -257,7 +259,7 @@ class _MessageViewPageState extends State<MessageViewPage> {
fontSize: 16,
),
Expanded(child: SizedBox()),
Text(_dateFormat.format(DateTime.parse(message.timestamp)), style: const TextStyle(fontSize: 14)),
Text(dateFormat.format(DateTime.parse(message.timestamp)), style: const TextStyle(fontSize: 14)),
],
),
SizedBox(height: 8),

View File

@@ -1,9 +1,10 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/models/sender_name_statistics.dart';
import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.dart';
import 'package:simplecloudnotifier/state/app_settings.dart';
import 'package:simplecloudnotifier/utils/navi.dart';
enum SenderListItemMode {
@@ -12,8 +13,6 @@ enum SenderListItemMode {
}
class SenderListItem extends StatelessWidget {
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting
const SenderListItem({
required this.item,
super.key,
@@ -23,6 +22,8 @@ class SenderListItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateFormat();
return Card.filled(
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
@@ -57,7 +58,7 @@ class SenderListItem extends StatelessWidget {
children: [
Expanded(
child: Text(
SenderListItem._dateFormat.format(DateTime.parse(item.lastTimestamp).toLocal()),
dateFormat.format(DateTime.parse(item.lastTimestamp).toLocal()),
style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)),
),
),

View File

@@ -1,17 +0,0 @@
import 'package:flutter/material.dart';
class SettingsRootPage extends StatefulWidget {
const SettingsRootPage({super.key, required bool isVisiblePage});
@override
State<SettingsRootPage> createState() => _SettingsRootPageState();
}
class _SettingsRootPageState extends State<SettingsRootPage> {
@override
Widget build(BuildContext context) {
return Center(
child: Text('(coming soon...)'), //TODO
);
}
}

View File

@@ -0,0 +1,117 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class SettingsNumberModal extends StatefulWidget {
final String title;
final int currentValue;
final int minValue;
final int maxValue;
final ValueChanged<int> onValueChanged;
const SettingsNumberModal({
Key? key,
required this.title,
required this.currentValue,
required this.minValue,
required this.maxValue,
required this.onValueChanged,
}) : super(key: key);
@override
State<SettingsNumberModal> createState() => _SettingsNumberModalState();
static Future<void> show(
BuildContext context, {
required String title,
required int currentValue,
required int minValue,
required int maxValue,
required ValueChanged<int> onValueChanged,
}) {
return showDialog(
context: context,
builder: (context) => SettingsNumberModal(
title: title,
currentValue: currentValue,
minValue: minValue,
maxValue: maxValue,
onValueChanged: onValueChanged,
),
);
}
}
class _SettingsNumberModalState extends State<SettingsNumberModal> {
late TextEditingController _controller;
late int selectedValue;
@override
void initState() {
super.initState();
selectedValue = widget.currentValue;
_controller = TextEditingController(text: widget.currentValue.toString());
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(widget.title),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: _controller,
keyboardType: TextInputType.number,
decoration: InputDecoration(
hintText: 'Enter a number',
errorText: _validateInput(),
),
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
onChanged: (value) {
setState(() {
selectedValue = int.tryParse(value) ?? widget.currentValue;
});
},
),
],
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Cancel'),
),
TextButton(
onPressed: _validateInput() == null
? () {
widget.onValueChanged(selectedValue);
Navigator.of(context).pop();
}
: null,
child: const Text('OK'),
),
],
);
}
String? _validateInput() {
final number = int.tryParse(_controller.text);
if (number == null) {
return 'Please enter a valid number';
}
if (number < widget.minValue) {
return 'Value must be at least ${widget.minValue}';
}
if (number > widget.maxValue) {
return 'Value must be at most ${widget.maxValue}';
}
return null;
}
}

View File

@@ -0,0 +1,43 @@
import 'package:flutter/material.dart';
import 'package:settings_ui/settings_ui.dart';
class SettingsPickerScreen<T> extends StatelessWidget {
const SettingsPickerScreen({
Key? key,
required this.title,
required this.initialValue,
required this.values,
required this.onValueChanged,
this.icons,
}) : super(key: key);
final String title;
final T initialValue;
final List<T> values;
final void Function(T value) onValueChanged;
final Widget Function(T v)? icons;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(title)),
body: SettingsList(
platform: PlatformUtils.detectPlatform(context),
sections: [
SettingsSection(
tiles: values.map((e) {
return SettingsTile(
leading: icons != null ? icons!(e) : null,
title: Text(e.toString()),
onPressed: (_) {
onValueChanged(e);
Navigator.of(context).pop();
},
);
}).toList(),
),
],
),
);
}
}

View File

@@ -0,0 +1,245 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart';
import 'package:settings_ui/settings_ui.dart';
import 'package:simplecloudnotifier/git_stamp/git_stamp.dart';
import 'package:simplecloudnotifier/pages/settings/settings_number_modal.dart';
import 'package:simplecloudnotifier/pages/settings/settings_picker_screen.dart';
import 'package:simplecloudnotifier/state/app_settings.dart';
import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/state/app_theme.dart';
import 'package:simplecloudnotifier/state/globals.dart';
import 'package:simplecloudnotifier/utils/navi.dart';
import 'package:simplecloudnotifier/utils/toaster.dart';
class SettingsRootPage extends StatefulWidget {
const SettingsRootPage({super.key, required bool isVisiblePage});
@override
State<SettingsRootPage> createState() => _SettingsRootPageState();
}
class _SettingsRootPageState extends State<SettingsRootPage> {
int _multiClickCounter = 0;
DateTime? _lastClickTime = null;
@override
Widget build(BuildContext context) {
final cfg = Provider.of<AppSettings>(context);
final thm = Provider.of<AppTheme>(context);
return SettingsList(
platform: PlatformUtils.detectPlatform(context),
contentPadding: EdgeInsets.fromLTRB(0, 0, 0, 24),
sections: [
SettingsSection(
title: Text('General'),
tiles: [
SettingsTile.navigation(
leading: Icon(thm.darkMode ? FontAwesomeIcons.solidMoon : FontAwesomeIcons.solidSun),
title: Text('Theme'),
value: Text(thm.darkMode ? 'Dark' : 'Light'),
onPressed: (_) => thm.switchDarkMode(),
),
SettingsTile.navigation(
leading: Icon(FontAwesomeIcons.solidSquare, color: thm.color.value),
title: Text('Color'),
value: Text(thm.color.displayStr),
onPressed: (_) => Navi.push(
context,
() => SettingsPickerScreen(
title: 'Color',
initialValue: thm.color,
values: ThemeColor.values,
icons: (v) => Icon(FontAwesomeIcons.solidSquare, color: v.value),
onValueChanged: (value) => AppTheme().setColor(value),
),
),
),
SettingsTile.navigation(
leading: Icon(FontAwesomeIcons.solidLineColumns),
title: Text('Message Preview Lines'),
value: Text("${cfg.messagePreviewLength}"),
onPressed: (_) {
SettingsNumberModal.show(
context,
title: 'Message Preview Lines',
currentValue: cfg.messagePreviewLength,
minValue: 1,
maxValue: 32,
onValueChanged: (value) => AppSettings().update((p) => p.messagePreviewLength = value),
);
},
),
if (Platform.isAndroid)
SettingsTile.switchTile(
initialValue: cfg.groupNotifications,
leading: Icon(FontAwesomeIcons.solidLayerGroup),
title: Text('Group notifications together'),
onToggle: (value) => AppSettings().update((p) => p.groupNotifications = !p.groupNotifications),
),
SettingsTile.navigation(
leading: Icon(FontAwesomeIcons.solidCalendarDays),
title: Text('Date Format'),
value: Text(cfg.dateFormat.displayStr),
onPressed: (_) => Navi.push(
context,
() => SettingsPickerScreen(
title: 'Date Format',
initialValue: cfg.dateFormat,
values: AppSettingsDateFormat.values,
onValueChanged: (value) => AppSettings().update((p) => p.dateFormat = value),
),
),
),
],
),
SettingsSection(
title: Text('Priority 0 (Low)'),
tiles: _buildNotificationTiles(context, cfg, 0),
),
SettingsSection(
title: Text('Priority 1 (Normal)'),
tiles: _buildNotificationTiles(context, cfg, 1),
),
SettingsSection(
title: Text('Priority 2 (High)'),
tiles: _buildNotificationTiles(context, cfg, 2),
),
SettingsSection(
title: Text('Advanced Settings'),
tiles: [
if (cfg.devMode)
SettingsTile.switchTile(
initialValue: cfg.showDebugButton,
leading: Icon(FontAwesomeIcons.solidSpiderBlackWidow),
title: Text('Debug Button anzeigen'),
onToggle: (value) => AppSettings().update((p) => p.showDebugButton = !p.showDebugButton),
),
SettingsTile.navigation(
leading: Icon(FontAwesomeIcons.solidList),
title: Text('Page Size (Messages)'),
value: Text("${cfg.messagePageSize}"),
onPressed: (_) {
SettingsNumberModal.show(
context,
title: 'Page Size (Messages)',
currentValue: cfg.messagePageSize,
minValue: 1,
maxValue: 2048,
onValueChanged: (value) => AppSettings().update((p) => p.messagePageSize = value),
);
},
),
SettingsTile.switchTile(
initialValue: cfg.backgroundRefreshMessageListOnPop,
leading: Icon(FontAwesomeIcons.solidPageCaretDown),
title: Text('Refresh messages on page navigation'),
onToggle: (value) => AppSettings().update((p) => p.backgroundRefreshMessageListOnPop = !p.backgroundRefreshMessageListOnPop),
),
SettingsTile.switchTile(
initialValue: cfg.alwaysBackgroundRefreshMessageListOnLifecycleResume,
leading: Icon(FontAwesomeIcons.solidRecycle),
title: Text('Refresh messages on app resume'),
onToggle: (value) => AppSettings().update((p) => p.alwaysBackgroundRefreshMessageListOnLifecycleResume = !p.alwaysBackgroundRefreshMessageListOnLifecycleResume),
),
],
),
SettingsSection(
title: Text('About'),
tiles: [
SettingsTile.navigation(
leading: Icon(FontAwesomeIcons.solidCodeCommit),
title: Text('Version'),
value: Text(Globals().version),
onPressed: (cfg.devMode)
? null
: (context) {
if (_lastClickTime == null || DateTime.now().difference(_lastClickTime!).inSeconds > 1) _multiClickCounter = 0;
_multiClickCounter++;
_lastClickTime = DateTime.now();
if (_multiClickCounter >= 12) {
Toaster.info("Debug", "Developer mode enabled");
AppSettings().update((p) {
p.devMode = true;
p.showDebugButton = true;
});
}
},
),
SettingsTile.navigation(
leading: Icon(FontAwesomeIcons.solidCodeBranch),
title: Text('Build'),
value: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(GitStamp.sha.substring(0, 7) + ' +' + Globals().buildNumber),
Text("( " + cfg.dateFormat.dateFormat().format(DateTime.parse(GitStamp.buildDateTime).toLocal()) + " )", style: TextStyle(fontStyle: FontStyle.italic)),
],
),
onPressed: (context) => _clipboardCopy(GitStamp.sha),
),
SettingsTile.navigation(
leading: Icon(FontAwesomeIcons.solidBell),
title: Text('FCM Token'),
value: Text(AppAuth().getToken()),
onPressed: (context) => _clipboardCopy(AppAuth().getToken()),
),
],
),
],
);
}
void _clipboardCopy(String v) {
Clipboard.setData(new ClipboardData(text: v));
Toaster.info("Clipboard", 'Copied to Clipboard');
print('================= [CLIPBOARD] =================\n${v}\n================= [/CLIPBOARD] =================');
}
List<AbstractSettingsTile> _buildNotificationTiles(BuildContext context, AppSettings cfg, int prio) {
final ncf = AppSettings().getNotificationSettings(prio);
return [
SettingsTile.switchTile(
initialValue: ncf.enableLights,
leading: Icon(FontAwesomeIcons.solidLightbulb),
title: Text('Enable Lights'),
onToggle: (value) => AppSettings().updateNotification(prio, (p) => p.withEnableLights(!p.enableLights)),
),
SettingsTile.switchTile(
initialValue: ncf.enableVibration,
leading: Icon(FontAwesomeIcons.solidShutters),
title: Text('Enable Vibration'),
onToggle: (value) => AppSettings().updateNotification(prio, (p) => p.withEnableVibration(!p.enableVibration)),
),
SettingsTile.navigation(
leading: Icon(FontAwesomeIcons.solidWaveform),
title: Text('Notification Sound'),
value: Text(ncf.sound ?? '(Default)'),
onPressed: (context) => {/*TODO*/},
),
SettingsTile.switchTile(
initialValue: ncf.playSound,
leading: Icon(FontAwesomeIcons.solidVolume),
title: Text('Play Sound'),
onToggle: (value) => AppSettings().updateNotification(prio, (p) => p.withPlaySound(!p.playSound)),
),
SettingsTile.switchTile(
initialValue: ncf.silent,
leading: Icon(FontAwesomeIcons.solidVolumeSlash),
title: Text('Silent'),
onToggle: (value) => AppSettings().updateNotification(prio, (p) => p.withSilent(!p.silent)),
),
SettingsTile.navigation(
leading: Icon(FontAwesomeIcons.solidStopwatch20),
title: Text('Auto Timeout'),
value: Text((ncf.timeoutAfter != null) ? "${ncf.timeoutAfter} sec" : "(None)"),
onPressed: (context) => {/*TODO*/},
),
];
}
}

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/models/subscription.dart';
@@ -15,8 +14,6 @@ enum SubscriptionListItemMode {
}
class SubscriptionListItem extends StatelessWidget {
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting
const SubscriptionListItem({
required this.item,
required this.userCache,

View File

@@ -10,6 +10,7 @@ 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/app_bar_state.dart';
import 'package:simplecloudnotifier/state/app_settings.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/types/immediate_future.dart';
import 'package:simplecloudnotifier/utils/dialogs.dart';
@@ -40,8 +41,6 @@ enum EditState { none, editing, saving }
enum SubscriptionViewPageInitState { loading, okay, error }
class _SubscriptionViewPageState extends State<SubscriptionViewPage> {
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting
ImmediateFuture<UserPreview> _futureChannelOwner = ImmediateFuture.ofPending();
ImmediateFuture<UserPreview> _futureSubscriber = ImmediateFuture.ofPending();
ImmediateFuture<ChannelPreview> _futureChannel = ImmediateFuture.ofPending();
@@ -161,6 +160,8 @@ class _SubscriptionViewPageState extends State<SubscriptionViewPage> {
}
Widget _buildOwnedSubscriptionView(BuildContext context, Subscription subscription) {
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateFormat();
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
@@ -181,7 +182,7 @@ class _SubscriptionViewPageState extends State<SubscriptionViewPage> {
context: context,
icon: FontAwesomeIcons.clock,
title: 'Created',
values: [_SubscriptionViewPageState._dateFormat.format(DateTime.parse(subscription.timestampCreated).toLocal())],
values: [dateFormat.format(DateTime.parse(subscription.timestampCreated).toLocal())],
),
_buildStatusCard(context),
UI.button(text: "Unsubscribe", onPressed: _unsubscribe, tonal: true),
@@ -192,6 +193,8 @@ class _SubscriptionViewPageState extends State<SubscriptionViewPage> {
}
Widget _buildIncomingSubscriptionView(BuildContext context, Subscription subscription) {
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateFormat();
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
@@ -212,7 +215,7 @@ class _SubscriptionViewPageState extends State<SubscriptionViewPage> {
context: context,
icon: FontAwesomeIcons.clock,
title: 'Created',
values: [_SubscriptionViewPageState._dateFormat.format(DateTime.parse(subscription.timestampCreated).toLocal())],
values: [dateFormat.format(DateTime.parse(subscription.timestampCreated).toLocal())],
),
_buildStatusCard(context),
if (subscription.confirmed) UI.button(text: "Revoke subscription", onPressed: _unsubscribe, color: Colors.red),
@@ -225,6 +228,8 @@ class _SubscriptionViewPageState extends State<SubscriptionViewPage> {
}
Widget _buildOutgoingSubscriptionView(BuildContext context, Subscription subscription) {
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateFormat();
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
@@ -245,7 +250,7 @@ class _SubscriptionViewPageState extends State<SubscriptionViewPage> {
context: context,
icon: FontAwesomeIcons.clock,
title: 'Created',
values: [_SubscriptionViewPageState._dateFormat.format(DateTime.parse(subscription.timestampCreated).toLocal())],
values: [dateFormat.format(DateTime.parse(subscription.timestampCreated).toLocal())],
),
_buildStatusCard(context),
if (subscription.confirmed && subscription.active) UI.button(text: "Deactivate subscription", onPressed: _deactivate, tonal: true),