Compare commits

..

6 Commits

49 changed files with 1037 additions and 222 deletions

View File

@ -37,6 +37,7 @@ android {
ndkVersion flutter.ndkVersion ndkVersion flutter.ndkVersion
compileOptions { compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
} }
@ -55,6 +56,7 @@ android {
targetSdkVersion flutter.targetSdkVersion targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
multiDexEnabled true
} }
signingConfigs { signingConfigs {
@ -77,4 +79,9 @@ flutter {
source '../..' source '../..'
} }
dependencies {} dependencies {
implementation 'androidx.window:window:1.0.0'
implementation 'androidx.window:window-java:1.0.0'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.2.2'
}

27
flutter/android/app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,27 @@
## Gson rules
# Gson uses generic type information stored in a class file when working with fields. Proguard
# removes such information by default, so configure it to keep all of it.
-keepattributes Signature
# For using GSON @Expose annotation
-keepattributes *Annotation*
# Gson specific classes
-dontwarn sun.misc.**
#-keep class com.google.gson.stream.** { *; }
# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory,
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
-keep class * extends com.google.gson.TypeAdapter
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer
# Prevent R8 from leaving Data object members always null
-keepclassmembers,allowobfuscation class * {
@com.google.gson.annotations.SerializedName <fields>;
}
# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher.
-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken

View File

@ -31,6 +31,9 @@
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
</activity> </activity>
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ActionBroadcastReceiver" />
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data <meta-data

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 B

View File

@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 949 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools" tools:keep="@drawable/*" />

View File

@ -6,6 +6,7 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.3.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
} }
} }

View File

@ -1,13 +1,27 @@
import UIKit import UIKit
import Flutter import Flutter
import flutter_local_notifications
@UIApplicationMain @UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate { @objc class AppDelegate: FlutterAppDelegate {
override func application( override func application(
_ application: UIApplication, _ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool { ) -> Bool {
// This is required to make any communication available in the action isolate.
FlutterLocalNotificationsPlugin.setPluginRegistrantCallback { (registry) in
GeneratedPluginRegistrant.register(with: registry)
}
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
}
GeneratedPluginRegistrant.register(with: self) GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions) return super.application(application, didFinishLaunchingWithOptions: launchOptions)
} }
} }

View File

@ -11,7 +11,7 @@ import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/globals.dart'; import 'package:simplecloudnotifier/state/globals.dart';
import 'package:simplecloudnotifier/state/request_log.dart'; import 'package:simplecloudnotifier/state/request_log.dart';
import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/models/message.dart'; import 'package:simplecloudnotifier/models/scn_message.dart';
import 'package:simplecloudnotifier/state/token_source.dart'; import 'package:simplecloudnotifier/state/token_source.dart';
import 'package:simplecloudnotifier/utils/toaster.dart'; import 'package:simplecloudnotifier/utils/toaster.dart';
@ -211,7 +211,7 @@ class APIClient {
); );
} }
static Future<(String, List<Message>)> getMessageList(TokenSource auth, String pageToken, {int? pageSize, List<String>? channelIDs}) async { static Future<(String, List<SCNMessage>)> getMessageList(TokenSource auth, String pageToken, {int? pageSize, List<String>? channelIDs}) async {
return await _request( return await _request(
name: 'getMessageList', name: 'getMessageList',
method: 'GET', method: 'GET',
@ -221,18 +221,18 @@ class APIClient {
if (pageSize != null) 'page_size': pageSize.toString(), if (pageSize != null) 'page_size': pageSize.toString(),
if (channelIDs != null) 'channel_id': channelIDs.join(","), if (channelIDs != null) 'channel_id': channelIDs.join(","),
}, },
fn: (json) => Message.fromPaginatedJsonArray(json, 'messages', 'next_page_token'), fn: (json) => SCNMessage.fromPaginatedJsonArray(json, 'messages', 'next_page_token'),
authToken: auth.getToken(), authToken: auth.getToken(),
); );
} }
static Future<Message> getMessage(TokenSource auth, String msgid) async { static Future<SCNMessage> getMessage(TokenSource auth, String msgid) async {
return await _request( return await _request(
name: 'getMessage', name: 'getMessage',
method: 'GET', method: 'GET',
relURL: 'messages/$msgid', relURL: 'messages/$msgid',
query: {}, query: {},
fn: Message.fromJson, fn: SCNMessage.fromJson,
authToken: auth.getToken(), authToken: auth.getToken(),
); );
} }

View File

@ -1,18 +1,20 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.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:provider/provider.dart';
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/settings/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'; import 'package:simplecloudnotifier/state/app_theme.dart';
import 'package:simplecloudnotifier/utils/navi.dart'; import 'package:simplecloudnotifier/utils/navi.dart';
class SCNAppBar extends StatelessWidget implements PreferredSizeWidget { class SCNAppBar extends StatefulWidget implements PreferredSizeWidget {
const SCNAppBar({ SCNAppBar({
Key? key, Key? key,
required this.title, required this.title,
required this.showThemeSwitch, required this.showThemeSwitch,
required this.showDebug,
required this.showSearch, required this.showSearch,
required this.showShare, required this.showShare,
this.onShare = null, this.onShare = null,
@ -20,16 +22,33 @@ class SCNAppBar extends StatelessWidget implements PreferredSizeWidget {
final String? title; final String? title;
final bool showThemeSwitch; final bool showThemeSwitch;
final bool showDebug;
final bool showSearch; final bool showSearch;
final bool showShare; final bool showShare;
final void Function()? onShare; final void Function()? onShare;
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
@override
State<SCNAppBar> createState() => _SCNAppBarState();
}
class _SCNAppBarState extends State<SCNAppBar> {
final TextEditingController _ctrlSearchField = TextEditingController();
@override
void dispose() {
_ctrlSearchField.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final cfg = Provider.of<AppSettings>(context);
var actions = <Widget>[]; var actions = <Widget>[];
if (showDebug) { if (cfg.showDebugButton) {
actions.add(IconButton( actions.add(IconButton(
icon: const Icon(FontAwesomeIcons.solidSpiderBlackWidow), icon: const Icon(FontAwesomeIcons.solidSpiderBlackWidow),
tooltip: 'Debug', tooltip: 'Debug',
@ -39,7 +58,7 @@ class SCNAppBar extends StatelessWidget implements PreferredSizeWidget {
)); ));
} }
if (showThemeSwitch) { if (widget.showThemeSwitch) {
actions.add(Consumer<AppTheme>( actions.add(Consumer<AppTheme>(
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),
@ -48,54 +67,115 @@ class SCNAppBar extends StatelessWidget implements PreferredSizeWidget {
), ),
)); ));
} else { } else {
actions.add(Visibility( actions.add(_buildSpacer());
visible: false,
maintainSize: true,
maintainAnimation: true,
maintainState: true,
child: IconButton(
icon: const Icon(FontAwesomeIcons.square),
onPressed: () {/*TODO*/},
),
));
} }
if (showSearch) { if (widget.showSearch) {
actions.add(IconButton(
icon: const Icon(FontAwesomeIcons.solidFilter),
tooltip: 'Filter',
onPressed: () => _showFilterDialog(context),
));
actions.add(IconButton( actions.add(IconButton(
icon: const Icon(FontAwesomeIcons.solidMagnifyingGlass), icon: const Icon(FontAwesomeIcons.solidMagnifyingGlass),
tooltip: 'Search', tooltip: 'Search',
onPressed: () {/*TODO*/}, onPressed: () => AppBarState().setShowSearchField(true),
)); ));
} else if (showShare) { } else if (widget.showShare) {
actions.add(_buildSpacer());
actions.add(IconButton( actions.add(IconButton(
icon: const Icon(FontAwesomeIcons.solidShareNodes), icon: const Icon(FontAwesomeIcons.solidShareNodes),
tooltip: 'Share', tooltip: 'Share',
onPressed: onShare ?? () {}, onPressed: widget.onShare ?? () {},
)); ));
} else { } else {
actions.add(Visibility( actions.add(_buildSpacer());
visible: false,
maintainSize: true,
maintainAnimation: true,
maintainState: true,
child: IconButton(
icon: const Icon(FontAwesomeIcons.square),
onPressed: () {/*TODO*/},
),
));
} }
return AppBar( return Consumer<AppBarState>(builder: (context, value, child) {
title: Text(title ?? 'Simple Cloud Notifier 2.0'), if (value.showSearchField) {
actions: actions, return AppBar(
backgroundColor: Theme.of(context).secondaryHeaderColor, leading: IconButton(
bottom: PreferredSize( icon: const Icon(FontAwesomeIcons.solidArrowLeft),
preferredSize: Size(double.infinity, 1.0), onPressed: () {
child: AppBarProgressIndicator(), value.setShowSearchField(false);
},
),
title: _buildSearchTextField(context),
actions: [
IconButton(
icon: const Icon(FontAwesomeIcons.solidMagnifyingGlass),
onPressed: () {
value.setShowSearchField(false);
AppEvents().notifySearchListeners(_ctrlSearchField.text);
_ctrlSearchField.clear();
},
),
],
backgroundColor: Theme.of(context).secondaryHeaderColor,
bottom: PreferredSize(
preferredSize: Size(double.infinity, 1.0),
child: AppBarProgressIndicator(show: value.loadingIndeterminate),
),
);
} else {
return AppBar(
title: Text(widget.title ?? 'SCN'),
actions: actions,
backgroundColor: Theme.of(context).secondaryHeaderColor,
bottom: PreferredSize(
preferredSize: Size(double.infinity, 1.0),
child: AppBarProgressIndicator(show: value.loadingIndeterminate),
),
);
}
});
}
Visibility _buildSpacer() {
return Visibility(
visible: false,
maintainSize: true,
maintainAnimation: true,
maintainState: true,
child: IconButton(
icon: const Icon(FontAwesomeIcons.square),
onPressed: () {/* NO-OP */},
), ),
); );
} }
@override Widget _buildSearchTextField(BuildContext context) {
Size get preferredSize => const Size.fromHeight(kToolbarHeight); return TextField(
controller: _ctrlSearchField,
autofocus: true,
style: TextStyle(fontSize: 20),
textInputAction: TextInputAction.search,
decoration: InputDecoration(
hintText: 'Search',
),
onSubmitted: (value) {
AppBarState().setShowSearchField(false);
AppEvents().notifySearchListeners(_ctrlSearchField.text);
_ctrlSearchField.clear();
},
);
}
void _showFilterDialog(BuildContext context) {
showDialog<void>(
context: context,
barrierDismissible: true,
barrierColor: Colors.transparent,
builder: (BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(0)),
alignment: Alignment.topCenter,
insetPadding: EdgeInsets.fromLTRB(0, this.widget.preferredSize.height, 0, 0),
backgroundColor: Colors.transparent,
child: AppBarFilterDialog(),
);
},
);
}
} }

View File

@ -0,0 +1,86 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:simplecloudnotifier/utils/navi.dart';
class AppBarFilterDialog extends StatefulWidget {
@override
_AppBarFilterDialogState createState() => _AppBarFilterDialogState();
}
class _AppBarFilterDialogState extends State<AppBarFilterDialog> {
double _height = 0;
double _targetHeight = 4 + (48 * 6) + (16 * 5) + 4;
@override
void initState() {
super.initState();
Future.delayed(Duration.zero, () {
setState(() {
_height = _targetHeight;
});
});
}
@override
Widget build(BuildContext context) {
double vpWidth = MediaQuery.sizeOf(context).width;
return Container(
margin: const EdgeInsets.all(0),
width: vpWidth,
color: Colors.transparent,
child: Column(
children: [
Container(
color: Theme.of(context).secondaryHeaderColor,
child: AnimatedContainer(
duration: Duration(milliseconds: 350),
curve: Curves.easeInCubic,
height: _height,
child: ClipRect(
child: OverflowBox(
alignment: Alignment.topCenter,
maxWidth: vpWidth,
minWidth: vpWidth,
minHeight: 0,
maxHeight: _targetHeight,
child: Column(
children: [
SizedBox(height: 4),
_buildFilterItem(context, FontAwesomeIcons.magnifyingGlass, 'Search'),
Divider(),
_buildFilterItem(context, FontAwesomeIcons.snake, 'Channel'),
Divider(),
_buildFilterItem(context, FontAwesomeIcons.signature, 'Sender'),
Divider(),
_buildFilterItem(context, FontAwesomeIcons.timer, 'Time'),
Divider(),
_buildFilterItem(context, FontAwesomeIcons.bolt, 'Priority'),
Divider(),
_buildFilterItem(context, FontAwesomeIcons.gearCode, 'Key'),
SizedBox(height: 4),
],
),
),
),
),
),
Expanded(child: GestureDetector(child: Container(width: vpWidth, color: Color(0x88000000)), onTap: () => Navi.popDialog(context))),
],
),
);
}
Widget _buildFilterItem(BuildContext context, IconData icon, String label) {
return ListTile(
visualDensity: VisualDensity.compact,
title: Text(label),
leading: Icon(icon),
onTap: () {
Navi.popDialog(context);
//TOOD show more...
},
);
}
}

View File

@ -1,21 +1,19 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/state/app_bar_state.dart';
class AppBarProgressIndicator extends StatelessWidget implements PreferredSizeWidget { class AppBarProgressIndicator extends StatelessWidget implements PreferredSizeWidget {
AppBarProgressIndicator({required this.show});
final bool show;
@override @override
Size get preferredSize => Size(double.infinity, 1.0); Size get preferredSize => Size(double.infinity, 1.0);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<AppBarState>( if (show) {
builder: (context, value, child) { return LinearProgressIndicator(value: null);
if (value.loadingIndeterminate) { } else {
return LinearProgressIndicator(value: null); return SizedBox.square(dimension: 4); // 4 height is the same as the LinearProgressIndicator
} else { }
return SizedBox.square(dimension: 4); // 4 height is the same as the LinearProgressIndicator
}
},
);
} }
} }

View File

@ -7,7 +7,6 @@ class SCNScaffold extends StatelessWidget {
required this.child, required this.child,
this.title, this.title,
this.showThemeSwitch = true, this.showThemeSwitch = true,
this.showDebug = true,
this.showSearch = true, this.showSearch = true,
this.showShare = false, this.showShare = false,
this.onShare = null, this.onShare = null,
@ -16,7 +15,6 @@ class SCNScaffold extends StatelessWidget {
final Widget child; final Widget child;
final String? title; final String? title;
final bool showThemeSwitch; final bool showThemeSwitch;
final bool showDebug;
final bool showSearch; final bool showSearch;
final bool showShare; final bool showShare;
final void Function()? onShare; final void Function()? onShare;
@ -27,7 +25,6 @@ class SCNScaffold extends StatelessWidget {
appBar: SCNAppBar( appBar: SCNAppBar(
title: title, title: title,
showThemeSwitch: showThemeSwitch, showThemeSwitch: showThemeSwitch,
showDebug: showDebug,
showSearch: showSearch, showSearch: showSearch,
showShare: showShare, showShare: showShare,
onShare: onShare ?? () {}, onShare: onShare ?? () {},

View File

@ -2,14 +2,17 @@ import 'dart:io';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.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';
import 'package:simplecloudnotifier/models/client.dart'; import 'package:simplecloudnotifier/models/client.dart';
import 'package:simplecloudnotifier/models/message.dart'; import 'package:simplecloudnotifier/models/scn_message.dart';
import 'package:simplecloudnotifier/nav_layout.dart'; import 'package:simplecloudnotifier/nav_layout.dart';
import 'package:simplecloudnotifier/settings/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_theme.dart'; import 'package:simplecloudnotifier/state/app_theme.dart';
import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/fb_message.dart'; import 'package:simplecloudnotifier/state/fb_message.dart';
@ -17,7 +20,9 @@ import 'package:simplecloudnotifier/state/globals.dart';
import 'package:simplecloudnotifier/state/request_log.dart'; import 'package:simplecloudnotifier/state/request_log.dart';
import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:simplecloudnotifier/state/scn_data_cache.dart';
import 'package:simplecloudnotifier/utils/navi.dart'; import 'package:simplecloudnotifier/utils/navi.dart';
import 'package:simplecloudnotifier/utils/notifier.dart';
import 'package:toastification/toastification.dart'; import 'package:toastification/toastification.dart';
import 'firebase_options.dart'; import 'firebase_options.dart';
@ -39,20 +44,10 @@ void main() async {
Hive.registerAdapter(SCNRequestAdapter()); Hive.registerAdapter(SCNRequestAdapter());
Hive.registerAdapter(SCNLogAdapter()); Hive.registerAdapter(SCNLogAdapter());
Hive.registerAdapter(SCNLogLevelAdapter()); Hive.registerAdapter(SCNLogLevelAdapter());
Hive.registerAdapter(MessageAdapter()); Hive.registerAdapter(SCNMessageAdapter());
Hive.registerAdapter(ChannelAdapter()); Hive.registerAdapter(ChannelAdapter());
Hive.registerAdapter(FBMessageAdapter()); Hive.registerAdapter(FBMessageAdapter());
print('[INIT] Load Hive<scn-requests>...');
try {
await Hive.openBox<SCNRequest>('scn-requests');
} catch (exc, trace) {
Hive.deleteBoxFromDisk('scn-requests');
await Hive.openBox<SCNRequest>('scn-requests');
ApplicationLog.error('Failed to open Hive-Box: scn-requests: ' + exc.toString(), trace: trace);
}
print('[INIT] Load Hive<scn-logs>...'); print('[INIT] Load Hive<scn-logs>...');
try { try {
@ -63,13 +58,23 @@ void main() async {
ApplicationLog.error('Failed to open Hive-Box: scn-logs: ' + exc.toString(), trace: trace); ApplicationLog.error('Failed to open Hive-Box: scn-logs: ' + exc.toString(), trace: trace);
} }
print('[INIT] Load Hive<scn-requests>...');
try {
await Hive.openBox<SCNRequest>('scn-requests');
} catch (exc, trace) {
Hive.deleteBoxFromDisk('scn-requests');
await Hive.openBox<SCNRequest>('scn-requests');
ApplicationLog.error('Failed to open Hive-Box: scn-requests: ' + exc.toString(), trace: trace);
}
print('[INIT] Load Hive<scn-message-cache>...'); print('[INIT] Load Hive<scn-message-cache>...');
try { try {
await Hive.openBox<Message>('scn-message-cache'); await Hive.openBox<SCNMessage>('scn-message-cache');
} catch (exc, trace) { } catch (exc, trace) {
Hive.deleteBoxFromDisk('scn-message-cache'); Hive.deleteBoxFromDisk('scn-message-cache');
await Hive.openBox<Message>('scn-message-cache'); await Hive.openBox<SCNMessage>('scn-message-cache');
ApplicationLog.error('Failed to open Hive-Box: scn-message-cache' + exc.toString(), trace: trace); ApplicationLog.error('Failed to open Hive-Box: scn-message-cache' + exc.toString(), trace: trace);
} }
@ -98,20 +103,19 @@ void main() async {
final appAuth = AppAuth(); // ensure UserAccount is loaded final appAuth = AppAuth(); // ensure UserAccount is loaded
if (appAuth.isAuth()) { if (appAuth.isAuth()) {
try { // load user+client in background
print('[INIT] Load User...'); () async {
await appAuth.loadUser(); try {
//TODO fallback to cached user (perhaps event use cached client (if exists) directly and only update/load in background) await appAuth.loadUser();
} catch (exc, trace) { } catch (exc, trace) {
ApplicationLog.error('Failed to load user (on startup): ' + exc.toString(), trace: trace); ApplicationLog.error('Failed to load user (background load on startup): ' + exc.toString(), trace: trace);
} }
try { try {
print('[INIT] Load Client...'); await appAuth.loadClient();
await appAuth.loadClient(); } catch (exc, trace) {
//TODO fallback to cached client (perhaps event use cached client (if exists) directly and only update/load in background) ApplicationLog.error('Failed to load user (background load on startup): ' + exc.toString(), trace: trace);
} catch (exc, trace) { }
ApplicationLog.error('Failed to load user (on startup): ' + exc.toString(), trace: trace); }();
}
} }
if (!Platform.isLinux) { if (!Platform.isLinux) {
@ -147,6 +151,38 @@ void main() async {
print('[INIT] Skip Firebase init (Platform == Linux)...'); print('[INIT] Skip Firebase init (Platform == Linux)...');
} }
print('[INIT] Load Notifications...');
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
final flutterLocalNotificationsPluginImpl = flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>();
if (flutterLocalNotificationsPluginImpl == null) {
ApplicationLog.error('Failed to get AndroidFlutterLocalNotificationsPlugin', trace: StackTrace.current);
} else {
flutterLocalNotificationsPluginImpl.requestNotificationsPermission();
final initializationSettingsAndroid = AndroidInitializationSettings('ic_notification_white');
final initializationSettingsDarwin = DarwinInitializationSettings(
requestAlertPermission: true,
requestBadgePermission: true,
requestSoundPermission: true,
onDidReceiveLocalNotification: _receiveLocalDarwinNotification,
notificationCategories: getDarwinNotificationCategories(),
);
final initializationSettingsLinux = LinuxInitializationSettings(defaultActionName: 'Open notification');
final initializationSettings = InitializationSettings(
android: initializationSettingsAndroid,
iOS: initializationSettingsDarwin,
linux: initializationSettingsLinux,
);
flutterLocalNotificationsPlugin.initialize(initializationSettings, onDidReceiveNotificationResponse: _receiveLocalNotification);
final appLaunchNotification = await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails();
if (appLaunchNotification != null) {
//TODO show message (also this only works on android+localnotifications, also handle ios)
ApplicationLog.info('App launched by notification: ${appLaunchNotification.notificationResponse?.id}');
}
}
ApplicationLog.debug('[INIT] Application started'); ApplicationLog.debug('[INIT] Application started');
runApp( runApp(
@ -155,6 +191,7 @@ void main() async {
ChangeNotifierProvider(create: (context) => AppAuth(), lazy: false), ChangeNotifierProvider(create: (context) => AppAuth(), lazy: false),
ChangeNotifierProvider(create: (context) => AppTheme(), lazy: false), ChangeNotifierProvider(create: (context) => AppTheme(), lazy: false),
ChangeNotifierProvider(create: (context) => AppBarState(), lazy: false), ChangeNotifierProvider(create: (context) => AppBarState(), lazy: false),
ChangeNotifierProvider(create: (context) => AppSettings(), lazy: false),
], ],
child: SCNApp(), child: SCNApp(),
), ),
@ -188,6 +225,11 @@ class SCNApp extends StatelessWidget {
} }
} }
@pragma('vm:entry-point')
void notificationTapBackground(NotificationResponse notificationResponse) {
ApplicationLog.info('Received local notification<vm:entry-point>: ${notificationResponse.id}');
}
void setFirebaseToken(String fcmToken) async { void setFirebaseToken(String fcmToken) async {
final acc = AppAuth(); final acc = AppAuth();
@ -224,18 +266,96 @@ void setFirebaseToken(String fcmToken) async {
} }
} }
@pragma('vm:entry-point')
Future<void> _onBackgroundMessage(RemoteMessage message) async { Future<void> _onBackgroundMessage(RemoteMessage message) async {
await _receiveMessage(message, false); await _receiveMessage(message, false);
} }
@pragma('vm:entry-point')
void _onForegroundMessage(RemoteMessage message) { void _onForegroundMessage(RemoteMessage message) {
_receiveMessage(message, true); _receiveMessage(message, true);
} }
Future<void> _receiveMessage(RemoteMessage message, bool foreground) async { Future<void> _receiveMessage(RemoteMessage message, bool foreground) async {
// ensure init try {
Hive.openBox<SCNLog>('scn-logs'); // ensure globals init
if (!Globals().isInitialized) {
print('[LATE-INIT] Init Globals() - to ensure working _receiveMessage($foreground)...');
await Globals().init();
}
// ensure hive init
if (!Hive.isBoxOpen('scn-logs')) {
print('[LATE-INIT] Init Hive - to ensure working _receiveMessage($foreground)...');
await Hive.initFlutter();
Hive.registerAdapter(SCNRequestAdapter());
Hive.registerAdapter(SCNLogAdapter());
Hive.registerAdapter(SCNLogLevelAdapter());
Hive.registerAdapter(SCNMessageAdapter());
Hive.registerAdapter(ChannelAdapter());
Hive.registerAdapter(FBMessageAdapter());
}
print('[LATE-INIT] Ensure hive boxes are open for _receiveMessage($foreground)...');
await Hive.openBox<SCNLog>('scn-logs');
await Hive.openBox<FBMessage>('scn-fb-messages');
await Hive.openBox<SCNMessage>('scn-message-cache');
} catch (exc, 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);
return;
}
ApplicationLog.info('Received FB message (${foreground ? 'foreground' : 'background'}): ${message.messageId ?? 'NULL'}'); ApplicationLog.info('Received FB message (${foreground ? 'foreground' : 'background'}): ${message.messageId ?? 'NULL'}');
FBMessageLog.insert(message);
String scn_msg_id;
try {
scn_msg_id = message.data['scn_msg_id'] as String;
final timestamp = DateTime.fromMillisecondsSinceEpoch(int.parse(message.data['timestamp'] as String) * 1000);
final title = message.data['title'] as String;
final channel = message.data['channel'] as String;
final channel_id = message.data['channel_id'] as String;
final body = message.data['body'] as String;
Notifier.showLocalNotification(channel_id, channel, 'Channel: ${channel}', title, body, timestamp);
} catch (exc, 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);
return;
}
try {
FBMessageLog.insert(message);
} catch (exc, 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);
return;
}
try {
final msg = await APIClient.getMessage(AppAuth(), scn_msg_id);
SCNDataCache().addToMessageCache([msg]);
if (foreground) AppEvents().notifyMessageReceivedListeners(msg);
} catch (exc, trace) {
ApplicationLog.error('Failed to query+persist message' + exc.toString(), trace: trace);
return;
}
}
void _receiveLocalDarwinNotification(int id, String? title, String? body, String? payload) {
ApplicationLog.info('Received local notification<darwin>: $id -> [$title]');
}
void _receiveLocalNotification(NotificationResponse details) {
ApplicationLog.info('Received local notification: ${details.id}');
}
List<DarwinNotificationCategory> getDarwinNotificationCategories() {
return <DarwinNotificationCategory>[
//TODO ?!?
];
} }

View File

@ -32,6 +32,19 @@ class Client {
); );
} }
Map<String, dynamic> toJson() {
return {
'client_id': clientID,
'user_id': userID,
'type': type,
'fcm_token': fcmToken,
'timestamp_created': timestampCreated,
'agent_model': agentModel,
'agent_version': agentVersion,
'name': name,
};
}
static List<Client> fromJsonArray(List<dynamic> jsonArr) { static List<Client> fromJsonArray(List<dynamic> jsonArr) {
return jsonArr.map<Client>((e) => Client.fromJson(e as Map<String, dynamic>)).toList(); return jsonArr.map<Client>((e) => Client.fromJson(e as Map<String, dynamic>)).toList();
} }

View File

@ -1,10 +1,10 @@
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:simplecloudnotifier/state/interfaces.dart'; import 'package:simplecloudnotifier/state/interfaces.dart';
part 'message.g.dart'; part 'scn_message.g.dart';
@HiveType(typeId: 105) @HiveType(typeId: 105)
class Message extends HiveObject implements FieldDebuggable { class SCNMessage extends HiveObject implements FieldDebuggable {
@HiveField(0) @HiveField(0)
final String messageID; final String messageID;
@ -33,7 +33,7 @@ class Message extends HiveObject implements FieldDebuggable {
@HiveField(21) @HiveField(21)
final bool trimmed; final bool trimmed;
Message({ SCNMessage({
required this.messageID, required this.messageID,
required this.senderUserID, required this.senderUserID,
required this.channelInternalName, required this.channelInternalName,
@ -49,8 +49,8 @@ class Message extends HiveObject implements FieldDebuggable {
required this.trimmed, required this.trimmed,
}); });
factory Message.fromJson(Map<String, dynamic> json) { factory SCNMessage.fromJson(Map<String, dynamic> json) {
return Message( return SCNMessage(
messageID: json['message_id'] as String, messageID: json['message_id'] as String,
senderUserID: json['sender_user_id'] as String, senderUserID: json['sender_user_id'] as String,
channelInternalName: json['channel_internal_name'] as String, channelInternalName: json['channel_internal_name'] as String,
@ -67,10 +67,10 @@ class Message extends HiveObject implements FieldDebuggable {
); );
} }
static (String, List<Message>) fromPaginatedJsonArray(Map<String, dynamic> data, String keyMessages, String keyToken) { static (String, List<SCNMessage>) fromPaginatedJsonArray(Map<String, dynamic> data, String keyMessages, String keyToken) {
final npt = data[keyToken] as String; final npt = data[keyToken] as String;
final messages = (data[keyMessages] as List<dynamic>).map<Message>((e) => Message.fromJson(e as Map<String, dynamic>)).toList(); final messages = (data[keyMessages] as List<dynamic>).map<SCNMessage>((e) => SCNMessage.fromJson(e as Map<String, dynamic>)).toList();
return (npt, messages); return (npt, messages);
} }

View File

@ -1,22 +1,22 @@
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
part of 'message.dart'; part of 'scn_message.dart';
// ************************************************************************** // **************************************************************************
// TypeAdapterGenerator // TypeAdapterGenerator
// ************************************************************************** // **************************************************************************
class MessageAdapter extends TypeAdapter<Message> { class SCNMessageAdapter extends TypeAdapter<SCNMessage> {
@override @override
final int typeId = 105; final int typeId = 105;
@override @override
Message read(BinaryReader reader) { SCNMessage read(BinaryReader reader) {
final numOfFields = reader.readByte(); final numOfFields = reader.readByte();
final fields = <int, dynamic>{ final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
}; };
return Message( return SCNMessage(
messageID: fields[0] as String, messageID: fields[0] as String,
senderUserID: fields[10] as String, senderUserID: fields[10] as String,
channelInternalName: fields[11] as String, channelInternalName: fields[11] as String,
@ -34,7 +34,7 @@ class MessageAdapter extends TypeAdapter<Message> {
} }
@override @override
void write(BinaryWriter writer, Message obj) { void write(BinaryWriter writer, SCNMessage obj) {
writer writer
..writeByte(13) ..writeByte(13)
..writeByte(0) ..writeByte(0)
@ -71,7 +71,7 @@ class MessageAdapter extends TypeAdapter<Message> {
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
other is MessageAdapter && other is SCNMessageAdapter &&
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
typeId == other.typeId; typeId == other.typeId;
} }

View File

@ -63,6 +63,29 @@ class User {
maxUserMessageIDLength: json['max_user_message_id_length'] as int, maxUserMessageIDLength: json['max_user_message_id_length'] as int,
); );
} }
Map<String, dynamic> toJson() {
return {
'user_id': userID,
'username': username,
'timestamp_created': timestampCreated,
'timestamp_lastread': timestampLastRead,
'timestamp_lastsent': timestampLastSent,
'messages_sent': messagesSent,
'quota_used': quotaUsed,
'quota_remaining': quotaRemaining,
'quota_max': quotaPerDay,
'is_pro': isPro,
'default_channel': defaultChannel,
'max_body_size': maxBodySize,
'max_title_length': maxTitleLength,
'default_priority': defaultPriority,
'max_channel_name_length': maxChannelNameLength,
'max_channel_description_length': maxChannelDescriptionLength,
'max_sender_name_length': maxSenderNameLength,
'max_user_message_id_length': maxUserMessageIDLength,
};
}
} }
class UserWithClientsAndKeys { class UserWithClientsAndKeys {

View File

@ -59,7 +59,6 @@ class _SCNNavLayoutState extends State<SCNNavLayout> {
return Scaffold( return Scaffold(
appBar: SCNAppBar( appBar: SCNAppBar(
title: null, title: null,
showDebug: true,
showSearch: _selectedIndex == 0 || _selectedIndex == 1, showSearch: _selectedIndex == 0 || _selectedIndex == 1,
showShare: false, showShare: false,
showThemeSwitch: true, showThemeSwitch: true,

View File

@ -3,7 +3,7 @@ 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';
import 'package:simplecloudnotifier/models/message.dart'; import 'package:simplecloudnotifier/models/scn_message.dart';
import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/state/app_auth.dart';
class ChannelListItem extends StatefulWidget { class ChannelListItem extends StatefulWidget {
@ -23,7 +23,7 @@ class ChannelListItem extends StatefulWidget {
} }
class _ChannelListItemState extends State<ChannelListItem> { class _ChannelListItemState extends State<ChannelListItem> {
Message? lastMessage; SCNMessage? lastMessage;
@override @override
void initState() { void initState() {

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:simplecloudnotifier/utils/notifier.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';
@ -52,6 +53,12 @@ class _DebugActionsPageState extends State<DebugActionsPage> {
onPressed: _sendTokenToServer, onPressed: _sendTokenToServer,
text: 'Send FCM Token to Server', text: 'Send FCM Token to Server',
), ),
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',
),
], ],
), ),
), ),

View File

@ -30,7 +30,6 @@ class _DebugMainPageState extends State<DebugMainPage> {
return SCNScaffold( return SCNScaffold(
title: 'Debug', title: 'Debug',
showSearch: false, showSearch: false,
showDebug: false,
child: Column( child: Column(
children: [ children: [
Padding( Padding(

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/models/message.dart'; import 'package:simplecloudnotifier/models/scn_message.dart';
import 'package:simplecloudnotifier/pages/debug/debug_persistence_hive.dart'; import 'package:simplecloudnotifier/pages/debug/debug_persistence_hive.dart';
import 'package:simplecloudnotifier/pages/debug/debug_persistence_sharedprefs.dart'; import 'package:simplecloudnotifier/pages/debug/debug_persistence_sharedprefs.dart';
import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/application_log.dart';
@ -36,7 +36,7 @@ class _DebugPersistencePageState extends State<DebugPersistencePage> {
_buildSharedPrefCard(context), _buildSharedPrefCard(context),
_buildHiveCard(context, () => Hive.box<SCNRequest>('scn-requests'), 'scn-requests'), _buildHiveCard(context, () => Hive.box<SCNRequest>('scn-requests'), 'scn-requests'),
_buildHiveCard(context, () => Hive.box<SCNLog>('scn-logs'), 'scn-logs'), _buildHiveCard(context, () => Hive.box<SCNLog>('scn-logs'), 'scn-logs'),
_buildHiveCard(context, () => Hive.box<Message>('scn-message-cache'), 'scn-message-cache'), _buildHiveCard(context, () => Hive.box<SCNMessage>('scn-message-cache'), 'scn-message-cache'),
_buildHiveCard(context, () => Hive.box<Channel>('scn-channel-cache'), 'scn-channel-cache'), _buildHiveCard(context, () => Hive.box<Channel>('scn-channel-cache'), 'scn-channel-cache'),
_buildHiveCard(context, () => Hive.box<FBMessage>('scn-fb-messages'), 'scn-fb-messages'), _buildHiveCard(context, () => Hive.box<FBMessage>('scn-fb-messages'), 'scn-fb-messages'),
], ],
@ -71,7 +71,7 @@ class _DebugPersistencePageState extends State<DebugPersistencePage> {
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
Navi.push(context, () => DebugHiveBoxPage(boxName: boxname, box: Hive.box<FBMessage>(boxname))); Navi.push(context, () => DebugHiveBoxPage(boxName: boxname, box: boxFunc()));
}, },
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,

View File

@ -16,7 +16,6 @@ class DebugHiveBoxPage extends StatelessWidget {
return SCNScaffold( return SCNScaffold(
title: 'Hive: ' + boxName, title: 'Hive: ' + boxName,
showSearch: false, showSearch: false,
showDebug: false,
child: ListView.separated( child: ListView.separated(
itemCount: box.length, itemCount: box.length,
itemBuilder: (context, listIndex) { itemBuilder: (context, listIndex) {
@ -24,8 +23,9 @@ class DebugHiveBoxPage extends StatelessWidget {
onTap: () { onTap: () {
Navi.push(context, () => DebugHiveEntryPage(value: box.getAt(listIndex)!)); Navi.push(context, () => DebugHiveEntryPage(value: box.getAt(listIndex)!));
}, },
child: ListTile( child: Container(
title: Text(box.getAt(listIndex).toString(), style: TextStyle(fontWeight: FontWeight.bold)), padding: EdgeInsets.fromLTRB(8, 4, 8, 4),
child: Text(box.getAt(listIndex).toString(), style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12)),
), ),
); );
}, },

View File

@ -13,11 +13,13 @@ class DebugHiveEntryPage extends StatelessWidget {
return SCNScaffold( return SCNScaffold(
title: 'HiveEntry', title: 'HiveEntry',
showSearch: false, showSearch: false,
showDebug: false,
child: ListView.separated( child: ListView.separated(
itemCount: fields.length, itemCount: fields.length,
itemBuilder: (context, listIndex) { itemBuilder: (context, listIndex) {
return ListTile( return ListTile(
dense: true,
contentPadding: EdgeInsets.fromLTRB(8, 0, 8, 0),
visualDensity: VisualDensity(horizontal: 0, vertical: -4),
title: Text(fields[listIndex].$1, style: TextStyle(fontWeight: FontWeight.bold)), title: Text(fields[listIndex].$1, style: TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text(fields[listIndex].$2, style: TextStyle(fontFamily: "monospace")), subtitle: Text(fields[listIndex].$2, style: TextStyle(fontFamily: "monospace")),
); );

View File

@ -13,7 +13,6 @@ class DebugSharedPrefPage extends StatelessWidget {
return SCNScaffold( return SCNScaffold(
title: 'SharedPreferences', title: 'SharedPreferences',
showSearch: false, showSearch: false,
showDebug: false,
child: ListView.separated( child: ListView.separated(
itemCount: sharedPref.getKeys().length, itemCount: sharedPref.getKeys().length,
itemBuilder: (context, listIndex) { itemBuilder: (context, listIndex) {

View File

@ -16,7 +16,6 @@ class DebugRequestViewPage extends StatelessWidget {
return SCNScaffold( return SCNScaffold(
title: 'Request', title: 'Request',
showSearch: false, showSearch: false,
showDebug: false,
child: SingleChildScrollView( child: SingleChildScrollView(
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),

View File

@ -0,0 +1,36 @@
import 'package:flutter/src/widgets/icon_data.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
enum MessageFilterChipletType {
search,
channel,
sender,
timeRange,
priority,
sendkey,
}
class MessageFilterChiplet {
final String label;
final String value;
final MessageFilterChipletType type;
MessageFilterChiplet({required this.label, required this.value, required this.type});
IconData? icon() {
switch (type) {
case MessageFilterChipletType.search:
return FontAwesomeIcons.magnifyingGlass;
case MessageFilterChipletType.channel:
return FontAwesomeIcons.snake;
case MessageFilterChipletType.sender:
return FontAwesomeIcons.signature;
case MessageFilterChipletType.timeRange:
return FontAwesomeIcons.timer;
case MessageFilterChipletType.priority:
return FontAwesomeIcons.bolt;
case MessageFilterChipletType.sendkey:
return FontAwesomeIcons.gearCode;
}
}
}

View File

@ -4,12 +4,16 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.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';
import 'package:simplecloudnotifier/models/message.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/pages/message_view/message_view.dart';
import 'package:simplecloudnotifier/settings/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/application_log.dart'; import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/pages/message_list/message_list_item.dart'; import 'package:simplecloudnotifier/pages/message_list/message_list_item.dart';
import 'package:simplecloudnotifier/state/scn_data_cache.dart';
import 'package:simplecloudnotifier/utils/navi.dart'; import 'package:simplecloudnotifier/utils/navi.dart';
class MessageListPage extends StatefulWidget { class MessageListPage extends StatefulWidget {
@ -25,20 +29,23 @@ class MessageListPage extends StatefulWidget {
} }
class _MessageListPageState extends State<MessageListPage> with RouteAware { class _MessageListPageState extends State<MessageListPage> with RouteAware {
static const _pageSize = 128;
late final AppLifecycleListener _lifecyleListener; late final AppLifecycleListener _lifecyleListener;
PagingController<String, Message> _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: '@start'); PagingController<String, SCNMessage> _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: '@start');
Map<String, Channel>? _channels = null; Map<String, Channel>? _channels = null;
bool _isInitialized = false; bool _isInitialized = false;
List<MessageFilterChiplet> _filterChiplets = [];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
AppEvents().subscribeSearchListener(_onAppBarSearch);
AppEvents().subscribeMessageReceivedListener(_onMessageReceivedViaNotification);
_pagingController.addPageRequestListener(_fetchPage); _pagingController.addPageRequestListener(_fetchPage);
if (widget.isVisiblePage && !_isInitialized) _realInitState(); if (widget.isVisiblePage && !_isInitialized) _realInitState();
@ -64,18 +71,12 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
void _realInitState() { void _realInitState() {
ApplicationLog.debug('MessageListPage::_realInitState'); ApplicationLog.debug('MessageListPage::_realInitState');
final chnCache = Hive.box<Channel>('scn-channel-cache'); if (SCNDataCache().hasMessagesAndChannels()) {
final msgCache = Hive.box<Message>('scn-message-cache');
if (chnCache.isNotEmpty && msgCache.isNotEmpty) {
// ==== Use cache values - and refresh in background // ==== Use cache values - and refresh in background
_channels = <String, Channel>{for (var v in chnCache.values) v.channelID: v}; _channels = SCNDataCache().getChannelMap();
final cacheMessages = msgCache.values.toList(); _pagingController.value = PagingState(nextPageKey: null, itemList: SCNDataCache().getMessagesSorted(), error: null);
cacheMessages.sort((a, b) => -1 * a.timestamp.compareTo(b.timestamp));
_pagingController.value = PagingState(nextPageKey: null, itemList: cacheMessages, error: null);
_backgroundRefresh(true); _backgroundRefresh(true);
} else { } else {
@ -95,6 +96,8 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
@override @override
void dispose() { void dispose() {
ApplicationLog.debug('MessageListPage::dispose'); ApplicationLog.debug('MessageListPage::dispose');
AppEvents().unsubscribeSearchListener(_onAppBarSearch);
AppEvents().unsubscribeMessageReceivedListener(_onMessageReceivedViaNotification);
Navi.modalRouteObserver.unsubscribe(this); Navi.modalRouteObserver.unsubscribe(this);
_pagingController.dispose(); _pagingController.dispose();
_lifecyleListener.dispose(); _lifecyleListener.dispose();
@ -108,8 +111,10 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
@override @override
void didPopNext() { void didPopNext() {
ApplicationLog.debug('[MessageList::RouteObserver] --> didPopNext (will background-refresh)'); if (AppSettings().alwaysBackgroundRefreshMessageListOnPop) {
_backgroundRefresh(false); ApplicationLog.debug('[MessageList::RouteObserver] --> didPopNext (will background-refresh)');
_backgroundRefresh(false);
}
} }
void _onLifecycleResume() { void _onLifecycleResume() {
@ -119,6 +124,7 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
Future<void> _fetchPage(String thisPageToken) async { Future<void> _fetchPage(String thisPageToken) async {
final acc = Provider.of<AppAuth>(context, listen: false); final acc = Provider.of<AppAuth>(context, listen: false);
final cfg = Provider.of<AppSettings>(context, listen: false);
ApplicationLog.debug('Start MessageList::_pagingController::_fetchPage [ ${thisPageToken} ]'); ApplicationLog.debug('Start MessageList::_pagingController::_fetchPage [ ${thisPageToken} ]');
@ -132,12 +138,12 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
final channels = await APIClient.getChannelList(acc, ChannelSelector.allAny); final channels = await APIClient.getChannelList(acc, ChannelSelector.allAny);
_channels = <String, Channel>{for (var v in channels) v.channel.channelID: v.channel}; _channels = <String, Channel>{for (var v in channels) v.channel.channelID: v.channel};
_setChannelCache(channels); // no await SCNDataCache().setChannelCache(channels); // no await
} }
final (npt, newItems) = await APIClient.getMessageList(acc, thisPageToken, pageSize: _pageSize); final (npt, newItems) = await APIClient.getMessageList(acc, thisPageToken, pageSize: cfg.messagePageSize);
_addToMessageCache(newItems); // no await SCNDataCache().addToMessageCache(newItems); // no await
ApplicationLog.debug('Finished MessageList::_pagingController::_fetchPage [ ${newItems.length} items and npt: ${thisPageToken} --> ${npt} ]'); ApplicationLog.debug('Finished MessageList::_pagingController::_fetchPage [ ${newItems.length} items and npt: ${thisPageToken} --> ${npt} ]');
@ -154,6 +160,7 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
Future<void> _backgroundRefresh(bool fullReplaceState) async { Future<void> _backgroundRefresh(bool fullReplaceState) async {
final acc = Provider.of<AppAuth>(context, listen: false); final acc = Provider.of<AppAuth>(context, listen: false);
final cfg = Provider.of<AppSettings>(context, listen: false);
ApplicationLog.debug('Start background refresh of message list (fullReplaceState: $fullReplaceState)'); ApplicationLog.debug('Start background refresh of message list (fullReplaceState: $fullReplaceState)');
@ -167,12 +174,12 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
setState(() { setState(() {
_channels = <String, Channel>{for (var v in channels) v.channel.channelID: v.channel}; _channels = <String, Channel>{for (var v in channels) v.channel.channelID: v.channel};
}); });
_setChannelCache(channels); // no await SCNDataCache().setChannelCache(channels); // no await
} }
final (npt, newItems) = await APIClient.getMessageList(acc, '@start', pageSize: _pageSize); final (npt, newItems) = await APIClient.getMessageList(acc, '@start', pageSize: cfg.messagePageSize);
_addToMessageCache(newItems); // no await SCNDataCache().addToMessageCache(newItems); // no await
if (fullReplaceState) { if (fullReplaceState) {
// fully replace/reset state // fully replace/reset state
@ -221,49 +228,63 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
padding: EdgeInsets.fromLTRB(8, 4, 8, 4), padding: EdgeInsets.fromLTRB(8, 4, 8, 4),
child: RefreshIndicator( child: Column(
onRefresh: () => Future.sync( crossAxisAlignment: CrossAxisAlignment.stretch,
() => _pagingController.refresh(), children: [
), if (_filterChiplets.isNotEmpty)
child: PagedListView<String, Message>( Wrap(
pagingController: _pagingController, alignment: WrapAlignment.start,
builderDelegate: PagedChildBuilderDelegate<Message>( spacing: 5.0,
itemBuilder: (context, item, index) => MessageListItem( children: [
message: item, for (var chiplet in _filterChiplets) _buildFilterChip(context, chiplet),
allChannels: _channels ?? {}, ],
onPressed: () { ),
Navi.push(context, () => MessageViewPage(message: item)); Expanded(
}, child: RefreshIndicator(
onRefresh: () => Future.sync(
() => _pagingController.refresh(),
),
child: PagedListView<String, SCNMessage>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<SCNMessage>(
itemBuilder: (context, item, index) => MessageListItem(
message: item,
allChannels: _channels ?? {},
onPressed: () {
Navi.push(context, () => MessageViewPage(message: item));
},
),
),
),
), ),
), ),
), ],
), ),
); );
} }
Future<void> _setChannelCache(List<ChannelWithSubscription> channels) async { Widget _buildFilterChip(BuildContext context, MessageFilterChiplet chiplet) {
final cache = Hive.box<Channel>('scn-channel-cache'); return Padding(
padding: const EdgeInsets.fromLTRB(0, 2, 0, 2),
if (cache.length != channels.length) await cache.clear(); child: InputChip(
avatar: Icon(chiplet.icon()),
for (var chn in channels) await cache.put(chn.channel.channelID, chn.channel); label: Text(chiplet.label),
onDeleted: () => setState(() => _filterChiplets.remove(chiplet)),
onPressed: () {/* TODO idk what to do here ? */},
visualDensity: VisualDensity(horizontal: -4, vertical: -4),
),
);
} }
Future<void> _addToMessageCache(List<Message> newItems) async { void _onAppBarSearch(String str) {
final cache = Hive.box<Message>('scn-message-cache'); setState(() {
_filterChiplets = _filterChiplets.where((element) => false).toList() + [MessageFilterChiplet(label: str, value: str, type: MessageFilterChipletType.search)];
});
}
for (var msg in newItems) await cache.put(msg.messageID, msg); void _onMessageReceivedViaNotification(SCNMessage msg) {
setState(() {
// delete all but the newest 128 messages _pagingController.itemList = [msg] + (_pagingController.itemList ?? []);
});
if (cache.length < _pageSize) return;
final allValues = cache.values.toList();
allValues.sort((a, b) => -1 * a.timestamp.compareTo(b.timestamp));
for (var val in allValues.sublist(_pageSize)) {
await cache.delete(val.messageID);
}
} }
} }

View File

@ -3,7 +3,7 @@ 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:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/models/message.dart'; import 'package:simplecloudnotifier/models/scn_message.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:simplecloudnotifier/utils/ui.dart'; import 'package:simplecloudnotifier/utils/ui.dart';
@ -18,7 +18,7 @@ class MessageListItem extends StatelessWidget {
super.key, super.key,
}); });
final Message message; final SCNMessage message;
final Map<String, Channel> allChannels; final Map<String, Channel> allChannels;
final Null Function() onPressed; final Null Function() onPressed;
@ -176,11 +176,11 @@ class MessageListItem extends StatelessWidget {
return v; return v;
} }
String resolveChannelName(Message message) { String resolveChannelName(SCNMessage message) {
return allChannels[message.channelID]?.displayName ?? message.channelInternalName; return allChannels[message.channelID]?.displayName ?? message.channelInternalName;
} }
bool showChannel(Message message) { bool showChannel(SCNMessage message) {
return message.channelInternalName != 'main'; return message.channelInternalName != 'main';
} }
} }

View File

@ -8,7 +8,7 @@ import 'package:simplecloudnotifier/api/api_client.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';
import 'package:simplecloudnotifier/models/message.dart'; import 'package:simplecloudnotifier/models/scn_message.dart';
import 'package:simplecloudnotifier/models/user.dart'; import 'package:simplecloudnotifier/models/user.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';
@ -18,15 +18,15 @@ import 'package:simplecloudnotifier/utils/ui.dart';
class MessageViewPage extends StatefulWidget { class MessageViewPage extends StatefulWidget {
const MessageViewPage({super.key, required this.message}); const MessageViewPage({super.key, required this.message});
final Message message; // Potentially trimmed final SCNMessage message; // Potentially trimmed
@override @override
State<MessageViewPage> createState() => _MessageViewPageState(); State<MessageViewPage> createState() => _MessageViewPageState();
} }
class _MessageViewPageState extends State<MessageViewPage> { class _MessageViewPageState extends State<MessageViewPage> {
late Future<(Message, ChannelPreview, KeyTokenPreview, UserPreview)>? mainFuture; late Future<(SCNMessage, ChannelPreview, KeyTokenPreview, UserPreview)>? mainFuture;
(Message, ChannelPreview, KeyTokenPreview, UserPreview)? mainFutureSnapshot = null; (SCNMessage, ChannelPreview, KeyTokenPreview, UserPreview)? mainFutureSnapshot = null;
static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm'); static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm');
bool _monospaceMode = false; bool _monospaceMode = false;
@ -37,7 +37,7 @@ class _MessageViewPageState extends State<MessageViewPage> {
super.initState(); super.initState();
} }
Future<(Message, ChannelPreview, KeyTokenPreview, UserPreview)> fetchData() async { Future<(SCNMessage, ChannelPreview, KeyTokenPreview, UserPreview)> fetchData() async {
try { try {
await Future.delayed(const Duration(seconds: 0), () {}); // this is annoyingly important - otherwise we call setLoadingIndeterminate directly in initStat() and get an exception.... await Future.delayed(const Duration(seconds: 0), () {}); // this is annoyingly important - otherwise we call setLoadingIndeterminate directly in initStat() and get an exception....
@ -79,7 +79,7 @@ class _MessageViewPageState extends State<MessageViewPage> {
showSearch: false, showSearch: false,
showShare: true, showShare: true,
onShare: _share, onShare: _share,
child: FutureBuilder<(Message, ChannelPreview, KeyTokenPreview, UserPreview)>( child: FutureBuilder<(SCNMessage, ChannelPreview, KeyTokenPreview, UserPreview)>(
future: mainFuture, future: mainFuture,
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasData) { if (snapshot.hasData) {
@ -118,7 +118,7 @@ class _MessageViewPageState extends State<MessageViewPage> {
} }
} }
Widget _buildMessageView(BuildContext context, Message message, ChannelPreview? channel, KeyTokenPreview? token, UserPreview? user) { Widget _buildMessageView(BuildContext context, SCNMessage message, ChannelPreview? channel, KeyTokenPreview? token, UserPreview? user) {
final userAccUserID = context.select<AppAuth, String?>((v) => v.userID); final userAccUserID = context.select<AppAuth, String?>((v) => v.userID);
return SingleChildScrollView( return SingleChildScrollView(
@ -137,6 +137,7 @@ class _MessageViewPageState extends State<MessageViewPage> {
_buildMetaCard(context, FontAwesomeIcons.solidSnake, 'Channel', [message.channelID, channel?.displayName ?? message.channelInternalName], () => {/*TODO*/}), _buildMetaCard(context, FontAwesomeIcons.solidSnake, 'Channel', [message.channelID, channel?.displayName ?? message.channelInternalName], () => {/*TODO*/}),
_buildMetaCard(context, FontAwesomeIcons.solidTimer, 'Timestamp', [message.timestamp], null), _buildMetaCard(context, FontAwesomeIcons.solidTimer, 'Timestamp', [message.timestamp], null),
_buildMetaCard(context, FontAwesomeIcons.solidUser, 'User', [if (user != null) user.userID, if (user?.username != null) user!.username!], () => {/*TODO*/}), //TODO _buildMetaCard(context, FontAwesomeIcons.solidUser, 'User', [if (user != null) user.userID, if (user?.username != null) user!.username!], () => {/*TODO*/}), //TODO
_buildMetaCard(context, FontAwesomeIcons.solidBolt, 'Priority', [_prettyPrintPriority(message.priority)], () => {/*TODO*/}), //TODO
if (message.senderUserID == userAccUserID) UI.button(text: "Delete Message", onPressed: () {/*TODO*/}, color: Colors.red[900]), if (message.senderUserID == userAccUserID) UI.button(text: "Delete Message", onPressed: () {/*TODO*/}, color: Colors.red[900]),
], ],
), ),
@ -144,11 +145,11 @@ class _MessageViewPageState extends State<MessageViewPage> {
); );
} }
String _resolveChannelName(ChannelPreview? channel, Message message) { String _resolveChannelName(ChannelPreview? channel, SCNMessage message) {
return channel?.displayName ?? message.channelInternalName; return channel?.displayName ?? message.channelInternalName;
} }
List<Widget> _buildMessageHeader(BuildContext context, Message message, ChannelPreview? channel) { List<Widget> _buildMessageHeader(BuildContext context, SCNMessage message, ChannelPreview? channel) {
return [ return [
Row( Row(
children: [ children: [
@ -167,7 +168,7 @@ class _MessageViewPageState extends State<MessageViewPage> {
]; ];
} }
List<Widget> _buildMessageContent(BuildContext context, Message message) { List<Widget> _buildMessageContent(BuildContext context, SCNMessage message) {
return [ return [
Row( Row(
children: [ children: [
@ -249,7 +250,20 @@ class _MessageViewPageState extends State<MessageViewPage> {
} }
} }
String _preformatTitle(Message message) { String _preformatTitle(SCNMessage message) {
return message.title.replaceAll('\n', '').replaceAll('\r', '').replaceAll('\t', ' '); return message.title.replaceAll('\n', '').replaceAll('\r', '').replaceAll('\t', ' ');
} }
String _prettyPrintPriority(int priority) {
switch (priority) {
case 0:
return 'Low (0)';
case 1:
return 'Normal (1)';
case 2:
return 'High (2)';
default:
return 'Unknown ($priority)';
}
}
} }

View File

@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
class AppSettings extends ChangeNotifier {
bool groupNotifications = true;
int messagePageSize = 128;
bool showDebugButton = true;
bool alwaysBackgroundRefreshMessageListOnPop = false;
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

@ -1,8 +1,11 @@
import 'dart:convert';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/api/api_client.dart';
import 'package:simplecloudnotifier/api/api_exception.dart'; import 'package:simplecloudnotifier/api/api_exception.dart';
import 'package:simplecloudnotifier/models/client.dart'; import 'package:simplecloudnotifier/models/client.dart';
import 'package:simplecloudnotifier/models/user.dart'; import 'package:simplecloudnotifier/models/user.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/globals.dart'; import 'package:simplecloudnotifier/state/globals.dart';
import 'package:simplecloudnotifier/state/token_source.dart'; import 'package:simplecloudnotifier/state/token_source.dart';
@ -12,9 +15,9 @@ class AppAuth extends ChangeNotifier implements TokenSource {
String? _tokenAdmin; String? _tokenAdmin;
String? _tokenSend; String? _tokenSend;
User? _user; (User, DateTime)? _user;
Client? _client;
DateTime? _clientQueryTime; (Client, DateTime)? _client;
String? get userID => _userID; String? get userID => _userID;
String? get tokenAdmin => _tokenAdmin; String? get tokenAdmin => _tokenAdmin;
@ -35,17 +38,21 @@ class AppAuth extends ChangeNotifier implements TokenSource {
} }
void set(User user, Client client, String tokenAdmin, String tokenSend) { void set(User user, Client client, String tokenAdmin, String tokenSend) {
_client = client; _client = (client, DateTime.now());
_user = user;
_user = (user, DateTime.now());
_userID = user.userID; _userID = user.userID;
_clientID = client.clientID; _clientID = client.clientID;
_tokenAdmin = tokenAdmin; _tokenAdmin = tokenAdmin;
_tokenSend = tokenSend; _tokenSend = tokenSend;
notifyListeners(); notifyListeners();
} }
void setClientAndClientID(Client client) { void setClientAndClientID(Client client) {
_client = client; _client = (client, DateTime.now());
_clientID = client.clientID; _clientID = client.clientID;
notifyListeners(); notifyListeners();
} }
@ -83,6 +90,33 @@ class AppAuth extends ChangeNotifier implements TokenSource {
_client = null; _client = null;
_user = null; _user = null;
final userjson = Globals().sharedPrefs.getString('auth.user.obj');
final userqdate = Globals().sharedPrefs.getString('auth.user.qdate');
final clientjson = Globals().sharedPrefs.getString('auth.client.obj');
final clientqdate = Globals().sharedPrefs.getString('auth.client.qdate');
if (userjson != null && userqdate != null) {
try {
final ts = DateTime.parse(userqdate);
final obj = User.fromJson(jsonDecode(userjson) as Map<String, dynamic>);
_user = (obj, ts);
} catch (exc, trace) {
ApplicationLog.error('failed to parse user object from shared-prefs (auth.user.obj): ' + exc.toString(), additional: 'Data:\n${userjson}\nQDate:\n${userqdate}', trace: trace);
_user = null;
}
}
if (clientjson != null && clientqdate != null) {
try {
final ts = DateTime.parse(clientqdate);
final obj = Client.fromJson(jsonDecode(clientjson) as Map<String, dynamic>);
_client = (obj, ts);
} catch (exc, trace) {
ApplicationLog.error('failed to parse user object from shared-prefs (auth.client.obj): ' + exc.toString(), additional: 'Data:\n${clientjson}\nQDate:\n${clientqdate}', trace: trace);
_client = null;
}
}
notifyListeners(); notifyListeners();
} }
@ -94,6 +128,10 @@ class AppAuth extends ChangeNotifier implements TokenSource {
await Globals().sharedPrefs.remove('auth.tokensend'); await Globals().sharedPrefs.remove('auth.tokensend');
await Globals().sharedPrefs.setString('auth.cdate', ""); await Globals().sharedPrefs.setString('auth.cdate', "");
await Globals().sharedPrefs.setString('auth.mdate', DateTime.now().toIso8601String()); await Globals().sharedPrefs.setString('auth.mdate', DateTime.now().toIso8601String());
await Globals().sharedPrefs.remove('auth.user.obj');
await Globals().sharedPrefs.remove('auth.user.qdate');
await Globals().sharedPrefs.remove('auth.client.obj');
await Globals().sharedPrefs.remove('auth.client.qdate');
} else { } else {
await Globals().sharedPrefs.setString('auth.userid', _userID!); await Globals().sharedPrefs.setString('auth.userid', _userID!);
await Globals().sharedPrefs.setString('auth.clientid', _clientID!); await Globals().sharedPrefs.setString('auth.clientid', _clientID!);
@ -101,14 +139,34 @@ class AppAuth extends ChangeNotifier implements TokenSource {
await Globals().sharedPrefs.setString('auth.tokensend', _tokenSend!); await Globals().sharedPrefs.setString('auth.tokensend', _tokenSend!);
if (Globals().sharedPrefs.getString('auth.cdate') == null) await Globals().sharedPrefs.setString('auth.cdate', DateTime.now().toIso8601String()); if (Globals().sharedPrefs.getString('auth.cdate') == null) await Globals().sharedPrefs.setString('auth.cdate', DateTime.now().toIso8601String());
await Globals().sharedPrefs.setString('auth.mdate', DateTime.now().toIso8601String()); await Globals().sharedPrefs.setString('auth.mdate', DateTime.now().toIso8601String());
if (_user != null) {
await Globals().sharedPrefs.setString('auth.user.obj', jsonEncode(_user!.$1.toJson()));
await Globals().sharedPrefs.setString('auth.user.qdate', _user!.$2.toIso8601String());
} else {
await Globals().sharedPrefs.remove('auth.user.obj');
await Globals().sharedPrefs.remove('auth.user.qdate');
}
if (_client != null) {
await Globals().sharedPrefs.setString('auth.client.obj', jsonEncode(_client!.$1.toJson()));
await Globals().sharedPrefs.setString('auth.client.qdate', _client!.$2.toIso8601String());
} else {
await Globals().sharedPrefs.remove('auth.client.obj');
await Globals().sharedPrefs.remove('auth.client.qdate');
}
} }
Globals().sharedPrefs.setString('auth.mdate', DateTime.now().toIso8601String()); Globals().sharedPrefs.setString('auth.mdate', DateTime.now().toIso8601String());
} }
Future<User> loadUser({bool force = false}) async { Future<User> loadUser({bool force = false, Duration? forceIfOlder = null}) async {
if (!force && _user != null && _user!.userID == _userID) { if (forceIfOlder != null && _user != null && _user!.$2.difference(DateTime.now()) > forceIfOlder) {
return _user!; force = true;
}
if (!force && _user != null && _user!.$1.userID == _userID) {
return _user!.$1;
} }
if (_userID == null || _tokenAdmin == null) { if (_userID == null || _tokenAdmin == null) {
@ -117,7 +175,7 @@ class AppAuth extends ChangeNotifier implements TokenSource {
final user = await APIClient.getUser(this, _userID!); final user = await APIClient.getUser(this, _userID!);
_user = user; _user = (user, DateTime.now());
await save(); await save();
@ -125,12 +183,12 @@ class AppAuth extends ChangeNotifier implements TokenSource {
} }
Future<Client?> loadClient({bool force = false, Duration? forceIfOlder = null}) async { Future<Client?> loadClient({bool force = false, Duration? forceIfOlder = null}) async {
if (forceIfOlder != null && _clientQueryTime != null && _clientQueryTime!.difference(DateTime.now()) > forceIfOlder) { if (forceIfOlder != null && _client != null && _client!.$2.difference(DateTime.now()) > forceIfOlder) {
force = true; force = true;
} }
if (!force && _client != null && _client!.clientID == _clientID) { if (!force && _client != null && _client!.$1.clientID == _clientID) {
return _client!; return _client!.$1;
} }
if (_clientID == null || _tokenAdmin == null) { if (_clientID == null || _tokenAdmin == null) {
@ -140,7 +198,7 @@ class AppAuth extends ChangeNotifier implements TokenSource {
try { try {
final client = await APIClient.getClient(this, _clientID!); final client = await APIClient.getClient(this, _clientID!);
_client = client; _client = (client, DateTime.now());
await save(); await save();

View File

@ -12,9 +12,18 @@ class AppBarState extends ChangeNotifier {
bool _loadingIndeterminate = false; bool _loadingIndeterminate = false;
bool get loadingIndeterminate => _loadingIndeterminate; bool get loadingIndeterminate => _loadingIndeterminate;
bool _showSearchField = false;
bool get showSearchField => _showSearchField;
void setLoadingIndeterminate(bool v) { void setLoadingIndeterminate(bool v) {
if (_loadingIndeterminate == v) return; if (_loadingIndeterminate == v) return;
_loadingIndeterminate = v; _loadingIndeterminate = v;
notifyListeners(); notifyListeners();
} }
void setShowSearchField(bool v) {
if (_showSearchField == v) return;
_showSearchField = v;
notifyListeners();
}
} }

View File

@ -0,0 +1,47 @@
import 'package:simplecloudnotifier/models/scn_message.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
class AppEvents {
static AppEvents? _singleton = AppEvents._internal();
factory AppEvents() {
return _singleton ?? (_singleton = AppEvents._internal());
}
AppEvents._internal() {}
List<void Function(String)> _searchListeners = [];
List<void Function(SCNMessage)> _messageReceivedListeners = [];
void subscribeSearchListener(void Function(String) listener) {
_searchListeners.add(listener);
}
void unsubscribeSearchListener(void Function(String) listener) {
_searchListeners.remove(listener);
}
void notifySearchListeners(String query) {
ApplicationLog.debug('[AppEvents] onSearch: $query');
for (var listener in _searchListeners) {
listener(query);
}
}
void subscribeMessageReceivedListener(void Function(SCNMessage) listener) {
_messageReceivedListeners.add(listener);
}
void unsubscribeMessageReceivedListener(void Function(SCNMessage) listener) {
_messageReceivedListeners.remove(listener);
}
void notifyMessageReceivedListeners(SCNMessage msg) {
ApplicationLog.debug('[AppEvents] onMessageReceived: ${msg.messageID}');
for (var listener in _messageReceivedListeners) {
listener(msg);
}
}
}

View File

@ -10,6 +10,7 @@ class ApplicationLog {
static void debug(String message, {String? additional, StackTrace? trace}) { static void debug(String message, {String? additional, StackTrace? trace}) {
(additional != null && additional != '') ? print('[DEBUG] ${message}: ${additional}') : print('[DEBUG] ${message}'); (additional != null && additional != '') ? print('[DEBUG] ${message}: ${additional}') : print('[DEBUG] ${message}');
if (!Hive.isBoxOpen('scn-logs')) return;
Hive.box<SCNLog>('scn-logs').add(SCNLog( Hive.box<SCNLog>('scn-logs').add(SCNLog(
id: Xid().toString(), id: Xid().toString(),
timestamp: DateTime.now(), timestamp: DateTime.now(),
@ -23,6 +24,7 @@ class ApplicationLog {
static void info(String message, {String? additional, StackTrace? trace}) { static void info(String message, {String? additional, StackTrace? trace}) {
(additional != null && additional != '') ? print('[INFO] ${message}: ${additional}') : print('[INFO] ${message}'); (additional != null && additional != '') ? print('[INFO] ${message}: ${additional}') : print('[INFO] ${message}');
if (!Hive.isBoxOpen('scn-logs')) return;
Hive.box<SCNLog>('scn-logs').add(SCNLog( Hive.box<SCNLog>('scn-logs').add(SCNLog(
id: Xid().toString(), id: Xid().toString(),
timestamp: DateTime.now(), timestamp: DateTime.now(),
@ -36,6 +38,7 @@ class ApplicationLog {
static void warn(String message, {String? additional, StackTrace? trace}) { static void warn(String message, {String? additional, StackTrace? trace}) {
(additional != null && additional != '') ? print('[WARN] ${message}: ${additional}') : print('[WARN] ${message}'); (additional != null && additional != '') ? print('[WARN] ${message}: ${additional}') : print('[WARN] ${message}');
if (!Hive.isBoxOpen('scn-logs')) return;
Hive.box<SCNLog>('scn-logs').add(SCNLog( Hive.box<SCNLog>('scn-logs').add(SCNLog(
id: Xid().toString(), id: Xid().toString(),
timestamp: DateTime.now(), timestamp: DateTime.now(),
@ -49,6 +52,7 @@ class ApplicationLog {
static void error(String message, {String? additional, StackTrace? trace}) { static void error(String message, {String? additional, StackTrace? trace}) {
(additional != null && additional != '') ? print('[ERROR] ${message}: ${additional}') : print('[ERROR] ${message}'); (additional != null && additional != '') ? print('[ERROR] ${message}: ${additional}') : print('[ERROR] ${message}');
if (!Hive.isBoxOpen('scn-logs')) return;
Hive.box<SCNLog>('scn-logs').add(SCNLog( Hive.box<SCNLog>('scn-logs').add(SCNLog(
id: Xid().toString(), id: Xid().toString(),
timestamp: DateTime.now(), timestamp: DateTime.now(),
@ -62,6 +66,7 @@ class ApplicationLog {
static void fatal(String message, {String? additional, StackTrace? trace}) { static void fatal(String message, {String? additional, StackTrace? trace}) {
(additional != null && additional != '') ? print('[FATAL] ${message}: ${additional}') : print('[FATAL] ${message}'); (additional != null && additional != '') ? print('[FATAL] ${message}: ${additional}') : print('[FATAL] ${message}');
if (!Hive.isBoxOpen('scn-logs')) return;
Hive.box<SCNLog>('scn-logs').add(SCNLog( Hive.box<SCNLog>('scn-logs').add(SCNLog(
id: Xid().toString(), id: Xid().toString(),
timestamp: DateTime.now(), timestamp: DateTime.now(),

View File

@ -32,8 +32,6 @@ class FBMessage extends HiveObject implements FieldDebuggable {
final String? messageType; final String? messageType;
@HiveField(8) @HiveField(8)
final bool mutableContent; final bool mutableContent;
@HiveField(9)
final RemoteNotification? notification;
@HiveField(10) @HiveField(10)
final DateTime? sentTime; final DateTime? sentTime;
@HiveField(11) @HiveField(11)
@ -54,7 +52,7 @@ class FBMessage extends HiveObject implements FieldDebuggable {
@HiveField(25) @HiveField(25)
final String? notificationAndroidLink; final String? notificationAndroidLink;
@HiveField(26) @HiveField(26)
final AndroidNotificationPriority? notificationAndroidPriority; final String? notificationAndroidPriority;
@HiveField(27) @HiveField(27)
final String? notificationAndroidSmallIcon; final String? notificationAndroidSmallIcon;
@HiveField(28) @HiveField(28)
@ -62,14 +60,14 @@ class FBMessage extends HiveObject implements FieldDebuggable {
@HiveField(29) @HiveField(29)
final String? notificationAndroidTicker; final String? notificationAndroidTicker;
@HiveField(30) @HiveField(30)
final AndroidNotificationVisibility? notificationAndroidVisibility; final String? notificationAndroidVisibility;
@HiveField(31) @HiveField(31)
final String? notificationAndroidTag; final String? notificationAndroidTag;
@HiveField(40) @HiveField(40)
final String? notificationAppleBadge; final String? notificationAppleBadge;
@HiveField(41) @HiveField(41)
final AppleNotificationSound? notificationAppleSound; final String? notificationAppleSound;
@HiveField(42) @HiveField(42)
final String? notificationAppleImageUrl; final String? notificationAppleImageUrl;
@HiveField(43) @HiveField(43)
@ -109,7 +107,6 @@ class FBMessage extends HiveObject implements FieldDebuggable {
required this.messageId, required this.messageId,
required this.messageType, required this.messageType,
required this.mutableContent, required this.mutableContent,
required this.notification,
required this.sentTime, required this.sentTime,
required this.threadId, required this.threadId,
required this.ttl, required this.ttl,
@ -152,7 +149,6 @@ class FBMessage extends HiveObject implements FieldDebuggable {
this.messageId = rmsg.messageId, this.messageId = rmsg.messageId,
this.messageType = rmsg.messageType, this.messageType = rmsg.messageType,
this.mutableContent = rmsg.mutableContent, this.mutableContent = rmsg.mutableContent,
this.notification = rmsg.notification,
this.sentTime = rmsg.sentTime, this.sentTime = rmsg.sentTime,
this.threadId = rmsg.threadId, this.threadId = rmsg.threadId,
this.ttl = rmsg.ttl, this.ttl = rmsg.ttl,
@ -162,14 +158,14 @@ class FBMessage extends HiveObject implements FieldDebuggable {
this.notificationAndroidCount = rmsg.notification?.android?.count, this.notificationAndroidCount = rmsg.notification?.android?.count,
this.notificationAndroidImageUrl = rmsg.notification?.android?.imageUrl, this.notificationAndroidImageUrl = rmsg.notification?.android?.imageUrl,
this.notificationAndroidLink = rmsg.notification?.android?.link, this.notificationAndroidLink = rmsg.notification?.android?.link,
this.notificationAndroidPriority = rmsg.notification?.android?.priority, this.notificationAndroidPriority = rmsg.notification?.android?.priority.toString(),
this.notificationAndroidSmallIcon = rmsg.notification?.android?.smallIcon, this.notificationAndroidSmallIcon = rmsg.notification?.android?.smallIcon,
this.notificationAndroidSound = rmsg.notification?.android?.sound, this.notificationAndroidSound = rmsg.notification?.android?.sound,
this.notificationAndroidTicker = rmsg.notification?.android?.ticker, this.notificationAndroidTicker = rmsg.notification?.android?.ticker,
this.notificationAndroidVisibility = rmsg.notification?.android?.visibility, this.notificationAndroidVisibility = rmsg.notification?.android?.visibility.toString(),
this.notificationAndroidTag = rmsg.notification?.android?.tag, this.notificationAndroidTag = rmsg.notification?.android?.tag,
this.notificationAppleBadge = rmsg.notification?.apple?.badge, this.notificationAppleBadge = rmsg.notification?.apple?.badge,
this.notificationAppleSound = rmsg.notification?.apple?.sound, this.notificationAppleSound = rmsg.notification?.apple?.sound?.toString(),
this.notificationAppleImageUrl = rmsg.notification?.apple?.imageUrl, this.notificationAppleImageUrl = rmsg.notification?.apple?.imageUrl,
this.notificationAppleSubtitle = rmsg.notification?.apple?.subtitle, this.notificationAppleSubtitle = rmsg.notification?.apple?.subtitle,
this.notificationAppleSubtitleLocArgs = rmsg.notification?.apple?.subtitleLocArgs, this.notificationAppleSubtitleLocArgs = rmsg.notification?.apple?.subtitleLocArgs,
@ -195,12 +191,11 @@ class FBMessage extends HiveObject implements FieldDebuggable {
('category', this.category ?? ''), ('category', this.category ?? ''),
('collapseKey', this.collapseKey ?? ''), ('collapseKey', this.collapseKey ?? ''),
('contentAvailable', this.contentAvailable.toString()), ('contentAvailable', this.contentAvailable.toString()),
('data', this.data.toString()), ('data', this.data.entries.map((e) => '${e.key} := ${e.value}').join('\n')),
('from', this.from ?? ''), ('from', this.from ?? ''),
('messageId', this.messageId ?? ''), ('messageId', this.messageId ?? ''),
('messageType', this.messageType ?? ''), ('messageType', this.messageType ?? ''),
('mutableContent', this.mutableContent.toString()), ('mutableContent', this.mutableContent.toString()),
('notification', this.notification?.toString() ?? ''),
('sentTime', this.sentTime?.toString() ?? ''), ('sentTime', this.sentTime?.toString() ?? ''),
('threadId', this.threadId ?? ''), ('threadId', this.threadId ?? ''),
('ttl', this.ttl?.toString() ?? ''), ('ttl', this.ttl?.toString() ?? ''),

View File

@ -26,7 +26,6 @@ class FBMessageAdapter extends TypeAdapter<FBMessage> {
messageId: fields[6] as String?, messageId: fields[6] as String?,
messageType: fields[7] as String?, messageType: fields[7] as String?,
mutableContent: fields[8] as bool, mutableContent: fields[8] as bool,
notification: fields[9] as RemoteNotification?,
sentTime: fields[10] as DateTime?, sentTime: fields[10] as DateTime?,
threadId: fields[11] as String?, threadId: fields[11] as String?,
ttl: fields[12] as int?, ttl: fields[12] as int?,
@ -36,15 +35,14 @@ class FBMessageAdapter extends TypeAdapter<FBMessage> {
notificationAndroidCount: fields[23] as int?, notificationAndroidCount: fields[23] as int?,
notificationAndroidImageUrl: fields[24] as String?, notificationAndroidImageUrl: fields[24] as String?,
notificationAndroidLink: fields[25] as String?, notificationAndroidLink: fields[25] as String?,
notificationAndroidPriority: fields[26] as AndroidNotificationPriority?, notificationAndroidPriority: fields[26] as String?,
notificationAndroidSmallIcon: fields[27] as String?, notificationAndroidSmallIcon: fields[27] as String?,
notificationAndroidSound: fields[28] as String?, notificationAndroidSound: fields[28] as String?,
notificationAndroidTicker: fields[29] as String?, notificationAndroidTicker: fields[29] as String?,
notificationAndroidVisibility: notificationAndroidVisibility: fields[30] as String?,
fields[30] as AndroidNotificationVisibility?,
notificationAndroidTag: fields[31] as String?, notificationAndroidTag: fields[31] as String?,
notificationAppleBadge: fields[40] as String?, notificationAppleBadge: fields[40] as String?,
notificationAppleSound: fields[41] as AppleNotificationSound?, notificationAppleSound: fields[41] as String?,
notificationAppleImageUrl: fields[42] as String?, notificationAppleImageUrl: fields[42] as String?,
notificationAppleSubtitle: fields[43] as String?, notificationAppleSubtitle: fields[43] as String?,
notificationAppleSubtitleLocArgs: (fields[44] as List?)?.cast<String>(), notificationAppleSubtitleLocArgs: (fields[44] as List?)?.cast<String>(),
@ -64,7 +62,7 @@ class FBMessageAdapter extends TypeAdapter<FBMessage> {
@override @override
void write(BinaryWriter writer, FBMessage obj) { void write(BinaryWriter writer, FBMessage obj) {
writer writer
..writeByte(40) ..writeByte(39)
..writeByte(0) ..writeByte(0)
..write(obj.senderId) ..write(obj.senderId)
..writeByte(1) ..writeByte(1)
@ -83,8 +81,6 @@ class FBMessageAdapter extends TypeAdapter<FBMessage> {
..write(obj.messageType) ..write(obj.messageType)
..writeByte(8) ..writeByte(8)
..write(obj.mutableContent) ..write(obj.mutableContent)
..writeByte(9)
..write(obj.notification)
..writeByte(10) ..writeByte(10)
..write(obj.sentTime) ..write(obj.sentTime)
..writeByte(11) ..writeByte(11)

View File

@ -13,6 +13,8 @@ class Globals {
Globals._internal(); Globals._internal();
bool _initialized = false;
String appName = ''; String appName = '';
String packageName = ''; String packageName = '';
String version = ''; String version = '';
@ -24,7 +26,11 @@ class Globals {
late SharedPreferences sharedPrefs; late SharedPreferences sharedPrefs;
bool get isInitialized => _initialized;
Future<void> init() async { Future<void> init() async {
if (_initialized) return;
PackageInfo packageInfo = await PackageInfo.fromPlatform(); PackageInfo packageInfo = await PackageInfo.fromPlatform();
this.appName = packageInfo.appName; this.appName = packageInfo.appName;
@ -54,6 +60,8 @@ class Globals {
} }
this.sharedPrefs = await SharedPreferences.getInstance(); this.sharedPrefs = await SharedPreferences.getInstance();
this._initialized = true;
} }
String? getPrefFCMToken() { String? getPrefFCMToken() {

View File

@ -0,0 +1,60 @@
import 'package:hive_flutter/hive_flutter.dart';
import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/models/scn_message.dart';
import 'package:simplecloudnotifier/settings/app_settings.dart';
class SCNDataCache {
SCNDataCache._internal();
static final SCNDataCache _instance = SCNDataCache._internal();
factory SCNDataCache() => _instance;
Future<void> addToMessageCache(List<SCNMessage> newItems) async {
final cfg = AppSettings();
final cache = Hive.box<SCNMessage>('scn-message-cache');
for (var msg in newItems) await cache.put(msg.messageID, msg);
// delete all but the newest 128 messages
if (cache.length < cfg.messagePageSize) return;
final allValues = cache.values.toList();
allValues.sort((a, b) => -1 * a.timestamp.compareTo(b.timestamp));
for (var val in allValues.sublist(cfg.messagePageSize)) {
await cache.delete(val.messageID);
}
}
Future<void> setChannelCache(List<ChannelWithSubscription> channels) async {
final cache = Hive.box<Channel>('scn-channel-cache');
if (cache.length != channels.length) await cache.clear();
for (var chn in channels) await cache.put(chn.channel.channelID, chn.channel);
}
bool hasMessagesAndChannels() {
final chnCache = Hive.box<Channel>('scn-channel-cache');
final msgCache = Hive.box<SCNMessage>('scn-message-cache');
return chnCache.isNotEmpty && msgCache.isNotEmpty;
}
Map<String, Channel> getChannelMap() {
final chnCache = Hive.box<Channel>('scn-channel-cache');
return <String, Channel>{for (var v in chnCache.values) v.channelID: v};
}
List<SCNMessage> getMessagesSorted() {
final msgCache = Hive.box<SCNMessage>('scn-message-cache');
final cacheMessages = msgCache.values.toList();
cacheMessages.sort((a, b) => -1 * a.timestamp.compareTo(b.timestamp));
return cacheMessages;
}
}

View File

@ -8,15 +8,21 @@ class Navi {
static void push<T extends Widget>(BuildContext context, T Function() builder) { static void push<T extends Widget>(BuildContext context, T Function() builder) {
Provider.of<AppBarState>(context, listen: false).setLoadingIndeterminate(false); Provider.of<AppBarState>(context, listen: false).setLoadingIndeterminate(false);
Provider.of<AppBarState>(context, listen: false).setShowSearchField(false);
Navigator.push(context, MaterialPageRoute<T>(builder: (context) => builder())); Navigator.push(context, MaterialPageRoute<T>(builder: (context) => builder()));
} }
static void popToRoot(BuildContext context) { static void popToRoot(BuildContext context) {
Provider.of<AppBarState>(context, listen: false).setLoadingIndeterminate(false); Provider.of<AppBarState>(context, listen: false).setLoadingIndeterminate(false);
Provider.of<AppBarState>(context, listen: false).setShowSearchField(false);
Navigator.popUntil(context, (route) => route.isFirst); Navigator.popUntil(context, (route) => route.isFirst);
} }
static void popDialog(BuildContext dialogContext) {
Navigator.pop(dialogContext);
}
} }
class SCNRouteObserver extends RouteObserver<PageRoute<dynamic>> { class SCNRouteObserver extends RouteObserver<PageRoute<dynamic>> {
@ -25,6 +31,7 @@ class SCNRouteObserver extends RouteObserver<PageRoute<dynamic>> {
super.didPush(route, previousRoute); super.didPush(route, previousRoute);
if (route is PageRoute) { if (route is PageRoute) {
AppBarState().setLoadingIndeterminate(false); AppBarState().setLoadingIndeterminate(false);
AppBarState().setShowSearchField(false);
print('[SCNRouteObserver] .didPush()'); print('[SCNRouteObserver] .didPush()');
} }
@ -35,6 +42,7 @@ class SCNRouteObserver extends RouteObserver<PageRoute<dynamic>> {
super.didReplace(newRoute: newRoute, oldRoute: oldRoute); super.didReplace(newRoute: newRoute, oldRoute: oldRoute);
if (newRoute is PageRoute) { if (newRoute is PageRoute) {
AppBarState().setLoadingIndeterminate(false); AppBarState().setLoadingIndeterminate(false);
AppBarState().setShowSearchField(false);
print('[SCNRouteObserver] .didReplace()'); print('[SCNRouteObserver] .didReplace()');
} }
@ -45,6 +53,7 @@ class SCNRouteObserver extends RouteObserver<PageRoute<dynamic>> {
super.didPop(route, previousRoute); super.didPop(route, previousRoute);
if (previousRoute is PageRoute && route is PageRoute) { if (previousRoute is PageRoute && route is PageRoute) {
AppBarState().setLoadingIndeterminate(false); AppBarState().setLoadingIndeterminate(false);
AppBarState().setShowSearchField(false);
print('[SCNRouteObserver] .didPop()'); print('[SCNRouteObserver] .didPop()');
} }

View File

@ -0,0 +1,70 @@
import 'dart:io';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:simplecloudnotifier/settings/app_settings.dart';
import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/globals.dart';
class Notifier {
static void showLocalNotification(String channelID, String channelName, String channelDescr, String title, String body, DateTime? timestamp) async {
final nid = Globals().sharedPrefs.getInt('notifier.nextid') ?? 1000;
Globals().sharedPrefs.setInt('notifier.nextid', nid + 7);
final existingSummaryNID = Globals().sharedPrefs.getInt('notifier.summary.$channelID');
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
if (Platform.isAndroid && AppSettings().groupNotifications) {
final activeNotifications = (await flutterLocalNotificationsPlugin.getActiveNotifications()).where((p) => p.groupKey == channelID).toList();
final summaryNotification = activeNotifications.where((p) => p.id == existingSummaryNID).toList();
ApplicationLog.debug('found ${activeNotifications.length} active notifications in this group (${summaryNotification.length} summary notifications for channel ${channelID} with nid [${existingSummaryNID}])');
if (activeNotifications.isNotEmpty && !activeNotifications.any((p) => p.id == existingSummaryNID)) {
// ======== SHOW SUMMARY/GROUPING NOTIFICATION ========
final newSummaryNID = nid + 1;
ApplicationLog.debug('Create new summary notifications for channel ${channelID} with nid [${newSummaryNID}])');
Globals().sharedPrefs.setInt('notifier.summary.$channelID', newSummaryNID);
await flutterLocalNotificationsPlugin.show(
newSummaryNID,
channelName,
"(multiple notifications)",
NotificationDetails(
android: AndroidNotificationDetails(
channelID,
channelName,
importance: Importance.max,
priority: Priority.high,
groupKey: channelID,
setAsGroupSummary: true,
subText: (channelName == 'main') ? null : channelName,
),
),
);
}
}
final newMessageNID = nid + 2;
ApplicationLog.debug('Create new local notifications for message in channel ${channelID} with nid [${newMessageNID}])');
// ======== SHOW NOTIFICATION ========
await flutterLocalNotificationsPlugin.show(
newMessageNID,
title,
body,
NotificationDetails(
android: AndroidNotificationDetails(
channelID,
channelName,
channelDescription: channelDescr,
importance: Importance.max,
priority: Priority.high,
when: timestamp?.millisecondsSinceEpoch,
groupKey: channelID,
subText: (channelName == 'main') ? null : channelName,
),
),
);
}
}

View File

@ -8,6 +8,7 @@ import Foundation
import device_info_plus import device_info_plus
import firebase_core import firebase_core
import firebase_messaging import firebase_messaging
import flutter_local_notifications
import package_info_plus import package_info_plus
import path_provider_foundation import path_provider_foundation
import share_plus import share_plus
@ -18,6 +19,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))

View File

@ -209,6 +209,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.6" version: "2.3.6"
dbus:
dependency: transitive
description:
name: dbus
sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac"
url: "https://pub.dev"
source: hosted
version: "0.7.10"
device_info_plus: device_info_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@ -342,6 +350,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.0" version: "4.0.0"
flutter_local_notifications:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: "40e6fbd2da7dcc7ed78432c5cdab1559674b4af035fddbfb2f9a8f9c2112fcef"
url: "https://pub.dev"
source: hosted
version: "17.1.2"
flutter_local_notifications_linux:
dependency: transitive
description:
name: flutter_local_notifications_linux
sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03"
url: "https://pub.dev"
source: hosted
version: "4.0.0+1"
flutter_local_notifications_platform_interface:
dependency: transitive
description:
name: flutter_local_notifications_platform_interface
sha256: "340abf67df238f7f0ef58f4a26d2a83e1ab74c77ab03cd2b2d5018ac64db30b7"
url: "https://pub.dev"
source: hosted
version: "7.1.0"
flutter_staggered_grid_view: flutter_staggered_grid_view:
dependency: transitive dependency: transitive
description: description:
@ -916,6 +948,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.1" version: "0.6.1"
timezone:
dependency: transitive
description:
name: timezone
sha256: a6ccda4a69a442098b602c44e61a1e2b4bf6f5516e875bbf0f427d5df14745d5
url: "https://pub.dev"
source: hosted
version: "0.9.3"
timing: timing:
dependency: transitive dependency: transitive
description: description:

View File

@ -33,6 +33,7 @@ dependencies:
toastification: ^2.0.0 toastification: ^2.0.0
uuid: ^4.4.0 uuid: ^4.4.0
share_plus: ^9.0.0 share_plus: ^9.0.0
flutter_local_notifications: ^17.1.2
dependency_overrides: dependency_overrides: