Merge branch 'flutter_app'
@ -1,7 +1,7 @@
|
||||
|
||||
|
||||
run:
|
||||
dart run build_runner build
|
||||
flutter pub run build_runner build
|
||||
flutter run
|
||||
|
||||
test:
|
||||
@ -11,8 +11,11 @@ fix:
|
||||
dart fix --apply
|
||||
|
||||
gen:
|
||||
dart run build_runner build
|
||||
flutter pub run build_runner build
|
||||
|
||||
autoreload:
|
||||
@# run `make run` in another terminal (or another variant of flutter run)
|
||||
@_utils/autoreload.sh
|
||||
|
||||
icons:
|
||||
flutter pub run flutter_launcher_icons -f "flutter_launcher_icons.yaml"
|
@ -9,7 +9,7 @@
|
||||
<application
|
||||
android:label="simplecloudnotifier"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
android:icon="@mipmap/launcher_icon">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
BIN
flutter/android/app/src/main/res/mipmap-hdpi/launcher_icon.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
flutter/android/app/src/main/res/mipmap-mdpi/launcher_icon.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
flutter/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png
Normal file
After Width: | Height: | Size: 7.5 KiB |
BIN
flutter/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png
Normal file
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 19 KiB |
19
flutter/flutter_launcher_icons.yaml
Normal file
@ -0,0 +1,19 @@
|
||||
flutter_launcher_icons:
|
||||
android: "launcher_icon"
|
||||
ios: true
|
||||
remove_alpha_ios: true
|
||||
image_path: "../data/icon_512_nobox.png"
|
||||
min_sdk_android: 21 # android min sdk min:16, default 21
|
||||
adaptive_icon_monochrome: "../data/icon_512_transparent.png"
|
||||
web:
|
||||
generate: true
|
||||
image_path: "../data/icon_512_nobox.png"
|
||||
background_color: "#hexcode"
|
||||
theme_color: "#hexcode"
|
||||
windows:
|
||||
generate: true
|
||||
image_path: "../data/icon_512_nobox.png"
|
||||
icon_size: 48 # min:48, max:256, default: 48
|
||||
macos:
|
||||
generate: true
|
||||
image_path: "../data/icon_512_nobox.png"
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 88 KiB |
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 926 B |
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 5.9 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 10 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 9.4 KiB |
@ -127,6 +127,16 @@ class APIClient {
|
||||
);
|
||||
}
|
||||
|
||||
static Future<UserPreview> getUserPreview(TokenSource auth, String uid) async {
|
||||
return await _request(
|
||||
name: 'getUserPreview',
|
||||
method: 'GET',
|
||||
relURL: 'preview/users/$uid',
|
||||
fn: UserPreview.fromJson,
|
||||
authToken: auth.getToken(),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<Client> addClient(TokenSource auth, String fcmToken, String agentModel, String agentVersion, String? name, String clientType) async {
|
||||
return await _request(
|
||||
name: 'addClient',
|
||||
@ -191,6 +201,16 @@ class APIClient {
|
||||
);
|
||||
}
|
||||
|
||||
static Future<ChannelPreview> getChannelPreview(TokenSource auth, String cid) async {
|
||||
return await _request(
|
||||
name: 'getChannelPreview',
|
||||
method: 'GET',
|
||||
relURL: 'preview/channels/${cid}',
|
||||
fn: ChannelPreview.fromJson,
|
||||
authToken: auth.getToken(),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<(String, List<Message>)> getMessageList(TokenSource auth, String pageToken, {int? pageSize, List<String>? channelIDs}) async {
|
||||
return await _request(
|
||||
name: 'getMessageList',
|
||||
@ -275,6 +295,16 @@ class APIClient {
|
||||
);
|
||||
}
|
||||
|
||||
static Future<KeyTokenPreview> getKeyTokenPreview(TokenSource auth, String kid) async {
|
||||
return await _request(
|
||||
name: 'getKeyTokenPreview',
|
||||
method: 'GET',
|
||||
relURL: 'preview/keys/$kid',
|
||||
fn: KeyTokenPreview.fromJson,
|
||||
authToken: auth.getToken(),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<KeyToken> getKeyTokenByToken(String userid, String token) async {
|
||||
return await _request(
|
||||
name: 'getCurrentKeyToken',
|
||||
|
@ -1,8 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/components/layout/app_bar_progress_indicator.dart';
|
||||
import 'package:simplecloudnotifier/pages/debug/debug_main.dart';
|
||||
import 'package:simplecloudnotifier/state/app_theme.dart';
|
||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||
|
||||
class SCNAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
const SCNAppBar({
|
||||
@ -26,6 +29,16 @@ class SCNAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
Widget build(BuildContext context) {
|
||||
var actions = <Widget>[];
|
||||
|
||||
if (showDebug) {
|
||||
actions.add(IconButton(
|
||||
icon: const Icon(FontAwesomeIcons.solidSpiderBlackWidow),
|
||||
tooltip: 'Debug',
|
||||
onPressed: () {
|
||||
Navi.push(context, () => DebugMainPage());
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
if (showThemeSwitch) {
|
||||
actions.add(Consumer<AppTheme>(
|
||||
builder: (context, appTheme, child) => IconButton(
|
||||
@ -35,19 +48,16 @@ class SCNAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
),
|
||||
));
|
||||
} else {
|
||||
actions.add(SizedBox.square(dimension: 40));
|
||||
}
|
||||
|
||||
if (showDebug) {
|
||||
actions.add(IconButton(
|
||||
icon: const Icon(FontAwesomeIcons.solidSpiderBlackWidow),
|
||||
tooltip: 'Debug',
|
||||
onPressed: () {
|
||||
Navigator.push(context, MaterialPageRoute<DebugMainPage>(builder: (context) => DebugMainPage()));
|
||||
},
|
||||
actions.add(Visibility(
|
||||
visible: false,
|
||||
maintainSize: true,
|
||||
maintainAnimation: true,
|
||||
maintainState: true,
|
||||
child: IconButton(
|
||||
icon: const Icon(FontAwesomeIcons.square),
|
||||
onPressed: () {/*TODO*/},
|
||||
),
|
||||
));
|
||||
} else {
|
||||
actions.add(SizedBox.square(dimension: 40));
|
||||
}
|
||||
|
||||
if (showSearch) {
|
||||
@ -63,13 +73,26 @@ class SCNAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
onPressed: onShare ?? () {},
|
||||
));
|
||||
} else {
|
||||
actions.add(SizedBox.square(dimension: 40));
|
||||
actions.add(Visibility(
|
||||
visible: false,
|
||||
maintainSize: true,
|
||||
maintainAnimation: true,
|
||||
maintainState: true,
|
||||
child: IconButton(
|
||||
icon: const Icon(FontAwesomeIcons.square),
|
||||
onPressed: () {/*TODO*/},
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
return AppBar(
|
||||
title: Text(title ?? 'Simple Cloud Notifier 2.0'),
|
||||
actions: actions,
|
||||
backgroundColor: Theme.of(context).secondaryHeaderColor,
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size(double.infinity, 1.0),
|
||||
child: AppBarProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,21 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
||||
|
||||
class AppBarProgressIndicator extends StatelessWidget implements PreferredSizeWidget {
|
||||
@override
|
||||
Size get preferredSize => Size(double.infinity, 1.0);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<AppBarState>(
|
||||
builder: (context, value, child) {
|
||||
if (value.loadingIndeterminate) {
|
||||
return LinearProgressIndicator(value: null);
|
||||
} else {
|
||||
return SizedBox.square(dimension: 4); // 4 height is the same as the LinearProgressIndicator
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -5,14 +5,19 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/models/channel.dart';
|
||||
import 'package:simplecloudnotifier/models/client.dart';
|
||||
import 'package:simplecloudnotifier/models/message.dart';
|
||||
import 'package:simplecloudnotifier/nav_layout.dart';
|
||||
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
||||
import 'package:simplecloudnotifier/state/app_theme.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/state/fb_message.dart';
|
||||
import 'package:simplecloudnotifier/state/globals.dart';
|
||||
import 'package:simplecloudnotifier/state/request_log.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||
import 'package:toastification/toastification.dart';
|
||||
import 'firebase_options.dart';
|
||||
|
||||
@ -34,6 +39,9 @@ void main() async {
|
||||
Hive.registerAdapter(SCNRequestAdapter());
|
||||
Hive.registerAdapter(SCNLogAdapter());
|
||||
Hive.registerAdapter(SCNLogLevelAdapter());
|
||||
Hive.registerAdapter(MessageAdapter());
|
||||
Hive.registerAdapter(ChannelAdapter());
|
||||
Hive.registerAdapter(FBMessageAdapter());
|
||||
|
||||
print('[INIT] Load Hive<scn-requests>...');
|
||||
|
||||
@ -55,6 +63,36 @@ void main() async {
|
||||
ApplicationLog.error('Failed to open Hive-Box: scn-logs: ' + exc.toString(), trace: trace);
|
||||
}
|
||||
|
||||
print('[INIT] Load Hive<scn-message-cache>...');
|
||||
|
||||
try {
|
||||
await Hive.openBox<Message>('scn-message-cache');
|
||||
} catch (exc, trace) {
|
||||
Hive.deleteBoxFromDisk('scn-message-cache');
|
||||
await Hive.openBox<Message>('scn-message-cache');
|
||||
ApplicationLog.error('Failed to open Hive-Box: scn-message-cache' + exc.toString(), trace: trace);
|
||||
}
|
||||
|
||||
print('[INIT] Load Hive<scn-channel-cache>...');
|
||||
|
||||
try {
|
||||
await Hive.openBox<Channel>('scn-channel-cache');
|
||||
} catch (exc, trace) {
|
||||
Hive.deleteBoxFromDisk('scn-channel-cache');
|
||||
await Hive.openBox<Channel>('scn-channel-cache');
|
||||
ApplicationLog.error('Failed to open Hive-Box: scn-channel-cache' + exc.toString(), trace: trace);
|
||||
}
|
||||
|
||||
print('[INIT] Load Hive<scn-fb-messages>...');
|
||||
|
||||
try {
|
||||
await Hive.openBox<FBMessage>('scn-fb-messages');
|
||||
} catch (exc, trace) {
|
||||
Hive.deleteBoxFromDisk('scn-fb-messages');
|
||||
await Hive.openBox<FBMessage>('scn-fb-messages');
|
||||
ApplicationLog.error('Failed to open Hive-Box: scn-fb-messages' + exc.toString(), trace: trace);
|
||||
}
|
||||
|
||||
print('[INIT] Load AppAuth...');
|
||||
|
||||
final appAuth = AppAuth(); // ensure UserAccount is loaded
|
||||
@ -102,6 +140,9 @@ void main() async {
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to get+set firebase token: ' + exc.toString(), trace: trace);
|
||||
}
|
||||
|
||||
FirebaseMessaging.onBackgroundMessage(_onBackgroundMessage);
|
||||
FirebaseMessaging.onMessage.listen(_onForegroundMessage);
|
||||
} else {
|
||||
print('[INIT] Skip Firebase init (Platform == Linux)...');
|
||||
}
|
||||
@ -113,12 +154,40 @@ void main() async {
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (context) => AppAuth(), lazy: false),
|
||||
ChangeNotifierProvider(create: (context) => AppTheme(), lazy: false),
|
||||
ChangeNotifierProvider(create: (context) => AppBarState(), lazy: false),
|
||||
],
|
||||
child: const SCNApp(),
|
||||
child: SCNApp(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class SCNApp extends StatelessWidget {
|
||||
SCNApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ToastificationWrapper(
|
||||
config: ToastificationConfig(
|
||||
itemWidth: 440,
|
||||
marginBuilder: (alignment) => EdgeInsets.symmetric(vertical: 64),
|
||||
animationDuration: Duration(milliseconds: 200),
|
||||
),
|
||||
child: Consumer<AppTheme>(
|
||||
builder: (context, appTheme, child) => MaterialApp(
|
||||
title: 'SimpleCloudNotifier',
|
||||
navigatorObservers: [Navi.routeObserver, Navi.modalRouteObserver],
|
||||
theme: ThemeData(
|
||||
//TODO color settings
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue, brightness: appTheme.darkMode ? Brightness.dark : Brightness.light),
|
||||
useMaterial3: true,
|
||||
),
|
||||
home: SCNNavLayout(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void setFirebaseToken(String fcmToken) async {
|
||||
final acc = AppAuth();
|
||||
|
||||
@ -132,7 +201,7 @@ void setFirebaseToken(String fcmToken) async {
|
||||
|
||||
Client? client;
|
||||
try {
|
||||
client = await acc.loadClient(force: true);
|
||||
client = await acc.loadClient(forceIfOlder: Duration(seconds: 60));
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to get client: ' + exc.toString(), trace: trace);
|
||||
return;
|
||||
@ -155,28 +224,18 @@ void setFirebaseToken(String fcmToken) async {
|
||||
}
|
||||
}
|
||||
|
||||
class SCNApp extends StatelessWidget {
|
||||
const SCNApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ToastificationWrapper(
|
||||
config: ToastificationConfig(
|
||||
itemWidth: 440,
|
||||
marginBuilder: (alignment) => EdgeInsets.symmetric(vertical: 64),
|
||||
animationDuration: Duration(milliseconds: 200),
|
||||
),
|
||||
child: Consumer<AppTheme>(
|
||||
builder: (context, appTheme, child) => MaterialApp(
|
||||
title: 'SimpleCloudNotifier',
|
||||
theme: ThemeData(
|
||||
//TODO color settings
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue, brightness: appTheme.darkMode ? Brightness.dark : Brightness.light),
|
||||
useMaterial3: true,
|
||||
),
|
||||
home: SCNNavLayout(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
Future<void> _onBackgroundMessage(RemoteMessage message) async {
|
||||
await _receiveMessage(message, false);
|
||||
}
|
||||
|
||||
void _onForegroundMessage(RemoteMessage message) {
|
||||
_receiveMessage(message, true);
|
||||
}
|
||||
|
||||
Future<void> _receiveMessage(RemoteMessage message, bool foreground) async {
|
||||
// ensure init
|
||||
Hive.openBox<SCNLog>('scn-logs');
|
||||
|
||||
ApplicationLog.info('Received FB message (${foreground ? 'foreground' : 'background'}): ${message.messageId ?? 'NULL'}');
|
||||
FBMessageLog.insert(message);
|
||||
}
|
||||
|
@ -1,17 +1,32 @@
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:simplecloudnotifier/models/subscription.dart';
|
||||
import 'package:simplecloudnotifier/state/interfaces.dart';
|
||||
|
||||
class Channel {
|
||||
part 'channel.g.dart';
|
||||
|
||||
@HiveType(typeId: 104)
|
||||
class Channel extends HiveObject implements FieldDebuggable {
|
||||
@HiveField(0)
|
||||
final String channelID;
|
||||
|
||||
@HiveField(10)
|
||||
final String ownerUserID;
|
||||
@HiveField(11)
|
||||
final String internalName;
|
||||
@HiveField(12)
|
||||
final String displayName;
|
||||
@HiveField(13)
|
||||
final String? descriptionName;
|
||||
@HiveField(14)
|
||||
final String? subscribeKey;
|
||||
@HiveField(15)
|
||||
final String timestampCreated;
|
||||
@HiveField(16)
|
||||
final String? timestampLastSent;
|
||||
@HiveField(17)
|
||||
final int messagesSent;
|
||||
|
||||
const Channel({
|
||||
Channel({
|
||||
required this.channelID,
|
||||
required this.ownerUserID,
|
||||
required this.internalName,
|
||||
@ -36,6 +51,25 @@ class Channel {
|
||||
messagesSent: json['messages_sent'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Channel[${this.channelID}]';
|
||||
}
|
||||
|
||||
List<(String, String)> debugFieldList() {
|
||||
return [
|
||||
('channelID', this.channelID),
|
||||
('ownerUserID', this.ownerUserID),
|
||||
('internalName', this.internalName),
|
||||
('displayName', this.displayName),
|
||||
('descriptionName', this.descriptionName ?? ''),
|
||||
('subscribeKey', this.subscribeKey ?? ''),
|
||||
('timestampCreated', this.timestampCreated),
|
||||
('timestampLastSent', this.timestampLastSent ?? ''),
|
||||
('messagesSent', '${this.messagesSent}'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelWithSubscription {
|
||||
@ -58,3 +92,29 @@ class ChannelWithSubscription {
|
||||
return jsonArr.map<ChannelWithSubscription>((e) => ChannelWithSubscription.fromJson(e as Map<String, dynamic>)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelPreview {
|
||||
final String channelID;
|
||||
final String ownerUserID;
|
||||
final String internalName;
|
||||
final String displayName;
|
||||
final String? descriptionName;
|
||||
|
||||
const ChannelPreview({
|
||||
required this.channelID,
|
||||
required this.ownerUserID,
|
||||
required this.internalName,
|
||||
required this.displayName,
|
||||
required this.descriptionName,
|
||||
});
|
||||
|
||||
factory ChannelPreview.fromJson(Map<String, dynamic> json) {
|
||||
return ChannelPreview(
|
||||
channelID: json['channel_id'] as String,
|
||||
ownerUserID: json['owner_user_id'] as String,
|
||||
internalName: json['internal_name'] as String,
|
||||
displayName: json['display_name'] as String,
|
||||
descriptionName: json['description_name'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
65
flutter/lib/models/channel.g.dart
Normal file
@ -0,0 +1,65 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'channel.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class ChannelAdapter extends TypeAdapter<Channel> {
|
||||
@override
|
||||
final int typeId = 104;
|
||||
|
||||
@override
|
||||
Channel read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return Channel(
|
||||
channelID: fields[0] as String,
|
||||
ownerUserID: fields[10] as String,
|
||||
internalName: fields[11] as String,
|
||||
displayName: fields[12] as String,
|
||||
descriptionName: fields[13] as String?,
|
||||
subscribeKey: fields[14] as String?,
|
||||
timestampCreated: fields[15] as String,
|
||||
timestampLastSent: fields[16] as String?,
|
||||
messagesSent: fields[17] as int,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, Channel obj) {
|
||||
writer
|
||||
..writeByte(9)
|
||||
..writeByte(0)
|
||||
..write(obj.channelID)
|
||||
..writeByte(10)
|
||||
..write(obj.ownerUserID)
|
||||
..writeByte(11)
|
||||
..write(obj.internalName)
|
||||
..writeByte(12)
|
||||
..write(obj.displayName)
|
||||
..writeByte(13)
|
||||
..write(obj.descriptionName)
|
||||
..writeByte(14)
|
||||
..write(obj.subscribeKey)
|
||||
..writeByte(15)
|
||||
..write(obj.timestampCreated)
|
||||
..writeByte(16)
|
||||
..write(obj.timestampLastSent)
|
||||
..writeByte(17)
|
||||
..write(obj.messagesSent);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ChannelAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
@ -39,3 +39,32 @@ class KeyToken {
|
||||
return jsonArr.map<KeyToken>((e) => KeyToken.fromJson(e as Map<String, dynamic>)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
class KeyTokenPreview {
|
||||
final String keytokenID;
|
||||
final String name;
|
||||
final String ownerUserID;
|
||||
final bool allChannels;
|
||||
final List<String> channels;
|
||||
final String permissions;
|
||||
|
||||
const KeyTokenPreview({
|
||||
required this.keytokenID,
|
||||
required this.name,
|
||||
required this.ownerUserID,
|
||||
required this.allChannels,
|
||||
required this.channels,
|
||||
required this.permissions,
|
||||
});
|
||||
|
||||
factory KeyTokenPreview.fromJson(Map<String, dynamic> json) {
|
||||
return KeyTokenPreview(
|
||||
keytokenID: json['keytoken_id'] as String,
|
||||
name: json['name'] as String,
|
||||
ownerUserID: json['owner_user_id'] as String,
|
||||
allChannels: json['all_channels'] as bool,
|
||||
channels: (json['channels'] as List<dynamic>).map((e) => e as String).toList(),
|
||||
permissions: json['permissions'] as String,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,39 @@
|
||||
class Message {
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:simplecloudnotifier/state/interfaces.dart';
|
||||
|
||||
part 'message.g.dart';
|
||||
|
||||
@HiveType(typeId: 105)
|
||||
class Message extends HiveObject implements FieldDebuggable {
|
||||
@HiveField(0)
|
||||
final String messageID;
|
||||
|
||||
@HiveField(10)
|
||||
final String senderUserID;
|
||||
@HiveField(11)
|
||||
final String channelInternalName;
|
||||
@HiveField(12)
|
||||
final String channelID;
|
||||
@HiveField(13)
|
||||
final String? senderName;
|
||||
@HiveField(14)
|
||||
final String senderIP;
|
||||
@HiveField(15)
|
||||
final String timestamp;
|
||||
@HiveField(16)
|
||||
final String title;
|
||||
@HiveField(17)
|
||||
final String? content;
|
||||
@HiveField(18)
|
||||
final int priority;
|
||||
@HiveField(19)
|
||||
final String? userMessageID;
|
||||
@HiveField(20)
|
||||
final String usedKeyID;
|
||||
@HiveField(21)
|
||||
final bool trimmed;
|
||||
|
||||
const Message({
|
||||
Message({
|
||||
required this.messageID,
|
||||
required this.senderUserID,
|
||||
required this.channelInternalName,
|
||||
@ -54,4 +74,27 @@ class Message {
|
||||
|
||||
return (npt, messages);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Message[${this.messageID}]';
|
||||
}
|
||||
|
||||
List<(String, String)> debugFieldList() {
|
||||
return [
|
||||
('messageID', this.messageID),
|
||||
('senderUserID', this.senderUserID),
|
||||
('channelInternalName', this.channelInternalName),
|
||||
('channelID', this.channelID),
|
||||
('senderName', this.senderName ?? ''),
|
||||
('senderIP', this.senderIP),
|
||||
('timestamp', this.timestamp),
|
||||
('title', this.title),
|
||||
('content', this.content ?? ''),
|
||||
('priority', '${this.priority}'),
|
||||
('userMessageID', this.userMessageID ?? ''),
|
||||
('usedKeyID', this.usedKeyID),
|
||||
('trimmed', '${this.trimmed}'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
77
flutter/lib/models/message.g.dart
Normal file
@ -0,0 +1,77 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'message.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class MessageAdapter extends TypeAdapter<Message> {
|
||||
@override
|
||||
final int typeId = 105;
|
||||
|
||||
@override
|
||||
Message read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return Message(
|
||||
messageID: fields[0] as String,
|
||||
senderUserID: fields[10] as String,
|
||||
channelInternalName: fields[11] as String,
|
||||
channelID: fields[12] as String,
|
||||
senderName: fields[13] as String?,
|
||||
senderIP: fields[14] as String,
|
||||
timestamp: fields[15] as String,
|
||||
title: fields[16] as String,
|
||||
content: fields[17] as String?,
|
||||
priority: fields[18] as int,
|
||||
userMessageID: fields[19] as String?,
|
||||
usedKeyID: fields[20] as String,
|
||||
trimmed: fields[21] as bool,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, Message obj) {
|
||||
writer
|
||||
..writeByte(13)
|
||||
..writeByte(0)
|
||||
..write(obj.messageID)
|
||||
..writeByte(10)
|
||||
..write(obj.senderUserID)
|
||||
..writeByte(11)
|
||||
..write(obj.channelInternalName)
|
||||
..writeByte(12)
|
||||
..write(obj.channelID)
|
||||
..writeByte(13)
|
||||
..write(obj.senderName)
|
||||
..writeByte(14)
|
||||
..write(obj.senderIP)
|
||||
..writeByte(15)
|
||||
..write(obj.timestamp)
|
||||
..writeByte(16)
|
||||
..write(obj.title)
|
||||
..writeByte(17)
|
||||
..write(obj.content)
|
||||
..writeByte(18)
|
||||
..write(obj.priority)
|
||||
..writeByte(19)
|
||||
..write(obj.userMessageID)
|
||||
..writeByte(20)
|
||||
..write(obj.usedKeyID)
|
||||
..writeByte(21)
|
||||
..write(obj.trimmed);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is MessageAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
@ -90,3 +90,20 @@ class UserWithClientsAndKeys {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UserPreview {
|
||||
final String userID;
|
||||
final String? username;
|
||||
|
||||
const UserPreview({
|
||||
required this.userID,
|
||||
required this.username,
|
||||
});
|
||||
|
||||
factory UserPreview.fromJson(Map<String, dynamic> json) {
|
||||
return UserPreview(
|
||||
userID: json['user_id'] as String,
|
||||
username: json['username'] as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -66,11 +66,11 @@ class _SCNNavLayoutState extends State<SCNNavLayout> {
|
||||
),
|
||||
body: IndexedStack(
|
||||
children: [
|
||||
ExcludeFocus(excluding: _selectedIndex != 0, child: MessageListPage()),
|
||||
ExcludeFocus(excluding: _selectedIndex != 1, child: ChannelRootPage()),
|
||||
ExcludeFocus(excluding: _selectedIndex != 2, child: AccountRootPage()),
|
||||
ExcludeFocus(excluding: _selectedIndex != 3, child: SettingsRootPage()),
|
||||
ExcludeFocus(excluding: _selectedIndex != 4, child: SendRootPage()),
|
||||
ExcludeFocus(excluding: _selectedIndex != 0, child: MessageListPage(isVisiblePage: _selectedIndex == 0)),
|
||||
ExcludeFocus(excluding: _selectedIndex != 1, child: ChannelRootPage(isVisiblePage: _selectedIndex == 1)),
|
||||
ExcludeFocus(excluding: _selectedIndex != 2, child: AccountRootPage(isVisiblePage: _selectedIndex == 2)),
|
||||
ExcludeFocus(excluding: _selectedIndex != 3, child: SettingsRootPage(isVisiblePage: _selectedIndex == 3)),
|
||||
ExcludeFocus(excluding: _selectedIndex != 4, child: SendRootPage(isVisiblePage: _selectedIndex == 4)),
|
||||
],
|
||||
index: _selectedIndex,
|
||||
),
|
||||
|
@ -7,47 +7,81 @@ import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/models/user.dart';
|
||||
import 'package:simplecloudnotifier/pages/account/login.dart';
|
||||
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/state/globals.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/types/immediate_future.dart';
|
||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class AccountRootPage extends StatefulWidget {
|
||||
const AccountRootPage({super.key});
|
||||
const AccountRootPage({super.key, required this.isVisiblePage});
|
||||
|
||||
final bool isVisiblePage;
|
||||
|
||||
@override
|
||||
State<AccountRootPage> createState() => _AccountRootPageState();
|
||||
}
|
||||
|
||||
class _AccountRootPageState extends State<AccountRootPage> {
|
||||
late Future<int>? futureSubscriptionCount;
|
||||
late Future<int>? futureClientCount;
|
||||
late Future<int>? futureKeyCount;
|
||||
late Future<int>? futureChannelAllCount;
|
||||
late Future<int>? futureChannelSubscribedCount;
|
||||
late ImmediateFuture<int>? futureSubscriptionCount;
|
||||
late ImmediateFuture<int>? futureClientCount;
|
||||
late ImmediateFuture<int>? futureKeyCount;
|
||||
late ImmediateFuture<int>? futureChannelAllCount;
|
||||
late ImmediateFuture<int>? futureChannelSubscribedCount;
|
||||
late ImmediateFuture<User>? futureUser;
|
||||
|
||||
late AppAuth userAcc;
|
||||
|
||||
bool loading = false;
|
||||
|
||||
bool _isInitialized = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
userAcc = Provider.of<AppAuth>(context, listen: false);
|
||||
userAcc.addListener(_onAuthStateChanged);
|
||||
|
||||
if (widget.isVisiblePage && !_isInitialized) _realInitState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(AccountRootPage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (oldWidget.isVisiblePage != widget.isVisiblePage && widget.isVisiblePage) {
|
||||
if (!_isInitialized) {
|
||||
_realInitState();
|
||||
} else {
|
||||
_backgroundRefresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _realInitState() {
|
||||
ApplicationLog.debug('AccountRootPage::_realInitState');
|
||||
_onAuthStateChanged();
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
ApplicationLog.debug('AccountRootPage::dispose');
|
||||
userAcc.removeListener(_onAuthStateChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onAuthStateChanged() {
|
||||
ApplicationLog.debug('AccountRootPage::_onAuthStateChanged');
|
||||
_createFutures();
|
||||
}
|
||||
|
||||
void _createFutures() {
|
||||
futureSubscriptionCount = null;
|
||||
futureClientCount = null;
|
||||
futureKeyCount = null;
|
||||
@ -55,35 +89,70 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
||||
futureChannelSubscribedCount = null;
|
||||
|
||||
if (userAcc.isAuth()) {
|
||||
futureChannelAllCount = () async {
|
||||
futureChannelAllCount = ImmediateFuture.ofFuture(() async {
|
||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||
final channels = await APIClient.getChannelList(userAcc, ChannelSelector.all);
|
||||
return channels.length;
|
||||
}();
|
||||
}());
|
||||
|
||||
futureChannelSubscribedCount = () async {
|
||||
futureChannelSubscribedCount = ImmediateFuture.ofFuture(() async {
|
||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||
final channels = await APIClient.getChannelList(userAcc, ChannelSelector.subscribed);
|
||||
return channels.length;
|
||||
}();
|
||||
}());
|
||||
|
||||
futureSubscriptionCount = () async {
|
||||
futureSubscriptionCount = ImmediateFuture.ofFuture(() async {
|
||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||
final subs = await APIClient.getSubscriptionList(userAcc);
|
||||
return subs.length;
|
||||
}();
|
||||
}());
|
||||
|
||||
futureClientCount = () async {
|
||||
futureClientCount = ImmediateFuture.ofFuture(() async {
|
||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||
final clients = await APIClient.getClientList(userAcc);
|
||||
return clients.length;
|
||||
}();
|
||||
}());
|
||||
|
||||
futureKeyCount = () async {
|
||||
futureKeyCount = ImmediateFuture.ofFuture(() async {
|
||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||
final keys = await APIClient.getKeyTokenList(userAcc);
|
||||
return keys.length;
|
||||
}();
|
||||
}());
|
||||
|
||||
futureUser = ImmediateFuture.ofFuture(userAcc.loadUser(force: false));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _backgroundRefresh() async {
|
||||
if (userAcc.isAuth()) {
|
||||
try {
|
||||
await Future.delayed(const Duration(seconds: 0), () {}); // this is annoyingly important - otherwise we call setLoadingIndeterminate directly in initStat() and get an exception....
|
||||
|
||||
AppBarState().setLoadingIndeterminate(true);
|
||||
|
||||
// refresh all data and then replace teh futures used in build()
|
||||
|
||||
final channelsAll = await APIClient.getChannelList(userAcc, ChannelSelector.all);
|
||||
final channelsSubscribed = await APIClient.getChannelList(userAcc, ChannelSelector.subscribed);
|
||||
final subs = await APIClient.getSubscriptionList(userAcc);
|
||||
final clients = await APIClient.getClientList(userAcc);
|
||||
final keys = await APIClient.getKeyTokenList(userAcc);
|
||||
final user = await userAcc.loadUser(force: true);
|
||||
|
||||
setState(() {
|
||||
futureChannelAllCount = ImmediateFuture.ofValue(channelsAll.length);
|
||||
futureChannelSubscribedCount = ImmediateFuture.ofValue(channelsSubscribed.length);
|
||||
futureSubscriptionCount = ImmediateFuture.ofValue(subs.length);
|
||||
futureClientCount = ImmediateFuture.ofValue(clients.length);
|
||||
futureKeyCount = ImmediateFuture.ofValue(keys.length);
|
||||
futureUser = ImmediateFuture.ofValue(user);
|
||||
});
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to refresh account data: ' + exc.toString(), trace: trace);
|
||||
Toaster.error("Error", 'Failed to refresh account data');
|
||||
} finally {
|
||||
AppBarState().setLoadingIndeterminate(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,19 +160,23 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<AppAuth>(
|
||||
builder: (context, acc, child) {
|
||||
if (!_isInitialized) return SizedBox();
|
||||
|
||||
if (!userAcc.isAuth()) {
|
||||
return _buildNoAuth(context);
|
||||
} else {
|
||||
return FutureBuilder(
|
||||
future: acc.loadUser(force: false),
|
||||
future: futureUser!.future,
|
||||
builder: ((context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (snapshot.hasError) {
|
||||
if (futureUser?.value != null) {
|
||||
return _buildShowAccount(context, acc, futureUser!.value!);
|
||||
} else if (snapshot.connectionState == ConnectionState.done && snapshot.hasError) {
|
||||
return Text('Error: ${snapshot.error}'); //TODO better error display
|
||||
}
|
||||
} else if (snapshot.connectionState == ConnectionState.done) {
|
||||
return _buildShowAccount(context, acc, snapshot.data!);
|
||||
}
|
||||
} else {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
@ -157,7 +230,7 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
||||
text: 'Use existing account',
|
||||
onPressed: () {
|
||||
if (loading) return;
|
||||
Navigator.push(context, MaterialPageRoute<AccountLoginPage>(builder: (context) => AccountLoginPage()));
|
||||
Navi.push(context, () => AccountLoginPage());
|
||||
},
|
||||
tonal: true,
|
||||
big: true,
|
||||
@ -255,12 +328,15 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
||||
children: [
|
||||
SizedBox(width: 80, child: Text("Channels", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)))),
|
||||
FutureBuilder(
|
||||
future: futureChannelAllCount,
|
||||
future: futureChannelAllCount!.future,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (futureChannelAllCount?.value != null) {
|
||||
return Text('${futureChannelAllCount!.value}');
|
||||
} else if (snapshot.connectionState == ConnectionState.done) {
|
||||
return Text('${snapshot.data}');
|
||||
}
|
||||
} else {
|
||||
return const SizedBox(width: 8, height: 8, child: Center(child: CircularProgressIndicator()));
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
@ -289,86 +365,10 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
||||
|
||||
List<Widget> _buildCards(BuildContext context, User user) {
|
||||
return [
|
||||
UI.buttonCard(
|
||||
context: context,
|
||||
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
||||
child: Row(
|
||||
children: [
|
||||
FutureBuilder(
|
||||
future: futureSubscriptionCount,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
||||
}
|
||||
return const SizedBox(width: 12, height: 12, child: Center(child: CircularProgressIndicator()));
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text('Subscriptions', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
||||
],
|
||||
),
|
||||
onTap: () {/*TODO*/},
|
||||
),
|
||||
UI.buttonCard(
|
||||
context: context,
|
||||
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
||||
child: Row(
|
||||
children: [
|
||||
FutureBuilder(
|
||||
future: futureClientCount,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
||||
}
|
||||
return const SizedBox(width: 12, height: 12, child: Center(child: CircularProgressIndicator()));
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text('Clients', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
||||
],
|
||||
),
|
||||
onTap: () {/*TODO*/},
|
||||
),
|
||||
UI.buttonCard(
|
||||
context: context,
|
||||
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
||||
child: Row(
|
||||
children: [
|
||||
FutureBuilder(
|
||||
future: futureKeyCount,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
||||
}
|
||||
return const SizedBox(width: 12, height: 12, child: Center(child: CircularProgressIndicator()));
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text('Keys', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
||||
],
|
||||
),
|
||||
onTap: () {/*TODO*/},
|
||||
),
|
||||
UI.buttonCard(
|
||||
context: context,
|
||||
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
||||
child: Row(
|
||||
children: [
|
||||
FutureBuilder(
|
||||
future: futureChannelSubscribedCount,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
||||
}
|
||||
return const SizedBox(width: 12, height: 12, child: Center(child: CircularProgressIndicator()));
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text('Channels', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
||||
],
|
||||
),
|
||||
onTap: () {/*TODO*/},
|
||||
),
|
||||
_buildNumberCard(context, 'Subscriptions', futureSubscriptionCount, () {/*TODO*/}),
|
||||
_buildNumberCard(context, 'Clients', futureClientCount, () {/*TODO*/}),
|
||||
_buildNumberCard(context, 'Keys', futureKeyCount, () {/*TODO*/}),
|
||||
_buildNumberCard(context, 'Channels', futureChannelSubscribedCount, () {/*TODO*/}),
|
||||
UI.buttonCard(
|
||||
context: context,
|
||||
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
||||
@ -384,6 +384,32 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
||||
];
|
||||
}
|
||||
|
||||
Widget _buildNumberCard(BuildContext context, String txt, ImmediateFuture<int>? future, void Function() action) {
|
||||
return UI.buttonCard(
|
||||
context: context,
|
||||
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
||||
child: Row(
|
||||
children: [
|
||||
FutureBuilder(
|
||||
future: future?.future,
|
||||
builder: (context, snapshot) {
|
||||
if (future?.value != null) {
|
||||
return Text('${future?.value}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
||||
} else if (snapshot.connectionState == ConnectionState.done) {
|
||||
return Text('${snapshot.data}', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20));
|
||||
} else {
|
||||
return const SizedBox(width: 12, height: 12, child: Center(child: CircularProgressIndicator()));
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(txt, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
|
||||
],
|
||||
),
|
||||
onTap: action,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFooter(BuildContext context, User user) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 0, 8, 0),
|
||||
|
@ -8,6 +8,7 @@ import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/state/globals.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/state/token_source.dart';
|
||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
||||
|
||||
@ -154,7 +155,7 @@ class _AccountLoginPageState extends State<AccountLoginPage> {
|
||||
await acc.save();
|
||||
|
||||
Toaster.success("Login", "Successfully logged in");
|
||||
Navigator.popUntil(context, (route) => route.isFirst);
|
||||
Navi.popToRoot(context);
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to verify token: ' + exc.toString(), trace: trace);
|
||||
Toaster.error("Error", 'Failed to verify token');
|
||||
|
@ -3,37 +3,65 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/models/channel.dart';
|
||||
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/pages/channel_list/channel_list_item.dart';
|
||||
|
||||
class ChannelRootPage extends StatefulWidget {
|
||||
const ChannelRootPage({super.key});
|
||||
const ChannelRootPage({super.key, required this.isVisiblePage});
|
||||
|
||||
final bool isVisiblePage;
|
||||
|
||||
@override
|
||||
State<ChannelRootPage> createState() => _ChannelRootPageState();
|
||||
}
|
||||
|
||||
class _ChannelRootPageState extends State<ChannelRootPage> {
|
||||
final PagingController<int, Channel> _pagingController = PagingController(firstPageKey: 0);
|
||||
final PagingController<int, Channel> _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: 0);
|
||||
|
||||
bool _isInitialized = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_pagingController.addPageRequestListener((pageKey) {
|
||||
_fetchPage(pageKey);
|
||||
});
|
||||
super.initState();
|
||||
|
||||
_pagingController.addPageRequestListener(_fetchPage);
|
||||
|
||||
if (widget.isVisiblePage && !_isInitialized) _realInitState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
ApplicationLog.debug('ChannelRootPage::dispose');
|
||||
_pagingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ChannelRootPage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (oldWidget.isVisiblePage != widget.isVisiblePage && widget.isVisiblePage) {
|
||||
if (!_isInitialized) {
|
||||
_realInitState();
|
||||
} else {
|
||||
_backgroundRefresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _realInitState() {
|
||||
ApplicationLog.debug('ChannelRootPage::_realInitState');
|
||||
_pagingController.refresh();
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
Future<void> _fetchPage(int pageKey) async {
|
||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
||||
|
||||
ApplicationLog.debug('Start ChannelList::_pagingController::_fetchPage [ ${pageKey} ]');
|
||||
|
||||
if (!acc.isAuth()) {
|
||||
_pagingController.error = 'Not logged in';
|
||||
return;
|
||||
@ -44,13 +72,41 @@ class _ChannelRootPageState extends State<ChannelRootPage> {
|
||||
|
||||
items.sort((a, b) => -1 * (a.timestampLastSent ?? '').compareTo(b.timestampLastSent ?? ''));
|
||||
|
||||
_pagingController.appendLastPage(items);
|
||||
_pagingController.value = PagingState(nextPageKey: null, itemList: items, error: null);
|
||||
} catch (exc, trace) {
|
||||
_pagingController.error = exc.toString();
|
||||
ApplicationLog.error('Failed to list channels: ' + exc.toString(), trace: trace);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _backgroundRefresh() async {
|
||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
||||
|
||||
ApplicationLog.debug('Start background refresh of channel list');
|
||||
|
||||
if (!acc.isAuth()) {
|
||||
_pagingController.error = 'Not logged in';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await Future.delayed(const Duration(seconds: 0), () {}); // this is annoyingly important - otherwise we call setLoadingIndeterminate directly in initStat() and get an exception....
|
||||
|
||||
AppBarState().setLoadingIndeterminate(true);
|
||||
|
||||
final items = (await APIClient.getChannelList(acc, ChannelSelector.all)).map((p) => p.channel).toList();
|
||||
|
||||
items.sort((a, b) => -1 * (a.timestampLastSent ?? '').compareTo(b.timestampLastSent ?? ''));
|
||||
|
||||
_pagingController.value = PagingState(nextPageKey: null, itemList: items, error: null);
|
||||
} catch (exc, trace) {
|
||||
_pagingController.error = exc.toString();
|
||||
ApplicationLog.error('Failed to list channels: ' + exc.toString(), trace: trace);
|
||||
} finally {
|
||||
AppBarState().setLoadingIndeterminate(false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RefreshIndicator(
|
||||
|
@ -1,10 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:simplecloudnotifier/models/channel.dart';
|
||||
import 'package:simplecloudnotifier/models/message.dart';
|
||||
import 'package:simplecloudnotifier/pages/debug/debug_persistence_hive.dart';
|
||||
import 'package:simplecloudnotifier/pages/debug/debug_persistence_sharedprefs.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/state/fb_message.dart';
|
||||
import 'package:simplecloudnotifier/state/interfaces.dart';
|
||||
import 'package:simplecloudnotifier/state/request_log.dart';
|
||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||
|
||||
class DebugPersistencePage extends StatefulWidget {
|
||||
@override
|
||||
@ -28,62 +33,56 @@ class _DebugPersistencePageState extends State<DebugPersistencePage> {
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Card.outlined(
|
||||
_buildSharedPrefCard(context),
|
||||
_buildHiveCard(context, () => Hive.box<SCNRequest>('scn-requests'), 'scn-requests'),
|
||||
_buildHiveCard(context, () => Hive.box<SCNLog>('scn-logs'), 'scn-logs'),
|
||||
_buildHiveCard(context, () => Hive.box<Message>('scn-message-cache'), 'scn-message-cache'),
|
||||
_buildHiveCard(context, () => Hive.box<Channel>('scn-channel-cache'), 'scn-channel-cache'),
|
||||
_buildHiveCard(context, () => Hive.box<FBMessage>('scn-fb-messages'), 'scn-fb-messages'),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSharedPrefCard(BuildContext context) {
|
||||
return Card.outlined(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute<DebugSharedPrefPage>(builder: (context) => DebugSharedPrefPage(sharedPref: prefs!)));
|
||||
Navi.push(context, () => DebugSharedPrefPage(sharedPref: prefs!));
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SizedBox(width: 30, child: Text('')),
|
||||
Expanded(child: Text('Shared Preferences', style: TextStyle(fontWeight: FontWeight.bold), textAlign: TextAlign.center)),
|
||||
SizedBox(width: 30, child: Text('${prefs?.getKeys().length.toString()}', textAlign: TextAlign.end)),
|
||||
SizedBox(width: 40, child: Text('${prefs?.getKeys().length.toString()}', textAlign: TextAlign.end)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Card.outlined(
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHiveCard(BuildContext context, Box<FieldDebuggable> Function() boxFunc, String boxname) {
|
||||
return Card.outlined(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute<DebugHiveBoxPage>(builder: (context) => DebugHiveBoxPage(boxName: 'scn-requests', box: Hive.box<SCNRequest>('scn-requests'))));
|
||||
Navi.push(context, () => DebugHiveBoxPage(boxName: boxname, box: Hive.box<FBMessage>(boxname)));
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SizedBox(width: 30, child: Text('')),
|
||||
Expanded(child: Text('Hive [scn-requests]', style: TextStyle(fontWeight: FontWeight.bold), textAlign: TextAlign.center)),
|
||||
SizedBox(width: 30, child: Text('${Hive.box<SCNRequest>('scn-requests').length.toString()}', textAlign: TextAlign.end)),
|
||||
Expanded(child: Text('Hive [$boxname]', style: TextStyle(fontWeight: FontWeight.bold), textAlign: TextAlign.center)),
|
||||
SizedBox(width: 40, child: Text('${boxFunc().length.toString()}', textAlign: TextAlign.end)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Card.outlined(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute<DebugHiveBoxPage>(builder: (context) => DebugHiveBoxPage(boxName: 'scn-requests', box: Hive.box<SCNLog>('scn-logs'))));
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SizedBox(width: 30, child: Text('')),
|
||||
Expanded(child: Text('Hive [scn-logs]', style: TextStyle(fontWeight: FontWeight.bold), textAlign: TextAlign.center)),
|
||||
SizedBox(width: 30, child: Text('${Hive.box<SCNLog>('scn-logs').length.toString()}', textAlign: TextAlign.end)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
|
||||
import 'package:simplecloudnotifier/pages/debug/debug_persistence_hiveentry.dart';
|
||||
import 'package:simplecloudnotifier/state/interfaces.dart';
|
||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||
|
||||
class DebugHiveBoxPage extends StatelessWidget {
|
||||
final String boxName;
|
||||
@ -21,7 +22,7 @@ class DebugHiveBoxPage extends StatelessWidget {
|
||||
itemBuilder: (context, listIndex) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute<DebugHiveEntryPage>(builder: (context) => DebugHiveEntryPage(value: box.getAt(listIndex)!)));
|
||||
Navi.push(context, () => DebugHiveEntryPage(value: box.getAt(listIndex)!));
|
||||
},
|
||||
child: ListTile(
|
||||
title: Text(box.getAt(listIndex).toString(), style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
|
@ -3,6 +3,7 @@ import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:simplecloudnotifier/pages/debug/debug_request_view.dart';
|
||||
import 'package:simplecloudnotifier/state/request_log.dart';
|
||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||
|
||||
class DebugRequestsPage extends StatefulWidget {
|
||||
@override
|
||||
@ -40,7 +41,7 @@ class _DebugRequestsPageState extends State<DebugRequestsPage> {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 2.0),
|
||||
child: GestureDetector(
|
||||
onTap: () => Navigator.push(context, MaterialPageRoute<DebugRequestViewPage>(builder: (context) => DebugRequestViewPage(request: req))),
|
||||
onTap: () => Navi.push(context, () => DebugRequestViewPage(request: req)),
|
||||
child: ListTile(
|
||||
tileColor: Theme.of(context).colorScheme.errorContainer,
|
||||
textColor: Theme.of(context).colorScheme.onErrorContainer,
|
||||
@ -76,7 +77,7 @@ class _DebugRequestsPageState extends State<DebugRequestsPage> {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 2.0),
|
||||
child: GestureDetector(
|
||||
onTap: () => Navigator.push(context, MaterialPageRoute<DebugRequestViewPage>(builder: (context) => DebugRequestViewPage(request: req))),
|
||||
onTap: () => Navi.push(context, () => DebugRequestViewPage(request: req)),
|
||||
child: ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
|
@ -1,16 +1,21 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/models/channel.dart';
|
||||
import 'package:simplecloudnotifier/models/message.dart';
|
||||
import 'package:simplecloudnotifier/pages/message_view/message_view.dart';
|
||||
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/pages/message_list/message_list_item.dart';
|
||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||
|
||||
class MessageListPage extends StatefulWidget {
|
||||
const MessageListPage({super.key});
|
||||
const MessageListPage({super.key, required this.isVisiblePage});
|
||||
|
||||
final bool isVisiblePage;
|
||||
|
||||
//TODO reload on switch to tab
|
||||
//TODO reload on app to foreground
|
||||
@ -19,31 +24,104 @@ class MessageListPage extends StatefulWidget {
|
||||
State<MessageListPage> createState() => _MessageListPageState();
|
||||
}
|
||||
|
||||
class _MessageListPageState extends State<MessageListPage> {
|
||||
class _MessageListPageState extends State<MessageListPage> with RouteAware {
|
||||
static const _pageSize = 128;
|
||||
|
||||
final PagingController<String, Message> _pagingController = PagingController(firstPageKey: '@start');
|
||||
late final AppLifecycleListener _lifecyleListener;
|
||||
|
||||
PagingController<String, Message> _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: '@start');
|
||||
|
||||
Map<String, Channel>? _channels = null;
|
||||
|
||||
bool _isInitialized = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
//TODO init with state from cache - also allow tho show cache on error
|
||||
_pagingController.addPageRequestListener((pageKey) {
|
||||
_fetchPage(pageKey);
|
||||
});
|
||||
super.initState();
|
||||
|
||||
_pagingController.addPageRequestListener(_fetchPage);
|
||||
|
||||
if (widget.isVisiblePage && !_isInitialized) _realInitState();
|
||||
|
||||
_lifecyleListener = AppLifecycleListener(
|
||||
onResume: _onLifecycleResume,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(MessageListPage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (oldWidget.isVisiblePage != widget.isVisiblePage && widget.isVisiblePage) {
|
||||
if (!_isInitialized) {
|
||||
_realInitState();
|
||||
} else {
|
||||
_backgroundRefresh(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _realInitState() {
|
||||
ApplicationLog.debug('MessageListPage::_realInitState');
|
||||
|
||||
final chnCache = Hive.box<Channel>('scn-channel-cache');
|
||||
final msgCache = Hive.box<Message>('scn-message-cache');
|
||||
|
||||
if (chnCache.isNotEmpty && msgCache.isNotEmpty) {
|
||||
// ==== Use cache values - and refresh in background
|
||||
|
||||
_channels = <String, Channel>{for (var v in chnCache.values) v.channelID: v};
|
||||
|
||||
final cacheMessages = msgCache.values.toList();
|
||||
cacheMessages.sort((a, b) => -1 * a.timestamp.compareTo(b.timestamp));
|
||||
|
||||
_pagingController.value = PagingState(nextPageKey: null, itemList: cacheMessages, error: null);
|
||||
|
||||
_backgroundRefresh(true);
|
||||
} else {
|
||||
// ==== Full refresh - no cache available
|
||||
_pagingController.refresh();
|
||||
}
|
||||
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
Navi.modalRouteObserver.subscribe(this, ModalRoute.of(context)!);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
ApplicationLog.debug('MessageListPage::dispose');
|
||||
Navi.modalRouteObserver.unsubscribe(this);
|
||||
_pagingController.dispose();
|
||||
_lifecyleListener.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didPush() {
|
||||
// ...
|
||||
}
|
||||
|
||||
@override
|
||||
void didPopNext() {
|
||||
ApplicationLog.debug('[MessageList::RouteObserver] --> didPopNext (will background-refresh)');
|
||||
_backgroundRefresh(false);
|
||||
}
|
||||
|
||||
void _onLifecycleResume() {
|
||||
ApplicationLog.debug('[MessageList::_onLifecycleResume] --> (will background-refresh)');
|
||||
_backgroundRefresh(false);
|
||||
}
|
||||
|
||||
Future<void> _fetchPage(String thisPageToken) async {
|
||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
||||
|
||||
ApplicationLog.debug('Start MessageList::_pagingController::_fetchPage [ ${thisPageToken} ]');
|
||||
|
||||
if (!acc.isAuth()) {
|
||||
_pagingController.error = 'Not logged in';
|
||||
return;
|
||||
@ -53,10 +131,16 @@ class _MessageListPageState extends State<MessageListPage> {
|
||||
if (_channels == null) {
|
||||
final channels = await APIClient.getChannelList(acc, ChannelSelector.allAny);
|
||||
_channels = <String, Channel>{for (var v in channels) v.channel.channelID: v.channel};
|
||||
|
||||
_setChannelCache(channels); // no await
|
||||
}
|
||||
|
||||
final (npt, newItems) = await APIClient.getMessageList(acc, thisPageToken, pageSize: _pageSize);
|
||||
|
||||
_addToMessageCache(newItems); // no await
|
||||
|
||||
ApplicationLog.debug('Finished MessageList::_pagingController::_fetchPage [ ${newItems.length} items and npt: ${thisPageToken} --> ${npt} ]');
|
||||
|
||||
if (npt == '@end') {
|
||||
_pagingController.appendLastPage(newItems);
|
||||
} else {
|
||||
@ -68,6 +152,71 @@ class _MessageListPageState extends State<MessageListPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _backgroundRefresh(bool fullReplaceState) async {
|
||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
||||
|
||||
ApplicationLog.debug('Start background refresh of message list (fullReplaceState: $fullReplaceState)');
|
||||
|
||||
try {
|
||||
await Future.delayed(const Duration(seconds: 0), () {}); // this is annoyingly important - otherwise we call setLoadingIndeterminate directly in initStat() and get an exception....
|
||||
|
||||
AppBarState().setLoadingIndeterminate(true);
|
||||
|
||||
if (_channels == null || fullReplaceState) {
|
||||
final channels = await APIClient.getChannelList(acc, ChannelSelector.allAny);
|
||||
setState(() {
|
||||
_channels = <String, Channel>{for (var v in channels) v.channel.channelID: v.channel};
|
||||
});
|
||||
_setChannelCache(channels); // no await
|
||||
}
|
||||
|
||||
final (npt, newItems) = await APIClient.getMessageList(acc, '@start', pageSize: _pageSize);
|
||||
|
||||
_addToMessageCache(newItems); // no await
|
||||
|
||||
if (fullReplaceState) {
|
||||
// fully replace/reset state
|
||||
ApplicationLog.debug('Background-refresh finished (fullReplaceState) - replace state with ${newItems.length} items and npt: [ $npt ]');
|
||||
setState(() {
|
||||
if (npt == '@end')
|
||||
_pagingController.value = PagingState(nextPageKey: null, itemList: newItems, error: null);
|
||||
else
|
||||
_pagingController.value = PagingState(nextPageKey: npt, itemList: newItems, error: null);
|
||||
});
|
||||
} else {
|
||||
final itemsToBeAdded = newItems.where((p1) => !(_pagingController.itemList ?? []).any((p2) => p1.messageID == p2.messageID)).toList();
|
||||
if (itemsToBeAdded.isEmpty) {
|
||||
// nothing to do - no new items...
|
||||
// ....
|
||||
ApplicationLog.debug('Background-refresh returned no new items - nothing to do.');
|
||||
} else if (itemsToBeAdded.length == newItems.length) {
|
||||
// all items are new ?!?, the current state is completely fucked - full replace
|
||||
ApplicationLog.debug('Background-refresh found only new items ?!? - fully replace state with ${newItems.length} items');
|
||||
setState(() {
|
||||
if (npt == '@end')
|
||||
_pagingController.value = PagingState(nextPageKey: null, itemList: newItems, error: null);
|
||||
else
|
||||
_pagingController.value = PagingState(nextPageKey: npt, itemList: newItems, error: null);
|
||||
_pagingController.itemList = null;
|
||||
});
|
||||
} else {
|
||||
// add new items to the front
|
||||
ApplicationLog.debug('Background-refresh found ${newItems.length} new items - add to front');
|
||||
setState(() {
|
||||
_pagingController.itemList = itemsToBeAdded + (_pagingController.itemList ?? []);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (exc, trace) {
|
||||
setState(() {
|
||||
_pagingController.error = exc.toString();
|
||||
});
|
||||
ApplicationLog.error('Failed to list messages: ' + exc.toString(), trace: trace);
|
||||
} finally {
|
||||
AppBarState().setLoadingIndeterminate(false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
@ -83,7 +232,7 @@ class _MessageListPageState extends State<MessageListPage> {
|
||||
message: item,
|
||||
allChannels: _channels ?? {},
|
||||
onPressed: () {
|
||||
Navigator.push(context, MaterialPageRoute<MessageViewPage>(builder: (context) => MessageViewPage(message: item)));
|
||||
Navi.push(context, () => MessageViewPage(message: item));
|
||||
},
|
||||
),
|
||||
),
|
||||
@ -91,4 +240,30 @@ class _MessageListPageState extends State<MessageListPage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Future<void> _addToMessageCache(List<Message> newItems) async {
|
||||
final cache = Hive.box<Message>('scn-message-cache');
|
||||
|
||||
for (var msg in newItems) await cache.put(msg.messageID, msg);
|
||||
|
||||
// delete all but the newest 128 messages
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -48,10 +48,6 @@ class MessageListItem extends StatelessWidget {
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (message.priority == 2) FaIcon(FontAwesomeIcons.solidTriangleExclamation, size: 16, color: Colors.red[900]),
|
||||
if (message.priority == 2) SizedBox(width: 4),
|
||||
if (message.priority == 0) FaIcon(FontAwesomeIcons.solidDown, size: 16, color: Colors.lightBlue[900]),
|
||||
if (message.priority == 0) SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
processTitle(message.title),
|
||||
@ -69,12 +65,23 @@ class MessageListItem extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
processContent(message.content),
|
||||
style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: _lineCount,
|
||||
),
|
||||
),
|
||||
if (message.priority == 2) SizedBox(width: 4),
|
||||
if (message.priority == 2) FaIcon(FontAwesomeIcons.solidTriangleExclamation, size: 16, color: Colors.red[900]),
|
||||
if (message.priority == 0) SizedBox(width: 4),
|
||||
if (message.priority == 0) FaIcon(FontAwesomeIcons.solidDown, size: 16, color: Colors.lightBlue[900]),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -99,10 +106,6 @@ class MessageListItem extends StatelessWidget {
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (message.priority == 2) FaIcon(FontAwesomeIcons.solidTriangleExclamation, size: 16, color: Colors.red[900]),
|
||||
if (message.priority == 2) SizedBox(width: 4),
|
||||
if (message.priority == 0) FaIcon(FontAwesomeIcons.solidDown, size: 16, color: Colors.lightBlue[900]),
|
||||
if (message.priority == 0) SizedBox(width: 4),
|
||||
UI.channelChip(
|
||||
context: context,
|
||||
text: resolveChannelName(message),
|
||||
@ -124,12 +127,23 @@ class MessageListItem extends StatelessWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 3,
|
||||
),
|
||||
Text(
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
processContent(message.content),
|
||||
style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: _lineCount,
|
||||
),
|
||||
),
|
||||
if (message.priority == 2) SizedBox(width: 4),
|
||||
if (message.priority == 2) FaIcon(FontAwesomeIcons.solidTriangleExclamation, size: 16, color: Colors.red[900]),
|
||||
if (message.priority == 0) SizedBox(width: 4),
|
||||
if (message.priority == 0) FaIcon(FontAwesomeIcons.solidDown, size: 16, color: Colors.lightBlue[900]),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -5,13 +5,13 @@ import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/api/api_exception.dart';
|
||||
import 'package:simplecloudnotifier/components/layout/scaffold.dart';
|
||||
import 'package:simplecloudnotifier/models/api_error.dart';
|
||||
import 'package:simplecloudnotifier/models/channel.dart';
|
||||
import 'package:simplecloudnotifier/models/keytoken.dart';
|
||||
import 'package:simplecloudnotifier/models/message.dart';
|
||||
import 'package:simplecloudnotifier/models/user.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
||||
|
||||
@ -25,54 +25,46 @@ class MessageViewPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MessageViewPageState extends State<MessageViewPage> {
|
||||
late Future<(Message, ChannelWithSubscription?, KeyToken?)>? mainFuture;
|
||||
(Message, ChannelWithSubscription?, KeyToken?)? mainFutureSnapshot = null;
|
||||
late Future<(Message, ChannelPreview, KeyTokenPreview, UserPreview)>? mainFuture;
|
||||
(Message, ChannelPreview, KeyTokenPreview, UserPreview)? mainFutureSnapshot = null;
|
||||
static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm');
|
||||
|
||||
bool _monospaceMode = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
mainFuture = fetchData();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
Future<(Message, ChannelWithSubscription?, KeyToken?)> fetchData() async {
|
||||
Future<(Message, ChannelPreview, KeyTokenPreview, UserPreview)> fetchData() async {
|
||||
try {
|
||||
await Future.delayed(const Duration(seconds: 0), () {}); // this is annoyingly important - otherwise we call setLoadingIndeterminate directly in initStat() and get an exception....
|
||||
|
||||
AppBarState().setLoadingIndeterminate(true);
|
||||
|
||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
||||
|
||||
final msg = await APIClient.getMessage(acc, widget.message.messageID);
|
||||
|
||||
ChannelWithSubscription? chn = null;
|
||||
try {
|
||||
chn = await APIClient.getChannel(acc, msg.channelID); //TODO getShortChannel (?) -> no perm
|
||||
} on APIException catch (e) {
|
||||
if (e.error == APIError.USER_AUTH_FAILED) {
|
||||
chn = null;
|
||||
} else {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
final fut_chn = APIClient.getChannelPreview(acc, msg.channelID);
|
||||
final fut_key = APIClient.getKeyTokenPreview(acc, msg.usedKeyID);
|
||||
final fut_usr = APIClient.getUserPreview(acc, msg.senderUserID);
|
||||
|
||||
KeyToken? tok = null;
|
||||
try {
|
||||
tok = await APIClient.getKeyToken(acc, msg.usedKeyID); //TODO getShortKeyToken (?) -> no perm
|
||||
} on APIException catch (e) {
|
||||
if (e.error == APIError.USER_AUTH_FAILED) {
|
||||
tok = null;
|
||||
} else {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
final chn = await fut_chn;
|
||||
final key = await fut_key;
|
||||
final usr = await fut_usr;
|
||||
|
||||
//TODO getShortUser for sender
|
||||
//await Future.delayed(const Duration(seconds: 10), () {});
|
||||
|
||||
//await Future.delayed(const Duration(seconds: 2), () {});
|
||||
|
||||
final r = (msg, chn, tok);
|
||||
final r = (msg, chn, key, usr);
|
||||
|
||||
mainFutureSnapshot = r;
|
||||
|
||||
return r;
|
||||
} finally {
|
||||
AppBarState().setLoadingIndeterminate(false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -87,16 +79,16 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
||||
showSearch: false,
|
||||
showShare: true,
|
||||
onShare: _share,
|
||||
child: FutureBuilder<(Message, ChannelWithSubscription?, KeyToken?)>(
|
||||
child: FutureBuilder<(Message, ChannelPreview, KeyTokenPreview, UserPreview)>(
|
||||
future: mainFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
final (msg, chn, tok) = snapshot.data!;
|
||||
return _buildMessageView(context, msg, chn, tok, false);
|
||||
final (msg, chn, tok, usr) = snapshot.data!;
|
||||
return _buildMessageView(context, msg, chn, tok, usr);
|
||||
} else if (snapshot.hasError) {
|
||||
return Center(child: Text('${snapshot.error}')); //TODO nice error page
|
||||
} else if (!widget.message.trimmed) {
|
||||
return _buildMessageView(context, widget.message, null, null, true);
|
||||
return _buildMessageView(context, widget.message, null, null, null);
|
||||
} else {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
@ -108,7 +100,7 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
||||
void _share() async {
|
||||
var msg = widget.message;
|
||||
if (mainFutureSnapshot != null) {
|
||||
(msg, _, _) = mainFutureSnapshot!;
|
||||
(msg, _, _, _) = mainFutureSnapshot!;
|
||||
}
|
||||
|
||||
if (msg.content != null) {
|
||||
@ -126,7 +118,7 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildMessageView(BuildContext context, Message message, ChannelWithSubscription? channel, KeyToken? token, bool loading) {
|
||||
Widget _buildMessageView(BuildContext context, Message message, ChannelPreview? channel, KeyTokenPreview? token, UserPreview? user) {
|
||||
final userAccUserID = context.select<AppAuth, String?>((v) => v.userID);
|
||||
|
||||
return SingleChildScrollView(
|
||||
@ -135,16 +127,16 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
..._buildMessageHeader(context, message, channel, token, loading),
|
||||
..._buildMessageHeader(context, message, channel),
|
||||
SizedBox(height: 8),
|
||||
if (message.content != null) ..._buildMessageContent(context, message, channel, token),
|
||||
if (message.content != null) ..._buildMessageContent(context, message),
|
||||
SizedBox(height: 8),
|
||||
if (message.senderName != null) _buildMetaCard(context, FontAwesomeIcons.solidSignature, 'Sender', [message.senderName!], () => {/*TODO*/}),
|
||||
_buildMetaCard(context, FontAwesomeIcons.solidGearCode, 'KeyToken', [message.usedKeyID, if (token != null) token.name], () => {/*TODO*/}),
|
||||
_buildMetaCard(context, FontAwesomeIcons.solidIdCardClip, 'MessageID', [message.messageID, if (message.userMessageID != null) message.userMessageID!], null),
|
||||
_buildMetaCard(context, FontAwesomeIcons.solidSnake, 'Channel', [message.channelID, channel?.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.solidUser, 'User', ['TODO'], () => {/*TODO*/}), //TODO
|
||||
_buildMetaCard(context, FontAwesomeIcons.solidUser, 'User', [if (user != null) user.userID, if (user?.username != null) user!.username!], () => {/*TODO*/}), //TODO
|
||||
if (message.senderUserID == userAccUserID) UI.button(text: "Delete Message", onPressed: () {/*TODO*/}, color: Colors.red[900]),
|
||||
],
|
||||
),
|
||||
@ -152,11 +144,11 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
||||
);
|
||||
}
|
||||
|
||||
String _resolveChannelName(ChannelWithSubscription? channel, Message message) {
|
||||
return channel?.channel.displayName ?? message.channelInternalName;
|
||||
String _resolveChannelName(ChannelPreview? channel, Message message) {
|
||||
return channel?.displayName ?? message.channelInternalName;
|
||||
}
|
||||
|
||||
List<Widget> _buildMessageHeader(BuildContext context, Message message, ChannelWithSubscription? channel, KeyToken? token, bool loading) {
|
||||
List<Widget> _buildMessageHeader(BuildContext context, Message message, ChannelPreview? channel) {
|
||||
return [
|
||||
Row(
|
||||
children: [
|
||||
@ -171,28 +163,11 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
||||
],
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
if (!loading) Text(message.title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
if (loading)
|
||||
Stack(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Flexible(child: Text(message.title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold))),
|
||||
SizedBox(height: 20, width: 20),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: SizedBox(width: 0)),
|
||||
SizedBox(child: CircularProgressIndicator(), height: 20, width: 20),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(_preformatTitle(message), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> _buildMessageContent(BuildContext context, Message message, ChannelWithSubscription? channel, KeyToken? token) {
|
||||
List<Widget> _buildMessageContent(BuildContext context, Message message) {
|
||||
return [
|
||||
Row(
|
||||
children: [
|
||||
@ -273,4 +248,8 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _preformatTitle(Message message) {
|
||||
return message.title.replaceAll('\n', '').replaceAll('\r', '').replaceAll('\t', ' ');
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
|
||||
class SendRootPage extends StatefulWidget {
|
||||
const SendRootPage({super.key});
|
||||
const SendRootPage({super.key, required bool isVisiblePage});
|
||||
|
||||
@override
|
||||
State<SendRootPage> createState() => _SendRootPageState();
|
||||
@ -130,6 +130,8 @@ class _SendRootPageState extends State<SendRootPage> {
|
||||
try {
|
||||
final Uri uri = Uri.parse(url);
|
||||
|
||||
ApplicationLog.debug('Opening URL: [ ${uri.toString()} ]');
|
||||
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri);
|
||||
} else {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SettingsRootPage extends StatefulWidget {
|
||||
const SettingsRootPage({super.key});
|
||||
const SettingsRootPage({super.key, required bool isVisiblePage});
|
||||
|
||||
@override
|
||||
State<SettingsRootPage> createState() => _SettingsRootPageState();
|
||||
|
@ -14,6 +14,7 @@ class AppAuth extends ChangeNotifier implements TokenSource {
|
||||
|
||||
User? _user;
|
||||
Client? _client;
|
||||
DateTime? _clientQueryTime;
|
||||
|
||||
String? get userID => _userID;
|
||||
String? get tokenAdmin => _tokenAdmin;
|
||||
@ -117,14 +118,17 @@ class AppAuth extends ChangeNotifier implements TokenSource {
|
||||
final user = await APIClient.getUser(this, _userID!);
|
||||
|
||||
_user = user;
|
||||
notifyListeners();
|
||||
|
||||
await save();
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
Future<Client?> loadClient({bool force = false}) async {
|
||||
Future<Client?> loadClient({bool force = false, Duration? forceIfOlder = null}) async {
|
||||
if (forceIfOlder != null && _clientQueryTime != null && _clientQueryTime!.difference(DateTime.now()) > forceIfOlder) {
|
||||
force = true;
|
||||
}
|
||||
|
||||
if (!force && _client != null && _client!.clientID == _clientID) {
|
||||
return _client!;
|
||||
}
|
||||
@ -137,14 +141,12 @@ class AppAuth extends ChangeNotifier implements TokenSource {
|
||||
final client = await APIClient.getClient(this, _clientID!);
|
||||
|
||||
_client = client;
|
||||
notifyListeners();
|
||||
|
||||
await save();
|
||||
|
||||
return client;
|
||||
} on APIException catch (_) {
|
||||
_client = null;
|
||||
notifyListeners();
|
||||
return null;
|
||||
} catch (exc) {
|
||||
_client = null;
|
||||
|
20
flutter/lib/state/app_bar_state.dart
Normal file
@ -0,0 +1,20 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class AppBarState extends ChangeNotifier {
|
||||
static AppBarState? _singleton = AppBarState._internal();
|
||||
|
||||
factory AppBarState() {
|
||||
return _singleton ?? (_singleton = AppBarState._internal());
|
||||
}
|
||||
|
||||
AppBarState._internal() {}
|
||||
|
||||
bool _loadingIndeterminate = false;
|
||||
bool get loadingIndeterminate => _loadingIndeterminate;
|
||||
|
||||
void setLoadingIndeterminate(bool v) {
|
||||
if (_loadingIndeterminate == v) return;
|
||||
_loadingIndeterminate = v;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
@ -5,8 +5,10 @@ import 'package:xid/xid.dart';
|
||||
part 'application_log.g.dart';
|
||||
|
||||
class ApplicationLog {
|
||||
//TODO max size, auto clear old
|
||||
|
||||
static void debug(String message, {String? additional, StackTrace? trace}) {
|
||||
print('[DEBUG] ${message}: ${additional ?? ''}');
|
||||
(additional != null && additional != '') ? print('[DEBUG] ${message}: ${additional}') : print('[DEBUG] ${message}');
|
||||
|
||||
Hive.box<SCNLog>('scn-logs').add(SCNLog(
|
||||
id: Xid().toString(),
|
||||
@ -19,7 +21,7 @@ class ApplicationLog {
|
||||
}
|
||||
|
||||
static void info(String message, {String? additional, StackTrace? trace}) {
|
||||
print('[INFO] ${message}: ${additional ?? ''}');
|
||||
(additional != null && additional != '') ? print('[INFO] ${message}: ${additional}') : print('[INFO] ${message}');
|
||||
|
||||
Hive.box<SCNLog>('scn-logs').add(SCNLog(
|
||||
id: Xid().toString(),
|
||||
@ -32,7 +34,7 @@ class ApplicationLog {
|
||||
}
|
||||
|
||||
static void warn(String message, {String? additional, StackTrace? trace}) {
|
||||
print('[WARN] ${message}: ${additional ?? ''}');
|
||||
(additional != null && additional != '') ? print('[WARN] ${message}: ${additional}') : print('[WARN] ${message}');
|
||||
|
||||
Hive.box<SCNLog>('scn-logs').add(SCNLog(
|
||||
id: Xid().toString(),
|
||||
@ -45,7 +47,7 @@ class ApplicationLog {
|
||||
}
|
||||
|
||||
static void error(String message, {String? additional, StackTrace? trace}) {
|
||||
print('[ERROR] ${message}: ${additional ?? ''}');
|
||||
(additional != null && additional != '') ? print('[ERROR] ${message}: ${additional}') : print('[ERROR] ${message}');
|
||||
|
||||
Hive.box<SCNLog>('scn-logs').add(SCNLog(
|
||||
id: Xid().toString(),
|
||||
@ -58,7 +60,7 @@ class ApplicationLog {
|
||||
}
|
||||
|
||||
static void fatal(String message, {String? additional, StackTrace? trace}) {
|
||||
print('[FATAL] ${message}: ${additional ?? ''}');
|
||||
(additional != null && additional != '') ? print('[FATAL] ${message}: ${additional}') : print('[FATAL] ${message}');
|
||||
|
||||
Hive.box<SCNLog>('scn-logs').add(SCNLog(
|
||||
id: Xid().toString(),
|
||||
|
236
flutter/lib/state/fb_message.dart
Normal file
@ -0,0 +1,236 @@
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:simplecloudnotifier/state/interfaces.dart';
|
||||
|
||||
part 'fb_message.g.dart';
|
||||
|
||||
class FBMessageLog {
|
||||
//TODO max size, auto clear old
|
||||
|
||||
static void insert(RemoteMessage msg) {
|
||||
Hive.box<FBMessage>('scn-fb-messages').add(FBMessage.fromRemoteMessage(msg));
|
||||
}
|
||||
}
|
||||
|
||||
@HiveType(typeId: 106)
|
||||
class FBMessage extends HiveObject implements FieldDebuggable {
|
||||
@HiveField(0)
|
||||
final String? senderId;
|
||||
@HiveField(1)
|
||||
final String? category;
|
||||
@HiveField(2)
|
||||
final String? collapseKey;
|
||||
@HiveField(3)
|
||||
final bool contentAvailable;
|
||||
@HiveField(4)
|
||||
final Map<String, String> data;
|
||||
@HiveField(5)
|
||||
final String? from;
|
||||
@HiveField(6)
|
||||
final String? messageId;
|
||||
@HiveField(7)
|
||||
final String? messageType;
|
||||
@HiveField(8)
|
||||
final bool mutableContent;
|
||||
@HiveField(9)
|
||||
final RemoteNotification? notification;
|
||||
@HiveField(10)
|
||||
final DateTime? sentTime;
|
||||
@HiveField(11)
|
||||
final String? threadId;
|
||||
@HiveField(12)
|
||||
final int? ttl;
|
||||
|
||||
@HiveField(20)
|
||||
final String? notificationAndroidChannelId;
|
||||
@HiveField(21)
|
||||
final String? notificationAndroidClickAction;
|
||||
@HiveField(22)
|
||||
final String? notificationAndroidColor;
|
||||
@HiveField(23)
|
||||
final int? notificationAndroidCount;
|
||||
@HiveField(24)
|
||||
final String? notificationAndroidImageUrl;
|
||||
@HiveField(25)
|
||||
final String? notificationAndroidLink;
|
||||
@HiveField(26)
|
||||
final AndroidNotificationPriority? notificationAndroidPriority;
|
||||
@HiveField(27)
|
||||
final String? notificationAndroidSmallIcon;
|
||||
@HiveField(28)
|
||||
final String? notificationAndroidSound;
|
||||
@HiveField(29)
|
||||
final String? notificationAndroidTicker;
|
||||
@HiveField(30)
|
||||
final AndroidNotificationVisibility? notificationAndroidVisibility;
|
||||
@HiveField(31)
|
||||
final String? notificationAndroidTag;
|
||||
|
||||
@HiveField(40)
|
||||
final String? notificationAppleBadge;
|
||||
@HiveField(41)
|
||||
final AppleNotificationSound? notificationAppleSound;
|
||||
@HiveField(42)
|
||||
final String? notificationAppleImageUrl;
|
||||
@HiveField(43)
|
||||
final String? notificationAppleSubtitle;
|
||||
@HiveField(44)
|
||||
final List<String>? notificationAppleSubtitleLocArgs;
|
||||
@HiveField(45)
|
||||
final String? notificationAppleSubtitleLocKey;
|
||||
|
||||
@HiveField(50)
|
||||
final String? notificationWebAnalyticsLabel;
|
||||
@HiveField(51)
|
||||
final String? notificationWebImage;
|
||||
@HiveField(52)
|
||||
final String? notificationWebLink;
|
||||
|
||||
@HiveField(60)
|
||||
final String? notificationTitle;
|
||||
@HiveField(61)
|
||||
final List<String>? notificationTitleLocArgs;
|
||||
@HiveField(62)
|
||||
final String? notificationTitleLocKey;
|
||||
@HiveField(63)
|
||||
final String? notificationBody;
|
||||
@HiveField(64)
|
||||
final List<String>? notificationBodyLocArgs;
|
||||
@HiveField(65)
|
||||
final String? notificationBodyLocKey;
|
||||
|
||||
FBMessage({
|
||||
required this.senderId,
|
||||
required this.category,
|
||||
required this.collapseKey,
|
||||
required this.contentAvailable,
|
||||
required this.data,
|
||||
required this.from,
|
||||
required this.messageId,
|
||||
required this.messageType,
|
||||
required this.mutableContent,
|
||||
required this.notification,
|
||||
required this.sentTime,
|
||||
required this.threadId,
|
||||
required this.ttl,
|
||||
required this.notificationAndroidChannelId,
|
||||
required this.notificationAndroidClickAction,
|
||||
required this.notificationAndroidColor,
|
||||
required this.notificationAndroidCount,
|
||||
required this.notificationAndroidImageUrl,
|
||||
required this.notificationAndroidLink,
|
||||
required this.notificationAndroidPriority,
|
||||
required this.notificationAndroidSmallIcon,
|
||||
required this.notificationAndroidSound,
|
||||
required this.notificationAndroidTicker,
|
||||
required this.notificationAndroidVisibility,
|
||||
required this.notificationAndroidTag,
|
||||
required this.notificationAppleBadge,
|
||||
required this.notificationAppleSound,
|
||||
required this.notificationAppleImageUrl,
|
||||
required this.notificationAppleSubtitle,
|
||||
required this.notificationAppleSubtitleLocArgs,
|
||||
required this.notificationAppleSubtitleLocKey,
|
||||
required this.notificationWebAnalyticsLabel,
|
||||
required this.notificationWebImage,
|
||||
required this.notificationWebLink,
|
||||
required this.notificationTitle,
|
||||
required this.notificationTitleLocArgs,
|
||||
required this.notificationTitleLocKey,
|
||||
required this.notificationBody,
|
||||
required this.notificationBodyLocArgs,
|
||||
required this.notificationBodyLocKey,
|
||||
});
|
||||
|
||||
FBMessage.fromRemoteMessage(RemoteMessage rmsg)
|
||||
: this.senderId = rmsg.senderId,
|
||||
this.category = rmsg.category,
|
||||
this.collapseKey = rmsg.collapseKey,
|
||||
this.contentAvailable = rmsg.contentAvailable,
|
||||
this.data = rmsg.data.map((key, value) => MapEntry(key, value?.toString() ?? '')),
|
||||
this.from = rmsg.from,
|
||||
this.messageId = rmsg.messageId,
|
||||
this.messageType = rmsg.messageType,
|
||||
this.mutableContent = rmsg.mutableContent,
|
||||
this.notification = rmsg.notification,
|
||||
this.sentTime = rmsg.sentTime,
|
||||
this.threadId = rmsg.threadId,
|
||||
this.ttl = rmsg.ttl,
|
||||
this.notificationAndroidChannelId = rmsg.notification?.android?.channelId,
|
||||
this.notificationAndroidClickAction = rmsg.notification?.android?.clickAction,
|
||||
this.notificationAndroidColor = rmsg.notification?.android?.color,
|
||||
this.notificationAndroidCount = rmsg.notification?.android?.count,
|
||||
this.notificationAndroidImageUrl = rmsg.notification?.android?.imageUrl,
|
||||
this.notificationAndroidLink = rmsg.notification?.android?.link,
|
||||
this.notificationAndroidPriority = rmsg.notification?.android?.priority,
|
||||
this.notificationAndroidSmallIcon = rmsg.notification?.android?.smallIcon,
|
||||
this.notificationAndroidSound = rmsg.notification?.android?.sound,
|
||||
this.notificationAndroidTicker = rmsg.notification?.android?.ticker,
|
||||
this.notificationAndroidVisibility = rmsg.notification?.android?.visibility,
|
||||
this.notificationAndroidTag = rmsg.notification?.android?.tag,
|
||||
this.notificationAppleBadge = rmsg.notification?.apple?.badge,
|
||||
this.notificationAppleSound = rmsg.notification?.apple?.sound,
|
||||
this.notificationAppleImageUrl = rmsg.notification?.apple?.imageUrl,
|
||||
this.notificationAppleSubtitle = rmsg.notification?.apple?.subtitle,
|
||||
this.notificationAppleSubtitleLocArgs = rmsg.notification?.apple?.subtitleLocArgs,
|
||||
this.notificationAppleSubtitleLocKey = rmsg.notification?.apple?.subtitleLocKey,
|
||||
this.notificationWebAnalyticsLabel = rmsg.notification?.web?.analyticsLabel,
|
||||
this.notificationWebImage = rmsg.notification?.web?.image,
|
||||
this.notificationWebLink = rmsg.notification?.web?.link,
|
||||
this.notificationTitle = rmsg.notification?.title,
|
||||
this.notificationTitleLocArgs = rmsg.notification?.titleLocArgs,
|
||||
this.notificationTitleLocKey = rmsg.notification?.titleLocKey,
|
||||
this.notificationBody = rmsg.notification?.body,
|
||||
this.notificationBodyLocArgs = rmsg.notification?.bodyLocArgs,
|
||||
this.notificationBodyLocKey = rmsg.notification?.bodyLocKey {}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'FBMessage[${this.messageId ?? 'NULL'}]';
|
||||
}
|
||||
|
||||
List<(String, String)> debugFieldList() {
|
||||
return [
|
||||
('senderId', this.senderId ?? ''),
|
||||
('category', this.category ?? ''),
|
||||
('collapseKey', this.collapseKey ?? ''),
|
||||
('contentAvailable', this.contentAvailable.toString()),
|
||||
('data', this.data.toString()),
|
||||
('from', this.from ?? ''),
|
||||
('messageId', this.messageId ?? ''),
|
||||
('messageType', this.messageType ?? ''),
|
||||
('mutableContent', this.mutableContent.toString()),
|
||||
('notification', this.notification?.toString() ?? ''),
|
||||
('sentTime', this.sentTime?.toString() ?? ''),
|
||||
('threadId', this.threadId ?? ''),
|
||||
('ttl', this.ttl?.toString() ?? ''),
|
||||
('notification.Android.ChannelId', this.notificationAndroidChannelId ?? ''),
|
||||
('notification.Android.ClickAction', this.notificationAndroidClickAction ?? ''),
|
||||
('notification.Android.Color', this.notificationAndroidColor ?? ''),
|
||||
('notification.Android.Count', this.notificationAndroidCount?.toString() ?? ''),
|
||||
('notification.Android.ImageUrl', this.notificationAndroidImageUrl ?? ''),
|
||||
('notification.Android.Link', this.notificationAndroidLink ?? ''),
|
||||
('notification.Android.Priority', this.notificationAndroidPriority?.toString() ?? ''),
|
||||
('notification.Android.SmallIcon', this.notificationAndroidSmallIcon ?? ''),
|
||||
('notification.Android.Sound', this.notificationAndroidSound ?? ''),
|
||||
('notification.Android.Ticker', this.notificationAndroidTicker ?? ''),
|
||||
('notification.Android.Visibility', this.notificationAndroidVisibility?.toString() ?? ''),
|
||||
('notification.Android.Tag', this.notificationAndroidTag ?? ''),
|
||||
('notification.Apple.Badge', this.notificationAppleBadge ?? ''),
|
||||
('notification.Apple.Sound', this.notificationAppleSound?.toString() ?? ''),
|
||||
('notification.Apple.ImageUrl', this.notificationAppleImageUrl ?? ''),
|
||||
('notification.Apple.Subtitle', this.notificationAppleSubtitle ?? ''),
|
||||
('notification.Apple.SubtitleLocArgs', this.notificationAppleSubtitleLocArgs?.toString() ?? ''),
|
||||
('notification.Apple.SubtitleLocKey', this.notificationAppleSubtitleLocKey ?? ''),
|
||||
('notification.Web.AnalyticsLabel', this.notificationWebAnalyticsLabel ?? ''),
|
||||
('notification.Web.Image', this.notificationWebImage ?? ''),
|
||||
('notification.Web.Link', this.notificationWebLink ?? ''),
|
||||
('notification.Title', this.notificationTitle ?? ''),
|
||||
('notification.TitleLocArgs', this.notificationTitleLocArgs?.toString() ?? ''),
|
||||
('notification.TitleLocKey', this.notificationTitleLocKey ?? ''),
|
||||
('notification.Body', this.notificationBody ?? ''),
|
||||
('notification.BodyLocArgs', this.notificationBodyLocArgs?.toString() ?? ''),
|
||||
('notification.BodyLocKey', this.notificationBodyLocKey ?? ''),
|
||||
];
|
||||
}
|
||||
}
|
159
flutter/lib/state/fb_message.g.dart
Normal file
@ -0,0 +1,159 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'fb_message.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class FBMessageAdapter extends TypeAdapter<FBMessage> {
|
||||
@override
|
||||
final int typeId = 106;
|
||||
|
||||
@override
|
||||
FBMessage read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return FBMessage(
|
||||
senderId: fields[0] as String?,
|
||||
category: fields[1] as String?,
|
||||
collapseKey: fields[2] as String?,
|
||||
contentAvailable: fields[3] as bool,
|
||||
data: (fields[4] as Map).cast<String, String>(),
|
||||
from: fields[5] as String?,
|
||||
messageId: fields[6] as String?,
|
||||
messageType: fields[7] as String?,
|
||||
mutableContent: fields[8] as bool,
|
||||
notification: fields[9] as RemoteNotification?,
|
||||
sentTime: fields[10] as DateTime?,
|
||||
threadId: fields[11] as String?,
|
||||
ttl: fields[12] as int?,
|
||||
notificationAndroidChannelId: fields[20] as String?,
|
||||
notificationAndroidClickAction: fields[21] as String?,
|
||||
notificationAndroidColor: fields[22] as String?,
|
||||
notificationAndroidCount: fields[23] as int?,
|
||||
notificationAndroidImageUrl: fields[24] as String?,
|
||||
notificationAndroidLink: fields[25] as String?,
|
||||
notificationAndroidPriority: fields[26] as AndroidNotificationPriority?,
|
||||
notificationAndroidSmallIcon: fields[27] as String?,
|
||||
notificationAndroidSound: fields[28] as String?,
|
||||
notificationAndroidTicker: fields[29] as String?,
|
||||
notificationAndroidVisibility:
|
||||
fields[30] as AndroidNotificationVisibility?,
|
||||
notificationAndroidTag: fields[31] as String?,
|
||||
notificationAppleBadge: fields[40] as String?,
|
||||
notificationAppleSound: fields[41] as AppleNotificationSound?,
|
||||
notificationAppleImageUrl: fields[42] as String?,
|
||||
notificationAppleSubtitle: fields[43] as String?,
|
||||
notificationAppleSubtitleLocArgs: (fields[44] as List?)?.cast<String>(),
|
||||
notificationAppleSubtitleLocKey: fields[45] as String?,
|
||||
notificationWebAnalyticsLabel: fields[50] as String?,
|
||||
notificationWebImage: fields[51] as String?,
|
||||
notificationWebLink: fields[52] as String?,
|
||||
notificationTitle: fields[60] as String?,
|
||||
notificationTitleLocArgs: (fields[61] as List?)?.cast<String>(),
|
||||
notificationTitleLocKey: fields[62] as String?,
|
||||
notificationBody: fields[63] as String?,
|
||||
notificationBodyLocArgs: (fields[64] as List?)?.cast<String>(),
|
||||
notificationBodyLocKey: fields[65] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, FBMessage obj) {
|
||||
writer
|
||||
..writeByte(40)
|
||||
..writeByte(0)
|
||||
..write(obj.senderId)
|
||||
..writeByte(1)
|
||||
..write(obj.category)
|
||||
..writeByte(2)
|
||||
..write(obj.collapseKey)
|
||||
..writeByte(3)
|
||||
..write(obj.contentAvailable)
|
||||
..writeByte(4)
|
||||
..write(obj.data)
|
||||
..writeByte(5)
|
||||
..write(obj.from)
|
||||
..writeByte(6)
|
||||
..write(obj.messageId)
|
||||
..writeByte(7)
|
||||
..write(obj.messageType)
|
||||
..writeByte(8)
|
||||
..write(obj.mutableContent)
|
||||
..writeByte(9)
|
||||
..write(obj.notification)
|
||||
..writeByte(10)
|
||||
..write(obj.sentTime)
|
||||
..writeByte(11)
|
||||
..write(obj.threadId)
|
||||
..writeByte(12)
|
||||
..write(obj.ttl)
|
||||
..writeByte(20)
|
||||
..write(obj.notificationAndroidChannelId)
|
||||
..writeByte(21)
|
||||
..write(obj.notificationAndroidClickAction)
|
||||
..writeByte(22)
|
||||
..write(obj.notificationAndroidColor)
|
||||
..writeByte(23)
|
||||
..write(obj.notificationAndroidCount)
|
||||
..writeByte(24)
|
||||
..write(obj.notificationAndroidImageUrl)
|
||||
..writeByte(25)
|
||||
..write(obj.notificationAndroidLink)
|
||||
..writeByte(26)
|
||||
..write(obj.notificationAndroidPriority)
|
||||
..writeByte(27)
|
||||
..write(obj.notificationAndroidSmallIcon)
|
||||
..writeByte(28)
|
||||
..write(obj.notificationAndroidSound)
|
||||
..writeByte(29)
|
||||
..write(obj.notificationAndroidTicker)
|
||||
..writeByte(30)
|
||||
..write(obj.notificationAndroidVisibility)
|
||||
..writeByte(31)
|
||||
..write(obj.notificationAndroidTag)
|
||||
..writeByte(40)
|
||||
..write(obj.notificationAppleBadge)
|
||||
..writeByte(41)
|
||||
..write(obj.notificationAppleSound)
|
||||
..writeByte(42)
|
||||
..write(obj.notificationAppleImageUrl)
|
||||
..writeByte(43)
|
||||
..write(obj.notificationAppleSubtitle)
|
||||
..writeByte(44)
|
||||
..write(obj.notificationAppleSubtitleLocArgs)
|
||||
..writeByte(45)
|
||||
..write(obj.notificationAppleSubtitleLocKey)
|
||||
..writeByte(50)
|
||||
..write(obj.notificationWebAnalyticsLabel)
|
||||
..writeByte(51)
|
||||
..write(obj.notificationWebImage)
|
||||
..writeByte(52)
|
||||
..write(obj.notificationWebLink)
|
||||
..writeByte(60)
|
||||
..write(obj.notificationTitle)
|
||||
..writeByte(61)
|
||||
..write(obj.notificationTitleLocArgs)
|
||||
..writeByte(62)
|
||||
..write(obj.notificationTitleLocKey)
|
||||
..writeByte(63)
|
||||
..write(obj.notificationBody)
|
||||
..writeByte(64)
|
||||
..write(obj.notificationBodyLocArgs)
|
||||
..writeByte(65)
|
||||
..write(obj.notificationBodyLocKey);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is FBMessageAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
@ -6,6 +6,8 @@ import 'package:xid/xid.dart';
|
||||
part 'request_log.g.dart';
|
||||
|
||||
class RequestLog {
|
||||
//TODO max size, auto clear old
|
||||
|
||||
static void addRequestException(String name, DateTime tStart, String method, Uri uri, String reqbody, Map<String, String> reqheaders, dynamic e, StackTrace trace) {
|
||||
Hive.box<SCNRequest>('scn-requests').add(SCNRequest(
|
||||
id: Xid().toString(),
|
||||
|
18
flutter/lib/types/immediate_future.dart
Normal file
@ -0,0 +1,18 @@
|
||||
// This class is useful togther with FutureBuilder
|
||||
// Unfortunately Future.value(x) in FutureBuilder always results in one frame were snapshot.connectionState is waiting
|
||||
// Whit way we can set the ImmediateFuture.value directly and circumvent that.
|
||||
|
||||
class ImmediateFuture<T> {
|
||||
final Future<T> future;
|
||||
final T? value;
|
||||
|
||||
ImmediateFuture(this.future, this.value);
|
||||
|
||||
ImmediateFuture.ofFuture(Future<T> v)
|
||||
: future = v,
|
||||
value = null;
|
||||
|
||||
ImmediateFuture.ofValue(T v)
|
||||
: future = Future.value(v),
|
||||
value = v;
|
||||
}
|
@ -1 +1,52 @@
|
||||
class Navi {}
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
||||
|
||||
class Navi {
|
||||
static final SCNRouteObserver routeObserver = SCNRouteObserver();
|
||||
static final RouteObserver<ModalRoute<void>> modalRouteObserver = RouteObserver<ModalRoute<void>>();
|
||||
|
||||
static void push<T extends Widget>(BuildContext context, T Function() builder) {
|
||||
Provider.of<AppBarState>(context, listen: false).setLoadingIndeterminate(false);
|
||||
|
||||
Navigator.push(context, MaterialPageRoute<T>(builder: (context) => builder()));
|
||||
}
|
||||
|
||||
static void popToRoot(BuildContext context) {
|
||||
Provider.of<AppBarState>(context, listen: false).setLoadingIndeterminate(false);
|
||||
|
||||
Navigator.popUntil(context, (route) => route.isFirst);
|
||||
}
|
||||
}
|
||||
|
||||
class SCNRouteObserver extends RouteObserver<PageRoute<dynamic>> {
|
||||
@override
|
||||
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
|
||||
super.didPush(route, previousRoute);
|
||||
if (route is PageRoute) {
|
||||
AppBarState().setLoadingIndeterminate(false);
|
||||
|
||||
print('[SCNRouteObserver] .didPush()');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
|
||||
super.didReplace(newRoute: newRoute, oldRoute: oldRoute);
|
||||
if (newRoute is PageRoute) {
|
||||
AppBarState().setLoadingIndeterminate(false);
|
||||
|
||||
print('[SCNRouteObserver] .didReplace()');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
|
||||
super.didPop(route, previousRoute);
|
||||
if (previousRoute is PageRoute && route is PageRoute) {
|
||||
AppBarState().setLoadingIndeterminate(false);
|
||||
|
||||
print('[SCNRouteObserver] .didPop()');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,68 +1,68 @@
|
||||
{
|
||||
"images" : [
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "xcode"
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_16.png",
|
||||
"scale" : "1x"
|
||||
"size": "16x16",
|
||||
"idiom": "mac",
|
||||
"filename": "app_icon_16.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"size" : "16x16",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_32.png",
|
||||
"scale" : "2x"
|
||||
"size": "16x16",
|
||||
"idiom": "mac",
|
||||
"filename": "app_icon_32.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_32.png",
|
||||
"scale" : "1x"
|
||||
"size": "32x32",
|
||||
"idiom": "mac",
|
||||
"filename": "app_icon_32.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"size" : "32x32",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_64.png",
|
||||
"scale" : "2x"
|
||||
"size": "32x32",
|
||||
"idiom": "mac",
|
||||
"filename": "app_icon_64.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_128.png",
|
||||
"scale" : "1x"
|
||||
"size": "128x128",
|
||||
"idiom": "mac",
|
||||
"filename": "app_icon_128.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"size" : "128x128",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_256.png",
|
||||
"scale" : "2x"
|
||||
"size": "128x128",
|
||||
"idiom": "mac",
|
||||
"filename": "app_icon_256.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_256.png",
|
||||
"scale" : "1x"
|
||||
"size": "256x256",
|
||||
"idiom": "mac",
|
||||
"filename": "app_icon_256.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"size" : "256x256",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_512.png",
|
||||
"scale" : "2x"
|
||||
"size": "256x256",
|
||||
"idiom": "mac",
|
||||
"filename": "app_icon_512.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_512.png",
|
||||
"scale" : "1x"
|
||||
"size": "512x512",
|
||||
"idiom": "mac",
|
||||
"filename": "app_icon_512.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"size" : "512x512",
|
||||
"idiom" : "mac",
|
||||
"filename" : "app_icon_1024.png",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"size": "512x512",
|
||||
"idiom": "mac",
|
||||
"filename": "app_icon_1024.png",
|
||||
"scale": "2x"
|
||||
}
|
||||
]
|
||||
}
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 178 KiB |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 520 B After Width: | Height: | Size: 727 B |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 4.5 KiB |
@ -25,6 +25,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.4.1"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.6.1"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -129,6 +137,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
cli_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cli_util
|
||||
sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.1"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -253,18 +269,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_platform_interface
|
||||
sha256: c437ae5d17e6b5cc7981cf6fd458a5db4d12979905f9aafd1fea930428a9fe63
|
||||
sha256: "1003a5a03a61fc9a22ef49f37cbcb9e46c86313a7b2e7029b9390cf8c6fc32cb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.0"
|
||||
version: "5.1.0"
|
||||
firebase_core_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_web
|
||||
sha256: "43d9e951ac52b87ae9cc38ecdcca1e8fa7b52a1dd26a96085ba41ce5108db8e9"
|
||||
sha256: "6643fe3dbd021e6ccfb751f7882b39df355708afbdeb4130fc50f9305a9d1a3d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.17.0"
|
||||
version: "2.17.2"
|
||||
firebase_messaging:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -302,6 +318,14 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_launcher_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_launcher_icons
|
||||
sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.13.1"
|
||||
flutter_lazy_indexed_stack:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -423,6 +447,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
image:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.0"
|
||||
infinite_scroll_pagination:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -631,14 +663,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0+3"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.2"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
|
||||
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.4"
|
||||
version: "3.1.5"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -675,10 +715,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pubspec_parse
|
||||
sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367
|
||||
sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.3"
|
||||
version: "1.3.0"
|
||||
qr:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -904,10 +944,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e"
|
||||
sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.6"
|
||||
version: "6.3.0"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1044,6 +1084,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.5.0"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1,5 +1,5 @@
|
||||
name: simplecloudnotifier
|
||||
description: "A new Flutter project."
|
||||
description: "Receive push messages"
|
||||
publish_to: 'none'
|
||||
|
||||
version: 2.0.0+100
|
||||
@ -11,6 +11,8 @@ dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
flutter_launcher_icons: "^0.13.1"
|
||||
|
||||
font_awesome_flutter: '>= 4.7.0'
|
||||
cupertino_icons: ^1.0.2
|
||||
http: ^1.2.0
|
||||
|
Before Width: | Height: | Size: 917 B After Width: | Height: | Size: 727 B |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 71 KiB |
@ -3,8 +3,8 @@
|
||||
"short_name": "simplecloudnotifier",
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"background_color": "#0175C2",
|
||||
"theme_color": "#0175C2",
|
||||
"background_color": "#hexcode",
|
||||
"theme_color": "#hexcode",
|
||||
"description": "A new Flutter project.",
|
||||
"orientation": "portrait-primary",
|
||||
"prefer_related_applications": false,
|
||||
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 3.2 KiB |
@ -11,8 +11,10 @@ import (
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@ -55,30 +57,43 @@ func (fb FirebaseConnector) SendNotification(ctx context.Context, user models.Us
|
||||
|
||||
uri := "https://fcm.googleapis.com/v1/projects/" + fb.fbProject + "/messages:send"
|
||||
|
||||
jsonBody := gin.H{
|
||||
"token": client.FCMToken,
|
||||
}
|
||||
jsonBody := gin.H{}
|
||||
|
||||
if client.Type == models.ClientTypeIOS {
|
||||
jsonBody["notification"] = gin.H{
|
||||
jsonBody = gin.H{
|
||||
"token": client.FCMToken,
|
||||
"notification": gin.H{
|
||||
"title": msg.Title,
|
||||
"body": msg.ShortContent(),
|
||||
},
|
||||
"apns": gin.H{},
|
||||
}
|
||||
jsonBody["apns"] = gin.H{}
|
||||
} else if client.Type == models.ClientTypeAndroid {
|
||||
jsonBody["android"] = gin.H{
|
||||
jsonBody = gin.H{
|
||||
"token": client.FCMToken,
|
||||
"android": gin.H{
|
||||
"priority": "high",
|
||||
"fcm_options": gin.H{},
|
||||
},
|
||||
"data": gin.H{
|
||||
"scn_msg_id": msg.MessageID.String(),
|
||||
"usr_msg_id": langext.Coalesce(msg.UserMessageID, ""),
|
||||
"client_id": client.ClientID.String(),
|
||||
"timestamp": strconv.FormatInt(msg.Timestamp().Unix(), 10),
|
||||
"priority": strconv.Itoa(msg.Priority),
|
||||
"trimmed": langext.Conditional(msg.NeedsTrim(), "true", "false"),
|
||||
"title": msg.Title,
|
||||
"channel": channel.DisplayName,
|
||||
"body": langext.Coalesce(msg.TrimmedContent(), ""),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
jsonBody = gin.H{
|
||||
"token": client.FCMToken,
|
||||
"notification": gin.H{
|
||||
"event_time": msg.Timestamp().Format(time.RFC3339),
|
||||
"title": msg.FormatNotificationTitle(user, channel),
|
||||
"body": msg.ShortContent(),
|
||||
},
|
||||
"fcm_options": gin.H{},
|
||||
}
|
||||
} else {
|
||||
jsonBody["notification"] = gin.H{
|
||||
"title": msg.FormatNotificationTitle(user, channel),
|
||||
"body": msg.ShortContent(),
|
||||
}
|
||||
}
|
||||
|
||||
|