diff --git a/flutter/.gitignore b/flutter/.gitignore index 8f08388..57c8c52 100644 --- a/flutter/.gitignore +++ b/flutter/.gitignore @@ -56,3 +56,5 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +/lib/git_stamp/ diff --git a/flutter/Makefile b/flutter/Makefile index 4ae6dc5..fa52657 100644 --- a/flutter/Makefile +++ b/flutter/Makefile @@ -9,28 +9,28 @@ # runs app locally (linux) -run-linux: +run-linux: gen dart run build_runner build _JAVA_OPTIONS="" flutter run -d linux # runs app locally (web | not really supported) -run-web: +run-web: gen dart run build_runner build _JAVA_OPTIONS="" flutter run -d chrome # runs on android device (must have network adb enabled teh correct IP) -run-android: +run-android: gen ping -c1 10.10.10.177 adb connect 10.10.10.177:5555 flutter pub run build_runner build _JAVA_OPTIONS="" flutter run -d 10.10.10.177:5555 -install-release: +install-release: gen # Install on Pixel 7a flutter build apk --release flutter run --release -d 35221JEHN07157 -build-release: +build-release: gen flutter build apk --release flutter build appbundle --release flutter build linux --release @@ -42,7 +42,9 @@ fix: dart fix --apply gen: + ./_utils/inc_buildnum.sh dart run build_runner build + dart run git_stamp git_stamp --build-type lite --limit 2 # run `make run` in another terminal (or another variant of flutter run) autoreload: diff --git a/flutter/_utils/inc_buildnum.sh b/flutter/_utils/inc_buildnum.sh new file mode 100755 index 0000000..410e8a2 --- /dev/null +++ b/flutter/_utils/inc_buildnum.sh @@ -0,0 +1,41 @@ +#!/bin/bash + + # 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 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. + + # shellcheck disable=SC2034 + cr=$'\n' + + function black() { if [ -t 1 ] && [ -n "$(tput colors)" ] && [ "$(tput colors)" -ge 8 ]; then echo -e "\\x1B[30m$1\\x1B[0m"; else echo "$1"; fi } + function red() { if [ -t 1 ] && [ -n "$(tput colors)" ] && [ "$(tput colors)" -ge 8 ]; then echo -e "\\x1B[31m$1\\x1B[0m"; else echo "$1"; fi; } + function green() { if [ -t 1 ] && [ -n "$(tput colors)" ] && [ "$(tput colors)" -ge 8 ]; then echo -e "\\x1B[32m$1\\x1B[0m"; else echo "$1"; fi; } + function yellow(){ if [ -t 1 ] && [ -n "$(tput colors)" ] && [ "$(tput colors)" -ge 8 ]; then echo -e "\\x1B[33m$1\\x1B[0m"; else echo "$1"; fi; } + function blue() { if [ -t 1 ] && [ -n "$(tput colors)" ] && [ "$(tput colors)" -ge 8 ]; then echo -e "\\x1B[34m$1\\x1B[0m"; else echo "$1"; fi; } + function purple(){ if [ -t 1 ] && [ -n "$(tput colors)" ] && [ "$(tput colors)" -ge 8 ]; then echo -e "\\x1B[35m$1\\x1B[0m"; else echo "$1"; fi; } + function cyan() { if [ -t 1 ] && [ -n "$(tput colors)" ] && [ "$(tput colors)" -ge 8 ]; then echo -e "\\x1B[36m$1\\x1B[0m"; else echo "$1"; fi; } + function white() { if [ -t 1 ] && [ -n "$(tput colors)" ] && [ "$(tput colors)" -ge 8 ]; then echo -e "\\x1B[37m$1\\x1B[0m"; else echo "$1"; fi; } + + + + +path_to_pubspec="$(dirname "$0")/../pubspec.yaml" +current_version=$(awk '/^version:/ {print $2}' $path_to_pubspec) +current_version_without_build=$(echo "$current_version" | sed 's/\+.*//') + +gitcount="$(git log | grep "^commit" | wc -l | xargs)" +new_version="$current_version_without_build+$gitcount" + +echo "Setting pubspec.yaml version $current_version to $new_version" + +if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS sed (requires a space after -i) + sed -i '' -e "s/version: $current_version/version: $new_version/g" $path_to_pubspec +else + # GNU sed (requires no space after -i) + sed -i'' -e "s/version: $current_version/version: $new_version/g" $path_to_pubspec +fi diff --git a/flutter/lib/components/layout/app_bar.dart b/flutter/lib/components/layout/app_bar.dart index 81b4fa2..f9ead34 100644 --- a/flutter/lib/components/layout/app_bar.dart +++ b/flutter/lib/components/layout/app_bar.dart @@ -5,7 +5,7 @@ import 'package:simplecloudnotifier/components/layout/app_bar_filter_dialog.dart import 'package:simplecloudnotifier/components/layout/app_bar_progress_indicator.dart'; import 'package:simplecloudnotifier/pages/debug/debug_main.dart'; import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.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/app_theme.dart'; @@ -64,7 +64,7 @@ class _SCNAppBarState extends State { builder: (context, appTheme, child) => IconButton( icon: Icon(appTheme.darkMode ? FontAwesomeIcons.solidSun : FontAwesomeIcons.solidMoon), tooltip: appTheme.darkMode ? 'Light mode' : 'Dark mode', - onPressed: appTheme.switchDarkMode, + onPressed: AppTheme().switchDarkMode, ), )); } else { diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 2cdda2d..5974862 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -14,7 +14,7 @@ import 'package:simplecloudnotifier/models/scn_message.dart'; import 'package:simplecloudnotifier/nav_layout.dart'; import 'package:simplecloudnotifier/pages/channel_view/channel_view.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/app_theme.dart'; @@ -248,8 +248,10 @@ class SCNApp extends StatelessWidget { title: 'SimpleCloudNotifier', navigatorObservers: [Navi.routeObserver, Navi.modalRouteObserver], theme: ThemeData( - //TODO color settings - colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue, brightness: appTheme.darkMode ? Brightness.dark : Brightness.light), + colorScheme: ColorScheme.fromSeed( + seedColor: appTheme.color.value, + brightness: appTheme.darkMode ? Brightness.dark : Brightness.light, + ), useMaterial3: true, ), home: SCNNavLayout(), @@ -343,7 +345,7 @@ Future _receiveMessage(RemoteMessage message, bool foreground) async { } catch (exc, trace) { ApplicationLog.writeRawFailure('Failed to init hive', {'exception': exc.toString(), 'trace': trace.toString(), 'data': message, 'foreground': foreground}); ApplicationLog.error('Failed to init hive:' + exc.toString(), trace: trace); - Notifier.showLocalNotification("", "@ERROR", "@ERROR", 'Error Channel', "Error", "Failed to receive SCN message (init failed)", null); + Notifier.showLocalNotification("", "@ERROR", "@ERROR", 'Error Channel', "Error", "Failed to receive SCN message (init failed)", null, null); return; } @@ -359,12 +361,13 @@ Future _receiveMessage(RemoteMessage message, bool foreground) async { final channel = message.data['channel'] as String; final channel_id = message.data['channel_id'] as String; final body = message.data['body'] as String; + final prio = int.parse(message.data['priority'] as String); - Notifier.showLocalNotification(scn_msg_id, channel_id, channel, 'Channel: ${channel}', title, body, timestamp); + Notifier.showLocalNotification(scn_msg_id, channel_id, channel, 'Channel: ${channel}', title, body, timestamp, prio); } catch (exc, trace) { ApplicationLog.writeRawFailure('Failed to decode received FB message', {'exception': exc.toString(), 'trace': trace.toString(), 'data': message, 'foreground': foreground}); ApplicationLog.error('Failed to decode received FB message' + exc.toString(), trace: trace); - Notifier.showLocalNotification("", "@ERROR", "@ERROR", 'Error Channel', "Error", "Failed to receive SCN message (decode failed)", null); + Notifier.showLocalNotification("", "@ERROR", "@ERROR", 'Error Channel', "Error", "Failed to receive SCN message (decode failed)", null, null); return; } @@ -373,7 +376,7 @@ Future _receiveMessage(RemoteMessage message, bool foreground) async { } catch (exc, trace) { ApplicationLog.writeRawFailure('Failed to persist received FB message', {'exception': exc.toString(), 'trace': trace.toString(), 'data': message, 'foreground': foreground}); ApplicationLog.error('Failed to persist received FB message' + exc.toString(), trace: trace); - Notifier.showLocalNotification("", "@ERROR", "@ERROR", 'Error Channel', "Error", "Failed to receive SCN message (persist failed)", null); + Notifier.showLocalNotification("", "@ERROR", "@ERROR", 'Error Channel', "Error", "Failed to receive SCN message (persist failed)", null, null); return; } diff --git a/flutter/lib/nav_layout.dart b/flutter/lib/nav_layout.dart index 55e1687..e30598b 100644 --- a/flutter/lib/nav_layout.dart +++ b/flutter/lib/nav_layout.dart @@ -8,7 +8,7 @@ import 'package:simplecloudnotifier/pages/send/send.dart'; import 'package:simplecloudnotifier/components/bottom_fab/fab_bottom_app_bar.dart'; import 'package:simplecloudnotifier/pages/account/account.dart'; import 'package:simplecloudnotifier/pages/message_list/message_list.dart'; -import 'package:simplecloudnotifier/pages/settings/root.dart'; +import 'package:simplecloudnotifier/pages/settings/settings_view.dart'; import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/utils/toaster.dart'; diff --git a/flutter/lib/pages/channel_list/channel_list_item.dart b/flutter/lib/pages/channel_list/channel_list_item.dart index 06ca685..cd02c22 100644 --- a/flutter/lib/pages/channel_list/channel_list_item.dart +++ b/flutter/lib/pages/channel_list/channel_list_item.dart @@ -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 { @override Widget build(BuildContext context) { + final dateFormat = context.select((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 { ), ), 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), ), ], diff --git a/flutter/lib/pages/channel_message_view/channel_message_view.dart b/flutter/lib/pages/channel_message_view/channel_message_view.dart index de7a086..8815cf7 100644 --- a/flutter/lib/pages/channel_message_view/channel_message_view.dart +++ b/flutter/lib/pages/channel_message_view/channel_message_view.dart @@ -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'; diff --git a/flutter/lib/pages/client_list/client_list_item.dart b/flutter/lib/pages/client_list/client_list_item.dart index ea8f2f4..78b0ba3 100644 --- a/flutter/lib/pages/client_list/client_list_item.dart +++ b/flutter/lib/pages/client_list/client_list_item.dart @@ -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((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)), ), ], diff --git a/flutter/lib/pages/debug/debug_actions.dart b/flutter/lib/pages/debug/debug_actions.dart index 4b45fc5..4bad575 100644 --- a/flutter/lib/pages/debug/debug_actions.dart +++ b/flutter/lib/pages/debug/debug_actions.dart @@ -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 { 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', ), ], ), diff --git a/flutter/lib/pages/debug/debug_logs.dart b/flutter/lib/pages/debug/debug_logs.dart index 17ffec8..0dff287 100644 --- a/flutter/lib/pages/debug/debug_logs.dart +++ b/flutter/lib/pages/debug/debug_logs.dart @@ -11,7 +11,7 @@ class DebugLogsPage extends StatefulWidget { class _DebugLogsPageState extends State { Box logBox = Hive.box('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) { diff --git a/flutter/lib/pages/debug/debug_persistence_sharedprefs.dart b/flutter/lib/pages/debug/debug_persistence_sharedprefs.dart index c0cadab..e96226d 100644 --- a/flutter/lib/pages/debug/debug_persistence_sharedprefs.dart +++ b/flutter/lib/pages/debug/debug_persistence_sharedprefs.dart @@ -6,7 +6,9 @@ class DebugSharedPrefPage extends StatelessWidget { final SharedPreferences sharedPref; final List 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) { diff --git a/flutter/lib/pages/debug/debug_requests.dart b/flutter/lib/pages/debug/debug_requests.dart index cf1e4c1..72b0798 100644 --- a/flutter/lib/pages/debug/debug_requests.dart +++ b/flutter/lib/pages/debug/debug_requests.dart @@ -13,7 +13,7 @@ class DebugRequestsPage extends StatefulWidget { class _DebugRequestsPageState extends State { Box requestsBox = Hive.box('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) { diff --git a/flutter/lib/pages/filtered_message_view/filtered_message_view.dart b/flutter/lib/pages/filtered_message_view/filtered_message_view.dart index 66fe985..96252a2 100644 --- a/flutter/lib/pages/filtered_message_view/filtered_message_view.dart +++ b/flutter/lib/pages/filtered_message_view/filtered_message_view.dart @@ -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'; diff --git a/flutter/lib/pages/keytoken_list/keytoken_list_item.dart b/flutter/lib/pages/keytoken_list/keytoken_list_item.dart index 86735dc..fdefe60 100644 --- a/flutter/lib/pages/keytoken_list/keytoken_list_item.dart +++ b/flutter/lib/pages/keytoken_list/keytoken_list_item.dart @@ -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((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), ), ], diff --git a/flutter/lib/pages/keytoken_view/keytoken_view.dart b/flutter/lib/pages/keytoken_view/keytoken_view.dart index 390f0d7..e7b0401 100644 --- a/flutter/lib/pages/keytoken_view/keytoken_view.dart +++ b/flutter/lib/pages/keytoken_view/keytoken_view.dart @@ -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 { - static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting - ImmediateFuture _futureOwner = ImmediateFuture.ofPending(); ImmediateFuture> _futureAllChannels = ImmediateFuture.ofPending(); @@ -195,6 +192,8 @@ class _KeyTokenViewPageState extends State { } Widget _buildOwnedKeyTokenView(BuildContext context, KeyToken keytoken) { + final dateFormat = context.select((v) => v.dateFormat).dateFormat(); + return SingleChildScrollView( child: Padding( padding: const EdgeInsets.fromLTRB(24, 16, 24, 16), @@ -217,13 +216,13 @@ class _KeyTokenViewPageState extends State { 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 { } void _editPermissions() async { - final acc = Provider.of(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 { } void _editChannels() async { - final acc = Provider.of(context, listen: false); - if (keytokenUserAccAdmin == null || keytokenUserAccAdmin!.keytokenID == keytokenPreview!.keytokenID) { Toaster.error("Error", "You cannot edit the currently used token"); return; diff --git a/flutter/lib/pages/message_list/message_list.dart b/flutter/lib/pages/message_list/message_list.dart index 84293ae..c3bc7cf 100644 --- a/flutter/lib/pages/message_list/message_list.dart +++ b/flutter/lib/pages/message_list/message_list.dart @@ -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'; diff --git a/flutter/lib/pages/message_list/message_list_item.dart b/flutter/lib/pages/message_list/message_list_item.dart index 0725f95..4b0739d 100644 --- a/flutter/lib/pages/message_list/message_list_item.dart +++ b/flutter/lib/pages/message_list/message_list_item.dart @@ -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((v) => v.dateFormat).dateFormat(); + final previewLineCount = context.select((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((v) => v.dateFormat).dateFormat(); + final previewLineCount = context.select((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) { diff --git a/flutter/lib/pages/message_view/message_view.dart b/flutter/lib/pages/message_view/message_view.dart index bf3f411..8c8200b 100644 --- a/flutter/lib/pages/message_view/message_view.dart +++ b/flutter/lib/pages/message_view/message_view.dart @@ -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 { 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 { 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 { } List _buildMessageHeader(BuildContext context, SCNMessage message, ChannelPreview? channel) { + final dateFormat = context.select((v) => v.dateFormat).dateFormat(); + return [ Row( children: [ @@ -257,7 +259,7 @@ class _MessageViewPageState extends State { 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), diff --git a/flutter/lib/pages/sender_list/sender_list_item.dart b/flutter/lib/pages/sender_list/sender_list_item.dart index 3e9df98..685ff2f 100644 --- a/flutter/lib/pages/sender_list/sender_list_item.dart +++ b/flutter/lib/pages/sender_list/sender_list_item.dart @@ -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((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)), ), ), diff --git a/flutter/lib/pages/settings/root.dart b/flutter/lib/pages/settings/root.dart deleted file mode 100644 index 6cb457e..0000000 --- a/flutter/lib/pages/settings/root.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter/material.dart'; - -class SettingsRootPage extends StatefulWidget { - const SettingsRootPage({super.key, required bool isVisiblePage}); - - @override - State createState() => _SettingsRootPageState(); -} - -class _SettingsRootPageState extends State { - @override - Widget build(BuildContext context) { - return Center( - child: Text('(coming soon...)'), //TODO - ); - } -} diff --git a/flutter/lib/pages/settings/settings_number_modal.dart b/flutter/lib/pages/settings/settings_number_modal.dart new file mode 100644 index 0000000..6cb3d57 --- /dev/null +++ b/flutter/lib/pages/settings/settings_number_modal.dart @@ -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 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 createState() => _SettingsNumberModalState(); + + static Future show( + BuildContext context, { + required String title, + required int currentValue, + required int minValue, + required int maxValue, + required ValueChanged onValueChanged, + }) { + return showDialog( + context: context, + builder: (context) => SettingsNumberModal( + title: title, + currentValue: currentValue, + minValue: minValue, + maxValue: maxValue, + onValueChanged: onValueChanged, + ), + ); + } +} + +class _SettingsNumberModalState extends State { + 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; + } +} diff --git a/flutter/lib/pages/settings/settings_picker_screen.dart b/flutter/lib/pages/settings/settings_picker_screen.dart new file mode 100644 index 0000000..6619cc7 --- /dev/null +++ b/flutter/lib/pages/settings/settings_picker_screen.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:settings_ui/settings_ui.dart'; + +class SettingsPickerScreen 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 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(), + ), + ], + ), + ); + } +} diff --git a/flutter/lib/pages/settings/settings_view.dart b/flutter/lib/pages/settings/settings_view.dart new file mode 100644 index 0000000..a6902ae --- /dev/null +++ b/flutter/lib/pages/settings/settings_view.dart @@ -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 createState() => _SettingsRootPageState(); +} + +class _SettingsRootPageState extends State { + int _multiClickCounter = 0; + DateTime? _lastClickTime = null; + + @override + Widget build(BuildContext context) { + final cfg = Provider.of(context); + final thm = Provider.of(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 _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*/}, + ), + ]; + } +} diff --git a/flutter/lib/pages/subscription_list/subscription_list_item.dart b/flutter/lib/pages/subscription_list/subscription_list_item.dart index c124840..2b37cb3 100644 --- a/flutter/lib/pages/subscription_list/subscription_list_item.dart +++ b/flutter/lib/pages/subscription_list/subscription_list_item.dart @@ -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, diff --git a/flutter/lib/pages/subscription_view/subscription_view.dart b/flutter/lib/pages/subscription_view/subscription_view.dart index 4ad0f6a..e81465d 100644 --- a/flutter/lib/pages/subscription_view/subscription_view.dart +++ b/flutter/lib/pages/subscription_view/subscription_view.dart @@ -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 { - static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting - ImmediateFuture _futureChannelOwner = ImmediateFuture.ofPending(); ImmediateFuture _futureSubscriber = ImmediateFuture.ofPending(); ImmediateFuture _futureChannel = ImmediateFuture.ofPending(); @@ -161,6 +160,8 @@ class _SubscriptionViewPageState extends State { } Widget _buildOwnedSubscriptionView(BuildContext context, Subscription subscription) { + final dateFormat = context.select((v) => v.dateFormat).dateFormat(); + return SingleChildScrollView( child: Padding( padding: const EdgeInsets.fromLTRB(24, 16, 24, 16), @@ -181,7 +182,7 @@ class _SubscriptionViewPageState extends State { 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 { } Widget _buildIncomingSubscriptionView(BuildContext context, Subscription subscription) { + final dateFormat = context.select((v) => v.dateFormat).dateFormat(); + return SingleChildScrollView( child: Padding( padding: const EdgeInsets.fromLTRB(24, 16, 24, 16), @@ -212,7 +215,7 @@ class _SubscriptionViewPageState extends State { 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 { } Widget _buildOutgoingSubscriptionView(BuildContext context, Subscription subscription) { + final dateFormat = context.select((v) => v.dateFormat).dateFormat(); + return SingleChildScrollView( child: Padding( padding: const EdgeInsets.fromLTRB(24, 16, 24, 16), @@ -245,7 +250,7 @@ class _SubscriptionViewPageState extends State { 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), diff --git a/flutter/lib/settings/app_settings.dart b/flutter/lib/settings/app_settings.dart deleted file mode 100644 index 424ce06..0000000 --- a/flutter/lib/settings/app_settings.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:flutter/material.dart'; - -class AppSettings extends ChangeNotifier { - bool groupNotifications = true; - int messagePageSize = 128; - bool showDebugButton = true; - bool backgroundRefreshMessageListOnPop = false; - bool alwaysBackgroundRefreshMessageListOnLifecycleResume = true; - - static AppSettings? _singleton = AppSettings._internal(); - - factory AppSettings() { - return _singleton ?? (_singleton = AppSettings._internal()); - } - - AppSettings._internal() { - load(); - } - - void clear() { - //TODO - - notifyListeners(); - } - - void load() { - //TODO - - notifyListeners(); - } - - Future save() async { - //TODO - } -} diff --git a/flutter/lib/state/app_settings.dart b/flutter/lib/state/app_settings.dart new file mode 100644 index 0000000..b3566eb --- /dev/null +++ b/flutter/lib/state/app_settings.dart @@ -0,0 +1,212 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:intl/intl.dart'; +import 'package:shared_preferences/src/shared_preferences_legacy.dart'; +import 'package:simplecloudnotifier/state/globals.dart'; + +enum AppSettingsDateFormat { + ISO(displayStr: 'ISO (yyyy-MM-dd)', key: 'ISO'), + German(displayStr: 'German (dd.MM.yyyy)', key: 'German'), + US(displayStr: 'US (MM/dd/yyyy)', key: 'US'); + + const AppSettingsDateFormat({required this.displayStr, required this.key}); + + final String displayStr; + final String key; + + @override + toString() => displayStr; + + DateFormat dateFormat() { + switch (this) { + case AppSettingsDateFormat.ISO: + return DateFormat('yyyy-MM-dd HH:mm'); + case AppSettingsDateFormat.German: + return DateFormat('dd.MM.yyyy HH:mm'); + case AppSettingsDateFormat.US: + return DateFormat('MM/dd/yyyy HH:mm'); + } + } + + static AppSettingsDateFormat? parse(String? string) { + if (string == null) return null; + return values.firstWhere((e) => e.key == string, orElse: null); + } +} + +class AppSettings extends ChangeNotifier { + bool groupNotifications = true; + int messagePageSize = 128; + bool devMode = false; + bool showDebugButton = false; + bool backgroundRefreshMessageListOnPop = false; + bool alwaysBackgroundRefreshMessageListOnLifecycleResume = true; + AppSettingsDateFormat dateFormat = AppSettingsDateFormat.ISO; + int messagePreviewLength = 3; + + AppNotificationSettings notification0 = AppNotificationSettings(); + AppNotificationSettings notification1 = AppNotificationSettings(); + AppNotificationSettings notification2 = AppNotificationSettings(); + + static AppSettings? _singleton = AppSettings._internal(); + + factory AppSettings() { + return _singleton ?? (_singleton = AppSettings._internal()); + } + + AppSettings._internal() { + load(); + } + + void reset() { + groupNotifications = true; + messagePageSize = 128; + devMode = false; + showDebugButton = false; + backgroundRefreshMessageListOnPop = false; + alwaysBackgroundRefreshMessageListOnLifecycleResume = true; + dateFormat = AppSettingsDateFormat.ISO; + messagePreviewLength = 3; + + notification0 = AppNotificationSettings(); + notification1 = AppNotificationSettings(); + notification2 = AppNotificationSettings(); + + notifyListeners(); + } + + void load() { + groupNotifications = Globals().sharedPrefs.getBool('settings.groupNotifications') ?? groupNotifications; + messagePageSize = Globals().sharedPrefs.getInt('settings.messagePageSize') ?? messagePageSize; + devMode = Globals().sharedPrefs.getBool('settings.devMode') ?? devMode; + showDebugButton = Globals().sharedPrefs.getBool('settings.showDebugButton') ?? showDebugButton; + backgroundRefreshMessageListOnPop = Globals().sharedPrefs.getBool('settings.backgroundRefreshMessageListOnPop') ?? backgroundRefreshMessageListOnPop; + alwaysBackgroundRefreshMessageListOnLifecycleResume = Globals().sharedPrefs.getBool('settings.alwaysBackgroundRefreshMessageListOnLifecycleResume') ?? alwaysBackgroundRefreshMessageListOnLifecycleResume; + dateFormat = AppSettingsDateFormat.parse(Globals().sharedPrefs.getString('settings.dateFormat')) ?? dateFormat; + messagePreviewLength = Globals().sharedPrefs.getInt('settings.messagePreviewLength') ?? messagePreviewLength; + + notification0 = AppNotificationSettings.load(Globals().sharedPrefs, 'settings.notification0'); + notification1 = AppNotificationSettings.load(Globals().sharedPrefs, 'settings.notification1'); + notification2 = AppNotificationSettings.load(Globals().sharedPrefs, 'settings.notification2'); + } + + Future save() async { + await Globals().sharedPrefs.setBool('settings.groupNotifications', groupNotifications); + await Globals().sharedPrefs.setInt('settings.messagePageSize', messagePageSize); + await Globals().sharedPrefs.setBool('settings.devMode', devMode); + await Globals().sharedPrefs.setBool('settings.showDebugButton', showDebugButton); + await Globals().sharedPrefs.setBool('settings.backgroundRefreshMessageListOnPop', backgroundRefreshMessageListOnPop); + await Globals().sharedPrefs.setBool('settings.alwaysBackgroundRefreshMessageListOnLifecycleResume', alwaysBackgroundRefreshMessageListOnLifecycleResume); + await Globals().sharedPrefs.setString('settings.dateFormat', dateFormat.key); + await Globals().sharedPrefs.setInt('settings.messagePreviewLength', messagePreviewLength); + + await notification0.save(Globals().sharedPrefs, 'settings.notification0'); + await notification1.save(Globals().sharedPrefs, 'settings.notification1'); + await notification2.save(Globals().sharedPrefs, 'settings.notification2'); + } + + void update(void Function(AppSettings p) fn) { + fn(this); + save(); + notifyListeners(); + } + + void updateNotification(int prio, AppNotificationSettings Function(AppNotificationSettings p) fn) { + if (prio == 0) { + notification0 = fn(notification0); + } else if (prio == 1) { + notification1 = fn(notification1); + } else if (prio == 2) { + notification2 = fn(notification2); + } + + save(); + notifyListeners(); + } + + AppNotificationSettings getNotificationSettings(int? prio) { + if (prio != null && prio == 0) { + return notification0; + } else if (prio != null && prio == 1) { + return notification1; + } else if (prio != null && prio == 2) { + return notification2; + } else { + return AppNotificationSettings(); + } + } +} + +class AppNotificationSettings { + // Immutable + AppNotificationSettings({ + this.enableLights = false, + this.enableVibration = true, + this.playSound = true, + this.sound = null, + this.silent = false, + this.timeoutAfter = null, + }); + + final bool enableLights; + final bool enableVibration; + final bool playSound; + final String? sound; + final bool silent; + final int? timeoutAfter; + + Future save(SharedPreferences sharedPrefs, String prefix) async { + await Globals().sharedPrefs.setBool('${prefix}.enableLights', enableLights); + await Globals().sharedPrefs.setBool('${prefix}.enableVibration', enableVibration); + await Globals().sharedPrefs.setBool('${prefix}.playSound', playSound); + await Globals().sharedPrefs.setString('${prefix}.sound', _encode(sound)); + await Globals().sharedPrefs.setBool('${prefix}.silent', silent); + await Globals().sharedPrefs.setString('${prefix}.timeoutAfter', _encode(timeoutAfter)); + } + + UriAndroidNotificationSound? soundURI() { + return (sound != null) ? UriAndroidNotificationSound(sound!) : null; + } + + AppNotificationSettings withEnableLights(bool v) => AppNotificationSettings(enableLights: v, enableVibration: enableVibration, playSound: playSound, sound: sound, silent: silent, timeoutAfter: timeoutAfter); + AppNotificationSettings withEnableVibration(bool v) => AppNotificationSettings(enableLights: enableLights, enableVibration: v, playSound: playSound, sound: sound, silent: silent, timeoutAfter: timeoutAfter); + AppNotificationSettings withPlaySound(bool v) => AppNotificationSettings(enableLights: enableLights, enableVibration: enableVibration, playSound: v, sound: sound, silent: silent, timeoutAfter: timeoutAfter); + AppNotificationSettings withSound(String? v) => AppNotificationSettings(enableLights: enableLights, enableVibration: enableVibration, playSound: playSound, sound: v, silent: silent, timeoutAfter: timeoutAfter); + AppNotificationSettings withSilent(bool v) => AppNotificationSettings(enableLights: enableLights, enableVibration: enableVibration, playSound: playSound, sound: sound, silent: v, timeoutAfter: timeoutAfter); + AppNotificationSettings withTimeoutAfter(int? v) => AppNotificationSettings(enableLights: enableLights, enableVibration: enableVibration, playSound: playSound, sound: sound, silent: silent, timeoutAfter: v); + + static AppNotificationSettings load(SharedPreferences prefs, String prefix) { + final def = AppNotificationSettings(); + + final enableLights = prefs.getBool('${prefix}.enableLights') ?? def.enableLights; + final enableVibration = prefs.getBool('${prefix}.enableVibration') ?? def.enableVibration; + final playSound = prefs.getBool('${prefix}.playSound') ?? def.playSound; + final sound = _decode(prefs.getString('${prefix}.sound'), def.sound); + final silent = prefs.getBool('${prefix}.silent') ?? def.silent; + final timeoutAfter = _decode(prefs.getString('${prefix}.timeoutAfter'), def.timeoutAfter); + + return AppNotificationSettings( + enableLights: enableLights, + enableVibration: enableVibration, + playSound: playSound, + sound: sound, + silent: silent, + timeoutAfter: timeoutAfter, + ); + } +} + +String _encode(T v) { + return JsonEncoder().convert(v); +} + +T _decode(String? v, T fallback) { + if (v == null) return fallback; + try { + return JsonDecoder().convert(v) as T; + } catch (_) { + return fallback; + } +} diff --git a/flutter/lib/state/app_theme.dart b/flutter/lib/state/app_theme.dart index 70f239e..62166bb 100644 --- a/flutter/lib/state/app_theme.dart +++ b/flutter/lib/state/app_theme.dart @@ -1,16 +1,87 @@ import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:simplecloudnotifier/state/globals.dart'; + +enum ThemeColor { + Pink(displayStr: 'Pink', key: 'PINK', value: Colors.pink), + Red(displayStr: 'Red', key: 'RED', value: Colors.red), + DeepOrange(displayStr: 'Deep-Orange', key: 'DEEPORANGE', value: Colors.deepOrange), + Orange(displayStr: 'Orange', key: 'ORANGE', value: Colors.orange), + Amber(displayStr: 'Amber', key: 'AMBER', value: Colors.amber), + Yellow(displayStr: 'Yellow', key: 'YELLOW', value: Colors.yellow), + Lime(displayStr: 'Lime', key: 'LIME', value: Colors.lime), + LightGreen(displayStr: 'Light-Green', key: 'LIGHTGREEN', value: Colors.lightGreen), + Green(displayStr: 'Green', key: 'GREEN', value: Colors.green), + Teal(displayStr: 'Teal', key: 'TEAL', value: Colors.teal), + Cyan(displayStr: 'Cyan', key: 'CYAN', value: Colors.cyan), + LightBlue(displayStr: 'Light-Blue', key: 'LIGHTBLUE', value: Colors.lightBlue), + Blue(displayStr: 'Blue', key: 'BLUE', value: Colors.blue), + Indigo(displayStr: 'Indigo', key: 'INDIGO', value: Colors.indigo), + Purple(displayStr: 'Purple', key: 'PURPLE', value: Colors.purple), + DeepPurple(displayStr: 'Deep-Purple', key: 'DEEPPURPLE', value: Colors.deepPurple), + BlueGrey(displayStr: 'Blue-Grey', key: 'BLUEGREY', value: Colors.blueGrey), + Brown(displayStr: 'Brown', key: 'BROWN', value: Colors.brown), + Grey(displayStr: 'Grey', key: 'GREY', value: Colors.grey); + + const ThemeColor({required this.displayStr, required this.key, required this.value}); + + final String displayStr; + final String key; + final Color value; + + @override + toString() => displayStr; + + static ThemeColor? parse(String? string) { + if (string == null) return null; + return values.firstWhere((e) => e.key == string, orElse: null); + } +} class AppTheme extends ChangeNotifier { + static AppTheme? _singleton = AppTheme._internal(); + + factory AppTheme() { + return _singleton ?? (_singleton = AppTheme._internal()); + } + + AppTheme._internal() {} + + // -------------------------------------------------------------------------- + bool _darkmode = false; bool get darkMode => _darkmode; + ThemeColor _color = ThemeColor.Blue; + ThemeColor get color => _color; + void setDarkMode(bool v) { _darkmode = v; notifyListeners(); + save(); } void switchDarkMode() { _darkmode = !_darkmode; notifyListeners(); + save(); + } + + void setColor(ThemeColor v) { + _color = v; + notifyListeners(); + save(); + } + + // -------------------------------------------------------------------------- + + void load() { + _darkmode = Globals().sharedPrefs.getBool('theme.dark') ?? _darkmode; + _color = ThemeColor.parse(Globals().sharedPrefs.getString('theme.color')) ?? _color; + } + + Future save() async { + await Globals().sharedPrefs.setBool('theme.dark', _darkmode); + await Globals().sharedPrefs.setString('theme.color', _color.key); } } diff --git a/flutter/lib/state/scn_data_cache.dart b/flutter/lib/state/scn_data_cache.dart index 9c9ad0f..5e559c5 100644 --- a/flutter/lib/state/scn_data_cache.dart +++ b/flutter/lib/state/scn_data_cache.dart @@ -3,7 +3,7 @@ import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/keytoken.dart'; import 'package:simplecloudnotifier/models/scn_message.dart'; -import 'package:simplecloudnotifier/settings/app_settings.dart'; +import 'package:simplecloudnotifier/state/app_settings.dart'; class SCNDataCache { SCNDataCache._internal(); diff --git a/flutter/lib/utils/notifier.dart b/flutter/lib/utils/notifier.dart index 80c7050..1170e4f 100644 --- a/flutter/lib/utils/notifier.dart +++ b/flutter/lib/utils/notifier.dart @@ -1,12 +1,12 @@ import 'dart:io'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:simplecloudnotifier/settings/app_settings.dart'; +import 'package:simplecloudnotifier/state/app_settings.dart'; import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/globals.dart'; class Notifier { - static void showLocalNotification(String messageID, String channelID, String channelName, String channelDescr, String title, String body, DateTime? timestamp) async { + static void showLocalNotification(String messageID, String channelID, String channelName, String channelDescr, String title, String body, DateTime? timestamp, int? prio) async { final nid = Globals().sharedPrefs.getInt('notifier.nextid') ?? 1000; Globals().sharedPrefs.setInt('notifier.nextid', nid + 7); @@ -60,6 +60,8 @@ class Notifier { payload = ['@SCN_MESSAGE', messageID, channelID, newMessageNID].join("\n"); } + final cfg = AppSettings().getNotificationSettings(prio); + // ======== SHOW NOTIFICATION ======== await flutterLocalNotificationsPlugin.show( newMessageNID, @@ -75,6 +77,12 @@ class Notifier { when: timestamp?.millisecondsSinceEpoch, groupKey: channelID, subText: (channelName == 'main') ? null : channelName, + enableLights: cfg.enableLights, + enableVibration: cfg.enableVibration, + playSound: cfg.playSound, + sound: cfg.soundURI(), + silent: cfg.silent, + timeoutAfter: cfg.timeoutAfter, ), ), payload: payload, diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index b8390d0..0095d9e 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -46,6 +46,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.7.0" + aron_gradient_line: + dependency: transitive + description: + name: aron_gradient_line + sha256: "6b0df835c33bc4226f6984321f709394aa4734d9729ad5a0411d1add9df8e9d2" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + asn1lib: + dependency: transitive + description: + name: asn1lib + sha256: e02d018628c870ef2d7f03e33f9ad179d89ff6ec52ca6c56bcb80bcef979867f + url: "https://pub.dev" + source: hosted + version: "1.6.2" async: dependency: transitive description: @@ -238,6 +254,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.2" + encrypt: + dependency: transitive + description: + name: encrypt + sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" + url: "https://pub.dev" + source: hosted + version: "5.0.3" equatable: dependency: transitive description: @@ -326,6 +350,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + fl_chart: + dependency: transitive + description: + name: fl_chart + sha256: "74959b99b92b9eebeed1a4049426fd67c4abc3c5a0f4d12e2877097d6a11ae08" + url: "https://pub.dev" + source: hosted + version: "0.69.2" flutter: dependency: "direct main" description: flutter @@ -412,6 +444,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + git_stamp: + dependency: "direct main" + description: + name: git_stamp + sha256: eddda29d15136503af57b5d927393fab2e7fe9660d4dc9ae418eb69c3f542c30 + url: "https://pub.dev" + source: hosted + version: "5.10.0" glob: dependency: transitive description: @@ -740,6 +780,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + url: "https://pub.dev" + source: hosted + version: "3.9.1" pool: dependency: transitive description: @@ -796,6 +844,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.0" + settings_ui: + dependency: "direct main" + description: + name: settings_ui + sha256: d9838037cb554b24b4218b2d07666fbada3478882edefae375ee892b6c820ef3 + url: "https://pub.dev" + source: hosted + version: "2.0.2" share_plus: dependency: "direct main" description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 791f69d..efc8e7b 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -2,7 +2,7 @@ name: simplecloudnotifier description: "Receive push messages" publish_to: 'none' -version: 2.0.0+100 +version: 2.0.0+474 environment: sdk: '>=3.2.6 <4.0.0' @@ -38,6 +38,8 @@ dependencies: path: any mobile_scanner: ^6.0.1 + settings_ui: ^2.0.2 + git_stamp: ^5.10.0 dependency_overrides: font_awesome_flutter: path: deps/font_awesome_flutter