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:
Mike Schwörer 2025-04-19 01:49:28 +02:00
parent 5417796f3f
commit b91ddc172d
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
33 changed files with 912 additions and 127 deletions

2
flutter/.gitignore vendored
View File

@ -56,3 +56,5 @@ app.*.map.json
/android/app/debug /android/app/debug
/android/app/profile /android/app/profile
/android/app/release /android/app/release
/lib/git_stamp/

View File

@ -9,28 +9,28 @@
# runs app locally (linux) # runs app locally (linux)
run-linux: run-linux: gen
dart run build_runner build dart run build_runner build
_JAVA_OPTIONS="" flutter run -d linux _JAVA_OPTIONS="" flutter run -d linux
# runs app locally (web | not really supported) # runs app locally (web | not really supported)
run-web: run-web: gen
dart run build_runner build dart run build_runner build
_JAVA_OPTIONS="" flutter run -d chrome _JAVA_OPTIONS="" flutter run -d chrome
# runs on android device (must have network adb enabled teh correct IP) # runs on android device (must have network adb enabled teh correct IP)
run-android: run-android: gen
ping -c1 10.10.10.177 ping -c1 10.10.10.177
adb connect 10.10.10.177:5555 adb connect 10.10.10.177:5555
flutter pub run build_runner build flutter pub run build_runner build
_JAVA_OPTIONS="" flutter run -d 10.10.10.177:5555 _JAVA_OPTIONS="" flutter run -d 10.10.10.177:5555
install-release: install-release: gen
# Install on Pixel 7a # Install on Pixel 7a
flutter build apk --release flutter build apk --release
flutter run --release -d 35221JEHN07157 flutter run --release -d 35221JEHN07157
build-release: build-release: gen
flutter build apk --release flutter build apk --release
flutter build appbundle --release flutter build appbundle --release
flutter build linux --release flutter build linux --release
@ -42,7 +42,9 @@ fix:
dart fix --apply dart fix --apply
gen: gen:
./_utils/inc_buildnum.sh
dart run build_runner build 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) # run `make run` in another terminal (or another variant of flutter run)
autoreload: autoreload:

41
flutter/_utils/inc_buildnum.sh Executable file
View File

@ -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

View File

@ -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/components/layout/app_bar_progress_indicator.dart';
import 'package:simplecloudnotifier/pages/debug/debug_main.dart'; import 'package:simplecloudnotifier/pages/debug/debug_main.dart';
import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.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_bar_state.dart';
import 'package:simplecloudnotifier/state/app_events.dart'; import 'package:simplecloudnotifier/state/app_events.dart';
import 'package:simplecloudnotifier/state/app_theme.dart'; import 'package:simplecloudnotifier/state/app_theme.dart';
@ -64,7 +64,7 @@ class _SCNAppBarState extends State<SCNAppBar> {
builder: (context, appTheme, child) => IconButton( builder: (context, appTheme, child) => IconButton(
icon: Icon(appTheme.darkMode ? FontAwesomeIcons.solidSun : FontAwesomeIcons.solidMoon), icon: Icon(appTheme.darkMode ? FontAwesomeIcons.solidSun : FontAwesomeIcons.solidMoon),
tooltip: appTheme.darkMode ? 'Light mode' : 'Dark mode', tooltip: appTheme.darkMode ? 'Light mode' : 'Dark mode',
onPressed: appTheme.switchDarkMode, onPressed: AppTheme().switchDarkMode,
), ),
)); ));
} else { } else {

View File

@ -14,7 +14,7 @@ import 'package:simplecloudnotifier/models/scn_message.dart';
import 'package:simplecloudnotifier/nav_layout.dart'; import 'package:simplecloudnotifier/nav_layout.dart';
import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart'; import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart';
import 'package:simplecloudnotifier/pages/message_view/message_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_bar_state.dart';
import 'package:simplecloudnotifier/state/app_events.dart'; import 'package:simplecloudnotifier/state/app_events.dart';
import 'package:simplecloudnotifier/state/app_theme.dart'; import 'package:simplecloudnotifier/state/app_theme.dart';
@ -248,8 +248,10 @@ class SCNApp extends StatelessWidget {
title: 'SimpleCloudNotifier', title: 'SimpleCloudNotifier',
navigatorObservers: [Navi.routeObserver, Navi.modalRouteObserver], navigatorObservers: [Navi.routeObserver, Navi.modalRouteObserver],
theme: ThemeData( theme: ThemeData(
//TODO color settings colorScheme: ColorScheme.fromSeed(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue, brightness: appTheme.darkMode ? Brightness.dark : Brightness.light), seedColor: appTheme.color.value,
brightness: appTheme.darkMode ? Brightness.dark : Brightness.light,
),
useMaterial3: true, useMaterial3: true,
), ),
home: SCNNavLayout(), home: SCNNavLayout(),
@ -343,7 +345,7 @@ Future<void> _receiveMessage(RemoteMessage message, bool foreground) async {
} catch (exc, trace) { } catch (exc, trace) {
ApplicationLog.writeRawFailure('Failed to init hive', {'exception': exc.toString(), 'trace': trace.toString(), 'data': message, 'foreground': foreground}); 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); 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; return;
} }
@ -359,12 +361,13 @@ Future<void> _receiveMessage(RemoteMessage message, bool foreground) async {
final channel = message.data['channel'] as String; final channel = message.data['channel'] as String;
final channel_id = message.data['channel_id'] as String; final channel_id = message.data['channel_id'] as String;
final body = message.data['body'] 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) { } catch (exc, trace) {
ApplicationLog.writeRawFailure('Failed to decode received FB message', {'exception': exc.toString(), 'trace': trace.toString(), 'data': message, 'foreground': foreground}); 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); 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; return;
} }
@ -373,7 +376,7 @@ Future<void> _receiveMessage(RemoteMessage message, bool foreground) async {
} catch (exc, trace) { } catch (exc, trace) {
ApplicationLog.writeRawFailure('Failed to persist received FB message', {'exception': exc.toString(), 'trace': trace.toString(), 'data': message, 'foreground': foreground}); 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); 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; return;
} }

View File

@ -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/components/bottom_fab/fab_bottom_app_bar.dart';
import 'package:simplecloudnotifier/pages/account/account.dart'; import 'package:simplecloudnotifier/pages/account/account.dart';
import 'package:simplecloudnotifier/pages/message_list/message_list.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/state/app_auth.dart';
import 'package:simplecloudnotifier/utils/toaster.dart'; import 'package:simplecloudnotifier/utils/toaster.dart';

View File

@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/channel.dart';
@ -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_message_view/channel_message_view.dart';
import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart'; import 'package:simplecloudnotifier/pages/channel_view/channel_view.dart';
import 'package:simplecloudnotifier/state/app_auth.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/application_log.dart';
import 'package:simplecloudnotifier/state/scn_data_cache.dart'; import 'package:simplecloudnotifier/state/scn_data_cache.dart';
import 'package:simplecloudnotifier/utils/navi.dart'; import 'package:simplecloudnotifier/utils/navi.dart';
@ -20,8 +20,6 @@ enum ChannelListItemMode {
} }
class ChannelListItem extends StatefulWidget { class ChannelListItem extends StatefulWidget {
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting
const ChannelListItem({ const ChannelListItem({
required this.channel, required this.channel,
required this.onChannelListReloadTrigger, required this.onChannelListReloadTrigger,
@ -64,6 +62,8 @@ class _ChannelListItemState extends State<ChannelListItem> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateFormat();
return Card.filled( return Card.filled(
margin: EdgeInsets.fromLTRB(0, 4, 0, 4), margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)), shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
@ -95,7 +95,7 @@ class _ChannelListItemState extends State<ChannelListItem> {
), ),
), ),
Text( 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), 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/models/scn_message.dart';
import 'package:simplecloudnotifier/pages/message_list/message_list_item.dart'; import 'package:simplecloudnotifier/pages/message_list/message_list_item.dart';
import 'package:simplecloudnotifier/pages/message_view/message_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_auth.dart'; import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/scn_data_cache.dart'; import 'package:simplecloudnotifier/state/scn_data_cache.dart';

View File

@ -1,7 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.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/models/client.dart';
import 'package:simplecloudnotifier/state/app_settings.dart';
enum ClientListItemMode { enum ClientListItemMode {
Messages, Messages,
@ -9,8 +10,6 @@ enum ClientListItemMode {
} }
class ClientListItem extends StatelessWidget { class ClientListItem extends StatelessWidget {
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting
const ClientListItem({ const ClientListItem({
required this.item, required this.item,
super.key, super.key,
@ -20,6 +19,8 @@ class ClientListItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateFormat();
return Card.filled( return Card.filled(
margin: EdgeInsets.fromLTRB(0, 4, 0, 4), margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)), shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
@ -43,7 +44,7 @@ class ClientListItem extends StatelessWidget {
), ),
), ),
Text( 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)), 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/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:simplecloudnotifier/api/api_client.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/app_auth.dart';
import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/utils/notifier.dart'; import 'package:simplecloudnotifier/utils/notifier.dart';
@ -67,8 +68,32 @@ class _DebugActionsPageState extends State<DebugActionsPage> {
SizedBox(height: 20), SizedBox(height: 20),
UI.button( UI.button(
big: false, big: false,
onPressed: () => Notifier.showLocalNotification('', 'TEST_CHANNEL', "Test Channel", "Channel for testing", "Hello World", "Local Notification test", null), onPressed: () => Notifier.showLocalNotification('', 'TEST_CHANNEL', "Test Channel", "Channel for testing", "Hello World", "Local Notification test", null, null),
text: 'Show local notification', 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> { class _DebugLogsPageState extends State<DebugLogsPage> {
Box<SCNLog> logBox = Hive.box<SCNLog>('scn-logs'); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -6,7 +6,9 @@ class DebugSharedPrefPage extends StatelessWidget {
final SharedPreferences sharedPref; final SharedPreferences sharedPref;
final List<String> keys; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -13,7 +13,7 @@ class DebugRequestsPage extends StatefulWidget {
class _DebugRequestsPageState extends State<DebugRequestsPage> { class _DebugRequestsPageState extends State<DebugRequestsPage> {
Box<SCNRequest> requestsBox = Hive.box<SCNRequest>('scn-requests'); 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 @override
Widget build(BuildContext context) { 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/models/scn_message.dart';
import 'package:simplecloudnotifier/pages/message_list/message_list_item.dart'; import 'package:simplecloudnotifier/pages/message_list/message_list_item.dart';
import 'package:simplecloudnotifier/pages/message_view/message_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_auth.dart'; import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/scn_data_cache.dart'; import 'package:simplecloudnotifier/state/scn_data_cache.dart';

View File

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

View File

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

View File

@ -6,7 +6,7 @@ import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/models/scn_message.dart'; import 'package:simplecloudnotifier/models/scn_message.dart';
import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.dart'; import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.dart';
import 'package:simplecloudnotifier/pages/message_view/message_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_bar_state.dart';
import 'package:simplecloudnotifier/state/app_events.dart'; import 'package:simplecloudnotifier/state/app_events.dart';
import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/application_log.dart';

View File

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

View File

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

View File

@ -1,9 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.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/api/api_client.dart';
import 'package:simplecloudnotifier/models/sender_name_statistics.dart'; import 'package:simplecloudnotifier/models/sender_name_statistics.dart';
import 'package:simplecloudnotifier/pages/filtered_message_view/filtered_message_view.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'; import 'package:simplecloudnotifier/utils/navi.dart';
enum SenderListItemMode { enum SenderListItemMode {
@ -12,8 +13,6 @@ enum SenderListItemMode {
} }
class SenderListItem extends StatelessWidget { class SenderListItem extends StatelessWidget {
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting
const SenderListItem({ const SenderListItem({
required this.item, required this.item,
super.key, super.key,
@ -23,6 +22,8 @@ class SenderListItem extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateFormat();
return Card.filled( return Card.filled(
margin: EdgeInsets.fromLTRB(0, 4, 0, 4), margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)), shape: BeveledRectangleBorder(borderRadius: BorderRadius.circular(0)),
@ -57,7 +58,7 @@ class SenderListItem extends StatelessWidget {
children: [ children: [
Expanded( Expanded(
child: Text( 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)), 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:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/models/subscription.dart'; import 'package:simplecloudnotifier/models/subscription.dart';
@ -15,8 +14,6 @@ enum SubscriptionListItemMode {
} }
class SubscriptionListItem extends StatelessWidget { class SubscriptionListItem extends StatelessWidget {
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting
const SubscriptionListItem({ const SubscriptionListItem({
required this.item, required this.item,
required this.userCache, 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/pages/channel_view/channel_view.dart';
import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/state/app_bar_state.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/application_log.dart';
import 'package:simplecloudnotifier/types/immediate_future.dart'; import 'package:simplecloudnotifier/types/immediate_future.dart';
import 'package:simplecloudnotifier/utils/dialogs.dart'; import 'package:simplecloudnotifier/utils/dialogs.dart';
@ -40,8 +41,6 @@ enum EditState { none, editing, saving }
enum SubscriptionViewPageInitState { loading, okay, error } enum SubscriptionViewPageInitState { loading, okay, error }
class _SubscriptionViewPageState extends State<SubscriptionViewPage> { class _SubscriptionViewPageState extends State<SubscriptionViewPage> {
static final _dateFormat = DateFormat('yyyy-MM-dd HH:mm'); //TODO setting
ImmediateFuture<UserPreview> _futureChannelOwner = ImmediateFuture.ofPending(); ImmediateFuture<UserPreview> _futureChannelOwner = ImmediateFuture.ofPending();
ImmediateFuture<UserPreview> _futureSubscriber = ImmediateFuture.ofPending(); ImmediateFuture<UserPreview> _futureSubscriber = ImmediateFuture.ofPending();
ImmediateFuture<ChannelPreview> _futureChannel = ImmediateFuture.ofPending(); ImmediateFuture<ChannelPreview> _futureChannel = ImmediateFuture.ofPending();
@ -161,6 +160,8 @@ class _SubscriptionViewPageState extends State<SubscriptionViewPage> {
} }
Widget _buildOwnedSubscriptionView(BuildContext context, Subscription subscription) { Widget _buildOwnedSubscriptionView(BuildContext context, Subscription subscription) {
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateFormat();
return SingleChildScrollView( return SingleChildScrollView(
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16), padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
@ -181,7 +182,7 @@ class _SubscriptionViewPageState extends State<SubscriptionViewPage> {
context: context, context: context,
icon: FontAwesomeIcons.clock, icon: FontAwesomeIcons.clock,
title: 'Created', title: 'Created',
values: [_SubscriptionViewPageState._dateFormat.format(DateTime.parse(subscription.timestampCreated).toLocal())], values: [dateFormat.format(DateTime.parse(subscription.timestampCreated).toLocal())],
), ),
_buildStatusCard(context), _buildStatusCard(context),
UI.button(text: "Unsubscribe", onPressed: _unsubscribe, tonal: true), UI.button(text: "Unsubscribe", onPressed: _unsubscribe, tonal: true),
@ -192,6 +193,8 @@ class _SubscriptionViewPageState extends State<SubscriptionViewPage> {
} }
Widget _buildIncomingSubscriptionView(BuildContext context, Subscription subscription) { Widget _buildIncomingSubscriptionView(BuildContext context, Subscription subscription) {
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateFormat();
return SingleChildScrollView( return SingleChildScrollView(
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16), padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
@ -212,7 +215,7 @@ class _SubscriptionViewPageState extends State<SubscriptionViewPage> {
context: context, context: context,
icon: FontAwesomeIcons.clock, icon: FontAwesomeIcons.clock,
title: 'Created', title: 'Created',
values: [_SubscriptionViewPageState._dateFormat.format(DateTime.parse(subscription.timestampCreated).toLocal())], values: [dateFormat.format(DateTime.parse(subscription.timestampCreated).toLocal())],
), ),
_buildStatusCard(context), _buildStatusCard(context),
if (subscription.confirmed) UI.button(text: "Revoke subscription", onPressed: _unsubscribe, color: Colors.red), 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) { Widget _buildOutgoingSubscriptionView(BuildContext context, Subscription subscription) {
final dateFormat = context.select<AppSettings, AppSettingsDateFormat>((v) => v.dateFormat).dateFormat();
return SingleChildScrollView( return SingleChildScrollView(
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(24, 16, 24, 16), padding: const EdgeInsets.fromLTRB(24, 16, 24, 16),
@ -245,7 +250,7 @@ class _SubscriptionViewPageState extends State<SubscriptionViewPage> {
context: context, context: context,
icon: FontAwesomeIcons.clock, icon: FontAwesomeIcons.clock,
title: 'Created', title: 'Created',
values: [_SubscriptionViewPageState._dateFormat.format(DateTime.parse(subscription.timestampCreated).toLocal())], values: [dateFormat.format(DateTime.parse(subscription.timestampCreated).toLocal())],
), ),
_buildStatusCard(context), _buildStatusCard(context),
if (subscription.confirmed && subscription.active) UI.button(text: "Deactivate subscription", onPressed: _deactivate, tonal: true), if (subscription.confirmed && subscription.active) UI.button(text: "Deactivate subscription", onPressed: _deactivate, tonal: true),

View File

@ -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<void> save() async {
//TODO
}
}

View File

@ -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<void> 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<void> 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>(T v) {
return JsonEncoder().convert(v);
}
T _decode<T>(String? v, T fallback) {
if (v == null) return fallback;
try {
return JsonDecoder().convert(v) as T;
} catch (_) {
return fallback;
}
}

View File

@ -1,16 +1,87 @@
import 'package:flutter/foundation.dart'; 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 { class AppTheme extends ChangeNotifier {
static AppTheme? _singleton = AppTheme._internal();
factory AppTheme() {
return _singleton ?? (_singleton = AppTheme._internal());
}
AppTheme._internal() {}
// --------------------------------------------------------------------------
bool _darkmode = false; bool _darkmode = false;
bool get darkMode => _darkmode; bool get darkMode => _darkmode;
ThemeColor _color = ThemeColor.Blue;
ThemeColor get color => _color;
void setDarkMode(bool v) { void setDarkMode(bool v) {
_darkmode = v; _darkmode = v;
notifyListeners(); notifyListeners();
save();
} }
void switchDarkMode() { void switchDarkMode() {
_darkmode = !_darkmode; _darkmode = !_darkmode;
notifyListeners(); 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<void> save() async {
await Globals().sharedPrefs.setBool('theme.dark', _darkmode);
await Globals().sharedPrefs.setString('theme.color', _color.key);
} }
} }

View File

@ -3,7 +3,7 @@ import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/models/keytoken.dart'; import 'package:simplecloudnotifier/models/keytoken.dart';
import 'package:simplecloudnotifier/models/scn_message.dart'; import 'package:simplecloudnotifier/models/scn_message.dart';
import 'package:simplecloudnotifier/settings/app_settings.dart'; import 'package:simplecloudnotifier/state/app_settings.dart';
class SCNDataCache { class SCNDataCache {
SCNDataCache._internal(); SCNDataCache._internal();

View File

@ -1,12 +1,12 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; 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/application_log.dart';
import 'package:simplecloudnotifier/state/globals.dart'; import 'package:simplecloudnotifier/state/globals.dart';
class Notifier { 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; final nid = Globals().sharedPrefs.getInt('notifier.nextid') ?? 1000;
Globals().sharedPrefs.setInt('notifier.nextid', nid + 7); Globals().sharedPrefs.setInt('notifier.nextid', nid + 7);
@ -60,6 +60,8 @@ class Notifier {
payload = ['@SCN_MESSAGE', messageID, channelID, newMessageNID].join("\n"); payload = ['@SCN_MESSAGE', messageID, channelID, newMessageNID].join("\n");
} }
final cfg = AppSettings().getNotificationSettings(prio);
// ======== SHOW NOTIFICATION ======== // ======== SHOW NOTIFICATION ========
await flutterLocalNotificationsPlugin.show( await flutterLocalNotificationsPlugin.show(
newMessageNID, newMessageNID,
@ -75,6 +77,12 @@ class Notifier {
when: timestamp?.millisecondsSinceEpoch, when: timestamp?.millisecondsSinceEpoch,
groupKey: channelID, groupKey: channelID,
subText: (channelName == 'main') ? null : channelName, 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, payload: payload,

View File

@ -46,6 +46,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.7.0" 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: async:
dependency: transitive dependency: transitive
description: description:
@ -238,6 +254,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.0.2" version: "7.0.2"
encrypt:
dependency: transitive
description:
name: encrypt
sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2"
url: "https://pub.dev"
source: hosted
version: "5.0.3"
equatable: equatable:
dependency: transitive dependency: transitive
description: description:
@ -326,6 +350,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" 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: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -412,6 +444,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.0" 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: glob:
dependency: transitive dependency: transitive
description: description:
@ -740,6 +780,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.8" version: "2.1.8"
pointycastle:
dependency: transitive
description:
name: pointycastle
sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe"
url: "https://pub.dev"
source: hosted
version: "3.9.1"
pool: pool:
dependency: transitive dependency: transitive
description: description:
@ -796,6 +844,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.0" 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: share_plus:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -2,7 +2,7 @@ name: simplecloudnotifier
description: "Receive push messages" description: "Receive push messages"
publish_to: 'none' publish_to: 'none'
version: 2.0.0+100 version: 2.0.0+474
environment: environment:
sdk: '>=3.2.6 <4.0.0' sdk: '>=3.2.6 <4.0.0'
@ -38,6 +38,8 @@ dependencies:
path: any path: any
mobile_scanner: ^6.0.1 mobile_scanner: ^6.0.1
settings_ui: ^2.0.2
git_stamp: ^5.10.0
dependency_overrides: dependency_overrides:
font_awesome_flutter: font_awesome_flutter:
path: deps/font_awesome_flutter path: deps/font_awesome_flutter