Compare commits
6 Commits
master
...
flutter_ap
Author | SHA1 | Date | |
---|---|---|---|
59d28d3c49 | |||
600f3365f6 | |||
5b8a1e86e0 | |||
c8bc7665f7 | |||
0bbe5fc7fa | |||
e9ea573e33 |
|
@ -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
27
flutter/android/app/proguard-rules.pro
vendored
Normal 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
|
|
@ -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 |
|
@ -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 |
2
flutter/android/app/src/main/res/raw/keep.xml
Normal file
2
flutter/android/app/src/main/res/raw/keep.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:tools="http://schemas.android.com/tools" tools:keep="@drawable/*" />
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
86
flutter/lib/components/layout/app_bar_filter_dialog.dart
Normal file
86
flutter/lib/components/layout/app_bar_filter_dialog.dart
Normal 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...
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 ?? () {},
|
||||||
|
|
|
@ -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 ?!?
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
|
@ -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;
|
||||||
}
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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',
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -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")),
|
||||||
);
|
);
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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),
|
||||||
|
|
36
flutter/lib/pages/message_list/message_filter_chiplet.dart
Normal file
36
flutter/lib/pages/message_list/message_filter_chiplet.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
34
flutter/lib/settings/app_settings.dart
Normal file
34
flutter/lib/settings/app_settings.dart
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
47
flutter/lib/state/app_events.dart
Normal file
47
flutter/lib/state/app_events.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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(),
|
||||||
|
|
|
@ -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() ?? ''),
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
60
flutter/lib/state/scn_data_cache.dart
Normal file
60
flutter/lib/state/scn_data_cache.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()');
|
||||||
}
|
}
|
||||||
|
|
70
flutter/lib/utils/notifier.dart
Normal file
70
flutter/lib/utils/notifier.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"))
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue
Block a user