Merge branch 'flutter_app'
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
|
|
||||||
run:
|
run:
|
||||||
dart run build_runner build
|
flutter pub run build_runner build
|
||||||
flutter run
|
flutter run
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@ -11,8 +11,11 @@ fix:
|
|||||||
dart fix --apply
|
dart fix --apply
|
||||||
|
|
||||||
gen:
|
gen:
|
||||||
dart run build_runner build
|
flutter pub run build_runner build
|
||||||
|
|
||||||
autoreload:
|
autoreload:
|
||||||
@# run `make run` in another terminal (or another variant of flutter run)
|
@# run `make run` in another terminal (or another variant of flutter run)
|
||||||
@_utils/autoreload.sh
|
@_utils/autoreload.sh
|
||||||
|
|
||||||
|
icons:
|
||||||
|
flutter pub run flutter_launcher_icons -f "flutter_launcher_icons.yaml"
|
@ -9,7 +9,7 @@
|
|||||||
<application
|
<application
|
||||||
android:label="simplecloudnotifier"
|
android:label="simplecloudnotifier"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/launcher_icon">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
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 {
|
static Future<Client> addClient(TokenSource auth, String fcmToken, String agentModel, String agentVersion, String? name, String clientType) async {
|
||||||
return await _request(
|
return await _request(
|
||||||
name: 'addClient',
|
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 {
|
static Future<(String, List<Message>)> getMessageList(TokenSource auth, String pageToken, {int? pageSize, List<String>? channelIDs}) async {
|
||||||
return await _request(
|
return await _request(
|
||||||
name: 'getMessageList',
|
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 {
|
static Future<KeyToken> getKeyTokenByToken(String userid, String token) async {
|
||||||
return await _request(
|
return await _request(
|
||||||
name: 'getCurrentKeyToken',
|
name: 'getCurrentKeyToken',
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:simplecloudnotifier/components/layout/app_bar_progress_indicator.dart';
|
||||||
import 'package:simplecloudnotifier/pages/debug/debug_main.dart';
|
import 'package:simplecloudnotifier/pages/debug/debug_main.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_theme.dart';
|
import 'package:simplecloudnotifier/state/app_theme.dart';
|
||||||
|
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||||
|
|
||||||
class SCNAppBar extends StatelessWidget implements PreferredSizeWidget {
|
class SCNAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
const SCNAppBar({
|
const SCNAppBar({
|
||||||
@ -26,6 +29,16 @@ class SCNAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var actions = <Widget>[];
|
var actions = <Widget>[];
|
||||||
|
|
||||||
|
if (showDebug) {
|
||||||
|
actions.add(IconButton(
|
||||||
|
icon: const Icon(FontAwesomeIcons.solidSpiderBlackWidow),
|
||||||
|
tooltip: 'Debug',
|
||||||
|
onPressed: () {
|
||||||
|
Navi.push(context, () => DebugMainPage());
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
if (showThemeSwitch) {
|
if (showThemeSwitch) {
|
||||||
actions.add(Consumer<AppTheme>(
|
actions.add(Consumer<AppTheme>(
|
||||||
builder: (context, appTheme, child) => IconButton(
|
builder: (context, appTheme, child) => IconButton(
|
||||||
@ -35,19 +48,16 @@ class SCNAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
),
|
),
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
actions.add(SizedBox.square(dimension: 40));
|
actions.add(Visibility(
|
||||||
}
|
visible: false,
|
||||||
|
maintainSize: true,
|
||||||
if (showDebug) {
|
maintainAnimation: true,
|
||||||
actions.add(IconButton(
|
maintainState: true,
|
||||||
icon: const Icon(FontAwesomeIcons.solidSpiderBlackWidow),
|
child: IconButton(
|
||||||
tooltip: 'Debug',
|
icon: const Icon(FontAwesomeIcons.square),
|
||||||
onPressed: () {
|
onPressed: () {/*TODO*/},
|
||||||
Navigator.push(context, MaterialPageRoute<DebugMainPage>(builder: (context) => DebugMainPage()));
|
),
|
||||||
},
|
|
||||||
));
|
));
|
||||||
} else {
|
|
||||||
actions.add(SizedBox.square(dimension: 40));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showSearch) {
|
if (showSearch) {
|
||||||
@ -63,13 +73,26 @@ class SCNAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
onPressed: onShare ?? () {},
|
onPressed: onShare ?? () {},
|
||||||
));
|
));
|
||||||
} else {
|
} 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(
|
return AppBar(
|
||||||
title: Text(title ?? 'Simple Cloud Notifier 2.0'),
|
title: Text(title ?? 'Simple Cloud Notifier 2.0'),
|
||||||
actions: actions,
|
actions: actions,
|
||||||
backgroundColor: Theme.of(context).secondaryHeaderColor,
|
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:provider/provider.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||||
|
import 'package:simplecloudnotifier/models/channel.dart';
|
||||||
import 'package:simplecloudnotifier/models/client.dart';
|
import 'package:simplecloudnotifier/models/client.dart';
|
||||||
|
import 'package:simplecloudnotifier/models/message.dart';
|
||||||
import 'package:simplecloudnotifier/nav_layout.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/app_theme.dart';
|
||||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||||
|
import 'package:simplecloudnotifier/state/fb_message.dart';
|
||||||
import 'package:simplecloudnotifier/state/globals.dart';
|
import 'package:simplecloudnotifier/state/globals.dart';
|
||||||
import 'package:simplecloudnotifier/state/request_log.dart';
|
import 'package:simplecloudnotifier/state/request_log.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||||
import 'package:firebase_core/firebase_core.dart';
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
|
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||||
import 'package:toastification/toastification.dart';
|
import 'package:toastification/toastification.dart';
|
||||||
import 'firebase_options.dart';
|
import 'firebase_options.dart';
|
||||||
|
|
||||||
@ -34,6 +39,9 @@ void main() async {
|
|||||||
Hive.registerAdapter(SCNRequestAdapter());
|
Hive.registerAdapter(SCNRequestAdapter());
|
||||||
Hive.registerAdapter(SCNLogAdapter());
|
Hive.registerAdapter(SCNLogAdapter());
|
||||||
Hive.registerAdapter(SCNLogLevelAdapter());
|
Hive.registerAdapter(SCNLogLevelAdapter());
|
||||||
|
Hive.registerAdapter(MessageAdapter());
|
||||||
|
Hive.registerAdapter(ChannelAdapter());
|
||||||
|
Hive.registerAdapter(FBMessageAdapter());
|
||||||
|
|
||||||
print('[INIT] Load Hive<scn-requests>...');
|
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);
|
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...');
|
print('[INIT] Load AppAuth...');
|
||||||
|
|
||||||
final appAuth = AppAuth(); // ensure UserAccount is loaded
|
final appAuth = AppAuth(); // ensure UserAccount is loaded
|
||||||
@ -102,6 +140,9 @@ void main() async {
|
|||||||
} catch (exc, trace) {
|
} catch (exc, trace) {
|
||||||
ApplicationLog.error('Failed to get+set firebase token: ' + exc.toString(), trace: trace);
|
ApplicationLog.error('Failed to get+set firebase token: ' + exc.toString(), trace: trace);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FirebaseMessaging.onBackgroundMessage(_onBackgroundMessage);
|
||||||
|
FirebaseMessaging.onMessage.listen(_onForegroundMessage);
|
||||||
} else {
|
} else {
|
||||||
print('[INIT] Skip Firebase init (Platform == Linux)...');
|
print('[INIT] Skip Firebase init (Platform == Linux)...');
|
||||||
}
|
}
|
||||||
@ -113,12 +154,40 @@ void main() async {
|
|||||||
providers: [
|
providers: [
|
||||||
ChangeNotifierProvider(create: (context) => AppAuth(), lazy: false),
|
ChangeNotifierProvider(create: (context) => AppAuth(), lazy: false),
|
||||||
ChangeNotifierProvider(create: (context) => AppTheme(), lazy: false),
|
ChangeNotifierProvider(create: (context) => AppTheme(), lazy: false),
|
||||||
|
ChangeNotifierProvider(create: (context) => AppBarState(), lazy: false),
|
||||||
],
|
],
|
||||||
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 {
|
void setFirebaseToken(String fcmToken) async {
|
||||||
final acc = AppAuth();
|
final acc = AppAuth();
|
||||||
|
|
||||||
@ -132,7 +201,7 @@ void setFirebaseToken(String fcmToken) async {
|
|||||||
|
|
||||||
Client? client;
|
Client? client;
|
||||||
try {
|
try {
|
||||||
client = await acc.loadClient(force: true);
|
client = await acc.loadClient(forceIfOlder: Duration(seconds: 60));
|
||||||
} catch (exc, trace) {
|
} catch (exc, trace) {
|
||||||
ApplicationLog.error('Failed to get client: ' + exc.toString(), trace: trace);
|
ApplicationLog.error('Failed to get client: ' + exc.toString(), trace: trace);
|
||||||
return;
|
return;
|
||||||
@ -155,28 +224,18 @@ void setFirebaseToken(String fcmToken) async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SCNApp extends StatelessWidget {
|
Future<void> _onBackgroundMessage(RemoteMessage message) async {
|
||||||
const SCNApp({super.key});
|
await _receiveMessage(message, false);
|
||||||
|
}
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
void _onForegroundMessage(RemoteMessage message) {
|
||||||
return ToastificationWrapper(
|
_receiveMessage(message, true);
|
||||||
config: ToastificationConfig(
|
}
|
||||||
itemWidth: 440,
|
|
||||||
marginBuilder: (alignment) => EdgeInsets.symmetric(vertical: 64),
|
Future<void> _receiveMessage(RemoteMessage message, bool foreground) async {
|
||||||
animationDuration: Duration(milliseconds: 200),
|
// ensure init
|
||||||
),
|
Hive.openBox<SCNLog>('scn-logs');
|
||||||
child: Consumer<AppTheme>(
|
|
||||||
builder: (context, appTheme, child) => MaterialApp(
|
ApplicationLog.info('Received FB message (${foreground ? 'foreground' : 'background'}): ${message.messageId ?? 'NULL'}');
|
||||||
title: 'SimpleCloudNotifier',
|
FBMessageLog.insert(message);
|
||||||
theme: ThemeData(
|
|
||||||
//TODO color settings
|
|
||||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue, brightness: appTheme.darkMode ? Brightness.dark : Brightness.light),
|
|
||||||
useMaterial3: true,
|
|
||||||
),
|
|
||||||
home: SCNNavLayout(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,32 @@
|
|||||||
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:simplecloudnotifier/models/subscription.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;
|
final String channelID;
|
||||||
|
|
||||||
|
@HiveField(10)
|
||||||
final String ownerUserID;
|
final String ownerUserID;
|
||||||
|
@HiveField(11)
|
||||||
final String internalName;
|
final String internalName;
|
||||||
|
@HiveField(12)
|
||||||
final String displayName;
|
final String displayName;
|
||||||
|
@HiveField(13)
|
||||||
final String? descriptionName;
|
final String? descriptionName;
|
||||||
|
@HiveField(14)
|
||||||
final String? subscribeKey;
|
final String? subscribeKey;
|
||||||
|
@HiveField(15)
|
||||||
final String timestampCreated;
|
final String timestampCreated;
|
||||||
|
@HiveField(16)
|
||||||
final String? timestampLastSent;
|
final String? timestampLastSent;
|
||||||
|
@HiveField(17)
|
||||||
final int messagesSent;
|
final int messagesSent;
|
||||||
|
|
||||||
const Channel({
|
Channel({
|
||||||
required this.channelID,
|
required this.channelID,
|
||||||
required this.ownerUserID,
|
required this.ownerUserID,
|
||||||
required this.internalName,
|
required this.internalName,
|
||||||
@ -36,6 +51,25 @@ class Channel {
|
|||||||
messagesSent: json['messages_sent'] as int,
|
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 {
|
class ChannelWithSubscription {
|
||||||
@ -58,3 +92,29 @@ class ChannelWithSubscription {
|
|||||||
return jsonArr.map<ChannelWithSubscription>((e) => ChannelWithSubscription.fromJson(e as Map<String, dynamic>)).toList();
|
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();
|
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;
|
final String messageID;
|
||||||
|
|
||||||
|
@HiveField(10)
|
||||||
final String senderUserID;
|
final String senderUserID;
|
||||||
|
@HiveField(11)
|
||||||
final String channelInternalName;
|
final String channelInternalName;
|
||||||
|
@HiveField(12)
|
||||||
final String channelID;
|
final String channelID;
|
||||||
|
@HiveField(13)
|
||||||
final String? senderName;
|
final String? senderName;
|
||||||
|
@HiveField(14)
|
||||||
final String senderIP;
|
final String senderIP;
|
||||||
|
@HiveField(15)
|
||||||
final String timestamp;
|
final String timestamp;
|
||||||
|
@HiveField(16)
|
||||||
final String title;
|
final String title;
|
||||||
|
@HiveField(17)
|
||||||
final String? content;
|
final String? content;
|
||||||
|
@HiveField(18)
|
||||||
final int priority;
|
final int priority;
|
||||||
|
@HiveField(19)
|
||||||
final String? userMessageID;
|
final String? userMessageID;
|
||||||
|
@HiveField(20)
|
||||||
final String usedKeyID;
|
final String usedKeyID;
|
||||||
|
@HiveField(21)
|
||||||
final bool trimmed;
|
final bool trimmed;
|
||||||
|
|
||||||
const Message({
|
Message({
|
||||||
required this.messageID,
|
required this.messageID,
|
||||||
required this.senderUserID,
|
required this.senderUserID,
|
||||||
required this.channelInternalName,
|
required this.channelInternalName,
|
||||||
@ -54,4 +74,27 @@ class Message {
|
|||||||
|
|
||||||
return (npt, messages);
|
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(
|
body: IndexedStack(
|
||||||
children: [
|
children: [
|
||||||
ExcludeFocus(excluding: _selectedIndex != 0, child: MessageListPage()),
|
ExcludeFocus(excluding: _selectedIndex != 0, child: MessageListPage(isVisiblePage: _selectedIndex == 0)),
|
||||||
ExcludeFocus(excluding: _selectedIndex != 1, child: ChannelRootPage()),
|
ExcludeFocus(excluding: _selectedIndex != 1, child: ChannelRootPage(isVisiblePage: _selectedIndex == 1)),
|
||||||
ExcludeFocus(excluding: _selectedIndex != 2, child: AccountRootPage()),
|
ExcludeFocus(excluding: _selectedIndex != 2, child: AccountRootPage(isVisiblePage: _selectedIndex == 2)),
|
||||||
ExcludeFocus(excluding: _selectedIndex != 3, child: SettingsRootPage()),
|
ExcludeFocus(excluding: _selectedIndex != 3, child: SettingsRootPage(isVisiblePage: _selectedIndex == 3)),
|
||||||
ExcludeFocus(excluding: _selectedIndex != 4, child: SendRootPage()),
|
ExcludeFocus(excluding: _selectedIndex != 4, child: SendRootPage(isVisiblePage: _selectedIndex == 4)),
|
||||||
],
|
],
|
||||||
index: _selectedIndex,
|
index: _selectedIndex,
|
||||||
),
|
),
|
||||||
|
@ -7,47 +7,81 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||||
import 'package:simplecloudnotifier/models/user.dart';
|
import 'package:simplecloudnotifier/models/user.dart';
|
||||||
import 'package:simplecloudnotifier/pages/account/login.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/application_log.dart';
|
||||||
import 'package:simplecloudnotifier/state/globals.dart';
|
import 'package:simplecloudnotifier/state/globals.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_auth.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/toaster.dart';
|
||||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
import 'package:simplecloudnotifier/utils/ui.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class AccountRootPage extends StatefulWidget {
|
class AccountRootPage extends StatefulWidget {
|
||||||
const AccountRootPage({super.key});
|
const AccountRootPage({super.key, required this.isVisiblePage});
|
||||||
|
|
||||||
|
final bool isVisiblePage;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<AccountRootPage> createState() => _AccountRootPageState();
|
State<AccountRootPage> createState() => _AccountRootPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AccountRootPageState extends State<AccountRootPage> {
|
class _AccountRootPageState extends State<AccountRootPage> {
|
||||||
late Future<int>? futureSubscriptionCount;
|
late ImmediateFuture<int>? futureSubscriptionCount;
|
||||||
late Future<int>? futureClientCount;
|
late ImmediateFuture<int>? futureClientCount;
|
||||||
late Future<int>? futureKeyCount;
|
late ImmediateFuture<int>? futureKeyCount;
|
||||||
late Future<int>? futureChannelAllCount;
|
late ImmediateFuture<int>? futureChannelAllCount;
|
||||||
late Future<int>? futureChannelSubscribedCount;
|
late ImmediateFuture<int>? futureChannelSubscribedCount;
|
||||||
|
late ImmediateFuture<User>? futureUser;
|
||||||
|
|
||||||
late AppAuth userAcc;
|
late AppAuth userAcc;
|
||||||
|
|
||||||
bool loading = false;
|
bool loading = false;
|
||||||
|
|
||||||
|
bool _isInitialized = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
userAcc = Provider.of<AppAuth>(context, listen: false);
|
userAcc = Provider.of<AppAuth>(context, listen: false);
|
||||||
userAcc.addListener(_onAuthStateChanged);
|
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();
|
_onAuthStateChanged();
|
||||||
|
_isInitialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
ApplicationLog.debug('AccountRootPage::dispose');
|
||||||
userAcc.removeListener(_onAuthStateChanged);
|
userAcc.removeListener(_onAuthStateChanged);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onAuthStateChanged() {
|
void _onAuthStateChanged() {
|
||||||
|
ApplicationLog.debug('AccountRootPage::_onAuthStateChanged');
|
||||||
|
_createFutures();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _createFutures() {
|
||||||
futureSubscriptionCount = null;
|
futureSubscriptionCount = null;
|
||||||
futureClientCount = null;
|
futureClientCount = null;
|
||||||
futureKeyCount = null;
|
futureKeyCount = null;
|
||||||
@ -55,35 +89,70 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
|||||||
futureChannelSubscribedCount = null;
|
futureChannelSubscribedCount = null;
|
||||||
|
|
||||||
if (userAcc.isAuth()) {
|
if (userAcc.isAuth()) {
|
||||||
futureChannelAllCount = () async {
|
futureChannelAllCount = ImmediateFuture.ofFuture(() async {
|
||||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||||
final channels = await APIClient.getChannelList(userAcc, ChannelSelector.all);
|
final channels = await APIClient.getChannelList(userAcc, ChannelSelector.all);
|
||||||
return channels.length;
|
return channels.length;
|
||||||
}();
|
}());
|
||||||
|
|
||||||
futureChannelSubscribedCount = () async {
|
futureChannelSubscribedCount = ImmediateFuture.ofFuture(() async {
|
||||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||||
final channels = await APIClient.getChannelList(userAcc, ChannelSelector.subscribed);
|
final channels = await APIClient.getChannelList(userAcc, ChannelSelector.subscribed);
|
||||||
return channels.length;
|
return channels.length;
|
||||||
}();
|
}());
|
||||||
|
|
||||||
futureSubscriptionCount = () async {
|
futureSubscriptionCount = ImmediateFuture.ofFuture(() async {
|
||||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||||
final subs = await APIClient.getSubscriptionList(userAcc);
|
final subs = await APIClient.getSubscriptionList(userAcc);
|
||||||
return subs.length;
|
return subs.length;
|
||||||
}();
|
}());
|
||||||
|
|
||||||
futureClientCount = () async {
|
futureClientCount = ImmediateFuture.ofFuture(() async {
|
||||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||||
final clients = await APIClient.getClientList(userAcc);
|
final clients = await APIClient.getClientList(userAcc);
|
||||||
return clients.length;
|
return clients.length;
|
||||||
}();
|
}());
|
||||||
|
|
||||||
futureKeyCount = () async {
|
futureKeyCount = ImmediateFuture.ofFuture(() async {
|
||||||
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
if (!userAcc.isAuth()) throw new Exception('not logged in');
|
||||||
final keys = await APIClient.getKeyTokenList(userAcc);
|
final keys = await APIClient.getKeyTokenList(userAcc);
|
||||||
return keys.length;
|
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) {
|
Widget build(BuildContext context) {
|
||||||
return Consumer<AppAuth>(
|
return Consumer<AppAuth>(
|
||||||
builder: (context, acc, child) {
|
builder: (context, acc, child) {
|
||||||
|
if (!_isInitialized) return SizedBox();
|
||||||
|
|
||||||
if (!userAcc.isAuth()) {
|
if (!userAcc.isAuth()) {
|
||||||
return _buildNoAuth(context);
|
return _buildNoAuth(context);
|
||||||
} else {
|
} else {
|
||||||
return FutureBuilder(
|
return FutureBuilder(
|
||||||
future: acc.loadUser(force: false),
|
future: futureUser!.future,
|
||||||
builder: ((context, snapshot) {
|
builder: ((context, snapshot) {
|
||||||
if (snapshot.connectionState == ConnectionState.done) {
|
if (futureUser?.value != null) {
|
||||||
if (snapshot.hasError) {
|
return _buildShowAccount(context, acc, futureUser!.value!);
|
||||||
return Text('Error: ${snapshot.error}'); //TODO better error display
|
} 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!);
|
return _buildShowAccount(context, acc, snapshot.data!);
|
||||||
|
} else {
|
||||||
|
return Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
return Center(child: CircularProgressIndicator());
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -157,7 +230,7 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
|||||||
text: 'Use existing account',
|
text: 'Use existing account',
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (loading) return;
|
if (loading) return;
|
||||||
Navigator.push(context, MaterialPageRoute<AccountLoginPage>(builder: (context) => AccountLoginPage()));
|
Navi.push(context, () => AccountLoginPage());
|
||||||
},
|
},
|
||||||
tonal: true,
|
tonal: true,
|
||||||
big: true,
|
big: true,
|
||||||
@ -255,12 +328,15 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
|||||||
children: [
|
children: [
|
||||||
SizedBox(width: 80, child: Text("Channels", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)))),
|
SizedBox(width: 80, child: Text("Channels", style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)))),
|
||||||
FutureBuilder(
|
FutureBuilder(
|
||||||
future: futureChannelAllCount,
|
future: futureChannelAllCount!.future,
|
||||||
builder: (context, snapshot) {
|
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}');
|
return Text('${snapshot.data}');
|
||||||
|
} else {
|
||||||
|
return const SizedBox(width: 8, height: 8, child: Center(child: CircularProgressIndicator()));
|
||||||
}
|
}
|
||||||
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) {
|
List<Widget> _buildCards(BuildContext context, User user) {
|
||||||
return [
|
return [
|
||||||
UI.buttonCard(
|
_buildNumberCard(context, 'Subscriptions', futureSubscriptionCount, () {/*TODO*/}),
|
||||||
context: context,
|
_buildNumberCard(context, 'Clients', futureClientCount, () {/*TODO*/}),
|
||||||
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
_buildNumberCard(context, 'Keys', futureKeyCount, () {/*TODO*/}),
|
||||||
child: Row(
|
_buildNumberCard(context, 'Channels', futureChannelSubscribedCount, () {/*TODO*/}),
|
||||||
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*/},
|
|
||||||
),
|
|
||||||
UI.buttonCard(
|
UI.buttonCard(
|
||||||
context: context,
|
context: context,
|
||||||
margin: EdgeInsets.fromLTRB(0, 4, 0, 4),
|
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) {
|
Widget _buildFooter(BuildContext context, User user) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(8, 0, 8, 0),
|
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/globals.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||||
import 'package:simplecloudnotifier/state/token_source.dart';
|
import 'package:simplecloudnotifier/state/token_source.dart';
|
||||||
|
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
import 'package:simplecloudnotifier/utils/ui.dart';
|
||||||
|
|
||||||
@ -154,7 +155,7 @@ class _AccountLoginPageState extends State<AccountLoginPage> {
|
|||||||
await acc.save();
|
await acc.save();
|
||||||
|
|
||||||
Toaster.success("Login", "Successfully logged in");
|
Toaster.success("Login", "Successfully logged in");
|
||||||
Navigator.popUntil(context, (route) => route.isFirst);
|
Navi.popToRoot(context);
|
||||||
} catch (exc, trace) {
|
} catch (exc, trace) {
|
||||||
ApplicationLog.error('Failed to verify token: ' + exc.toString(), trace: trace);
|
ApplicationLog.error('Failed to verify token: ' + exc.toString(), trace: trace);
|
||||||
Toaster.error("Error", 'Failed to verify token');
|
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:provider/provider.dart';
|
||||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||||
import 'package:simplecloudnotifier/models/channel.dart';
|
import 'package:simplecloudnotifier/models/channel.dart';
|
||||||
|
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
||||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||||
import 'package:simplecloudnotifier/pages/channel_list/channel_list_item.dart';
|
import 'package:simplecloudnotifier/pages/channel_list/channel_list_item.dart';
|
||||||
|
|
||||||
class ChannelRootPage extends StatefulWidget {
|
class ChannelRootPage extends StatefulWidget {
|
||||||
const ChannelRootPage({super.key});
|
const ChannelRootPage({super.key, required this.isVisiblePage});
|
||||||
|
|
||||||
|
final bool isVisiblePage;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ChannelRootPage> createState() => _ChannelRootPageState();
|
State<ChannelRootPage> createState() => _ChannelRootPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChannelRootPageState extends State<ChannelRootPage> {
|
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
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
_pagingController.addPageRequestListener((pageKey) {
|
|
||||||
_fetchPage(pageKey);
|
|
||||||
});
|
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
|
_pagingController.addPageRequestListener(_fetchPage);
|
||||||
|
|
||||||
|
if (widget.isVisiblePage && !_isInitialized) _realInitState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
ApplicationLog.debug('ChannelRootPage::dispose');
|
||||||
_pagingController.dispose();
|
_pagingController.dispose();
|
||||||
super.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 {
|
Future<void> _fetchPage(int pageKey) async {
|
||||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
final acc = Provider.of<AppAuth>(context, listen: false);
|
||||||
|
|
||||||
|
ApplicationLog.debug('Start ChannelList::_pagingController::_fetchPage [ ${pageKey} ]');
|
||||||
|
|
||||||
if (!acc.isAuth()) {
|
if (!acc.isAuth()) {
|
||||||
_pagingController.error = 'Not logged in';
|
_pagingController.error = 'Not logged in';
|
||||||
return;
|
return;
|
||||||
@ -44,13 +72,41 @@ class _ChannelRootPageState extends State<ChannelRootPage> {
|
|||||||
|
|
||||||
items.sort((a, b) => -1 * (a.timestampLastSent ?? '').compareTo(b.timestampLastSent ?? ''));
|
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) {
|
} catch (exc, trace) {
|
||||||
_pagingController.error = exc.toString();
|
_pagingController.error = exc.toString();
|
||||||
ApplicationLog.error('Failed to list channels: ' + exc.toString(), trace: trace);
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:simplecloudnotifier/models/channel.dart';
|
||||||
|
import 'package:simplecloudnotifier/models/message.dart';
|
||||||
import 'package:simplecloudnotifier/pages/debug/debug_persistence_hive.dart';
|
import 'package:simplecloudnotifier/pages/debug/debug_persistence_hive.dart';
|
||||||
import 'package:simplecloudnotifier/pages/debug/debug_persistence_sharedprefs.dart';
|
import 'package:simplecloudnotifier/pages/debug/debug_persistence_sharedprefs.dart';
|
||||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||||
|
import 'package:simplecloudnotifier/state/fb_message.dart';
|
||||||
|
import 'package:simplecloudnotifier/state/interfaces.dart';
|
||||||
import 'package:simplecloudnotifier/state/request_log.dart';
|
import 'package:simplecloudnotifier/state/request_log.dart';
|
||||||
|
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||||
|
|
||||||
class DebugPersistencePage extends StatefulWidget {
|
class DebugPersistencePage extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
@ -28,62 +33,56 @@ class _DebugPersistencePageState extends State<DebugPersistencePage> {
|
|||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Card.outlined(
|
_buildSharedPrefCard(context),
|
||||||
child: Padding(
|
_buildHiveCard(context, () => Hive.box<SCNRequest>('scn-requests'), 'scn-requests'),
|
||||||
padding: const EdgeInsets.all(8.0),
|
_buildHiveCard(context, () => Hive.box<SCNLog>('scn-logs'), 'scn-logs'),
|
||||||
child: GestureDetector(
|
_buildHiveCard(context, () => Hive.box<Message>('scn-message-cache'), 'scn-message-cache'),
|
||||||
onTap: () {
|
_buildHiveCard(context, () => Hive.box<Channel>('scn-channel-cache'), 'scn-channel-cache'),
|
||||||
Navigator.push(context, MaterialPageRoute<DebugSharedPrefPage>(builder: (context) => DebugSharedPrefPage(sharedPref: prefs!)));
|
_buildHiveCard(context, () => Hive.box<FBMessage>('scn-fb-messages'), 'scn-fb-messages'),
|
||||||
},
|
|
||||||
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)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
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'))));
|
|
||||||
},
|
|
||||||
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)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
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)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildSharedPrefCard(BuildContext context) {
|
||||||
|
return Card.outlined(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
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: 40, child: Text('${prefs?.getKeys().length.toString()}', textAlign: TextAlign.end)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHiveCard(BuildContext context, Box<FieldDebuggable> Function() boxFunc, String boxname) {
|
||||||
|
return Card.outlined(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
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 [$boxname]', style: TextStyle(fontWeight: FontWeight.bold), textAlign: TextAlign.center)),
|
||||||
|
SizedBox(width: 40, child: Text('${boxFunc().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/components/layout/scaffold.dart';
|
||||||
import 'package:simplecloudnotifier/pages/debug/debug_persistence_hiveentry.dart';
|
import 'package:simplecloudnotifier/pages/debug/debug_persistence_hiveentry.dart';
|
||||||
import 'package:simplecloudnotifier/state/interfaces.dart';
|
import 'package:simplecloudnotifier/state/interfaces.dart';
|
||||||
|
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||||
|
|
||||||
class DebugHiveBoxPage extends StatelessWidget {
|
class DebugHiveBoxPage extends StatelessWidget {
|
||||||
final String boxName;
|
final String boxName;
|
||||||
@ -21,7 +22,7 @@ class DebugHiveBoxPage extends StatelessWidget {
|
|||||||
itemBuilder: (context, listIndex) {
|
itemBuilder: (context, listIndex) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.push(context, MaterialPageRoute<DebugHiveEntryPage>(builder: (context) => DebugHiveEntryPage(value: box.getAt(listIndex)!)));
|
Navi.push(context, () => DebugHiveEntryPage(value: box.getAt(listIndex)!));
|
||||||
},
|
},
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: Text(box.getAt(listIndex).toString(), style: TextStyle(fontWeight: FontWeight.bold)),
|
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:intl/intl.dart';
|
||||||
import 'package:simplecloudnotifier/pages/debug/debug_request_view.dart';
|
import 'package:simplecloudnotifier/pages/debug/debug_request_view.dart';
|
||||||
import 'package:simplecloudnotifier/state/request_log.dart';
|
import 'package:simplecloudnotifier/state/request_log.dart';
|
||||||
|
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||||
|
|
||||||
class DebugRequestsPage extends StatefulWidget {
|
class DebugRequestsPage extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
@ -40,7 +41,7 @@ class _DebugRequestsPageState extends State<DebugRequestsPage> {
|
|||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 2.0),
|
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 2.0),
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () => Navigator.push(context, MaterialPageRoute<DebugRequestViewPage>(builder: (context) => DebugRequestViewPage(request: req))),
|
onTap: () => Navi.push(context, () => DebugRequestViewPage(request: req)),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
tileColor: Theme.of(context).colorScheme.errorContainer,
|
tileColor: Theme.of(context).colorScheme.errorContainer,
|
||||||
textColor: Theme.of(context).colorScheme.onErrorContainer,
|
textColor: Theme.of(context).colorScheme.onErrorContainer,
|
||||||
@ -76,7 +77,7 @@ class _DebugRequestsPageState extends State<DebugRequestsPage> {
|
|||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 2.0),
|
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 2.0),
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () => Navigator.push(context, MaterialPageRoute<DebugRequestViewPage>(builder: (context) => DebugRequestViewPage(request: req))),
|
onTap: () => Navi.push(context, () => DebugRequestViewPage(request: req)),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: Row(
|
title: Row(
|
||||||
children: [
|
children: [
|
||||||
|
@ -1,16 +1,21 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||||
import 'package:simplecloudnotifier/models/channel.dart';
|
import 'package:simplecloudnotifier/models/channel.dart';
|
||||||
import 'package:simplecloudnotifier/models/message.dart';
|
import 'package:simplecloudnotifier/models/message.dart';
|
||||||
import 'package:simplecloudnotifier/pages/message_view/message_view.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/application_log.dart';
|
||||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||||
import 'package:simplecloudnotifier/pages/message_list/message_list_item.dart';
|
import 'package:simplecloudnotifier/pages/message_list/message_list_item.dart';
|
||||||
|
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||||
|
|
||||||
class MessageListPage extends StatefulWidget {
|
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 switch to tab
|
||||||
//TODO reload on app to foreground
|
//TODO reload on app to foreground
|
||||||
@ -19,31 +24,104 @@ class MessageListPage extends StatefulWidget {
|
|||||||
State<MessageListPage> createState() => _MessageListPageState();
|
State<MessageListPage> createState() => _MessageListPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MessageListPageState extends State<MessageListPage> {
|
class _MessageListPageState extends State<MessageListPage> with RouteAware {
|
||||||
static const _pageSize = 128;
|
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;
|
Map<String, Channel>? _channels = null;
|
||||||
|
|
||||||
|
bool _isInitialized = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
//TODO init with state from cache - also allow tho show cache on error
|
|
||||||
_pagingController.addPageRequestListener((pageKey) {
|
|
||||||
_fetchPage(pageKey);
|
|
||||||
});
|
|
||||||
super.initState();
|
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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
ApplicationLog.debug('MessageListPage::dispose');
|
||||||
|
Navi.modalRouteObserver.unsubscribe(this);
|
||||||
_pagingController.dispose();
|
_pagingController.dispose();
|
||||||
|
_lifecyleListener.dispose();
|
||||||
super.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 {
|
Future<void> _fetchPage(String thisPageToken) async {
|
||||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
final acc = Provider.of<AppAuth>(context, listen: false);
|
||||||
|
|
||||||
|
ApplicationLog.debug('Start MessageList::_pagingController::_fetchPage [ ${thisPageToken} ]');
|
||||||
|
|
||||||
if (!acc.isAuth()) {
|
if (!acc.isAuth()) {
|
||||||
_pagingController.error = 'Not logged in';
|
_pagingController.error = 'Not logged in';
|
||||||
return;
|
return;
|
||||||
@ -53,10 +131,16 @@ class _MessageListPageState extends State<MessageListPage> {
|
|||||||
if (_channels == null) {
|
if (_channels == null) {
|
||||||
final channels = await APIClient.getChannelList(acc, ChannelSelector.allAny);
|
final channels = await APIClient.getChannelList(acc, ChannelSelector.allAny);
|
||||||
_channels = <String, Channel>{for (var v in channels) v.channel.channelID: v.channel};
|
_channels = <String, Channel>{for (var v in channels) v.channel.channelID: v.channel};
|
||||||
|
|
||||||
|
_setChannelCache(channels); // no await
|
||||||
}
|
}
|
||||||
|
|
||||||
final (npt, newItems) = await APIClient.getMessageList(acc, thisPageToken, pageSize: _pageSize);
|
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') {
|
if (npt == '@end') {
|
||||||
_pagingController.appendLastPage(newItems);
|
_pagingController.appendLastPage(newItems);
|
||||||
} else {
|
} 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
return Padding(
|
||||||
@ -83,7 +232,7 @@ class _MessageListPageState extends State<MessageListPage> {
|
|||||||
message: item,
|
message: item,
|
||||||
allChannels: _channels ?? {},
|
allChannels: _channels ?? {},
|
||||||
onPressed: () {
|
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(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
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(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
processTitle(message.title),
|
processTitle(message.title),
|
||||||
@ -69,11 +65,22 @@ class MessageListItem extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(height: 4),
|
SizedBox(height: 4),
|
||||||
Text(
|
Row(
|
||||||
processContent(message.content),
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)),
|
children: [
|
||||||
overflow: TextOverflow.ellipsis,
|
Expanded(
|
||||||
maxLines: _lineCount,
|
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(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
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(
|
UI.channelChip(
|
||||||
context: context,
|
context: context,
|
||||||
text: resolveChannelName(message),
|
text: resolveChannelName(message),
|
||||||
@ -124,11 +127,22 @@ class MessageListItem extends StatelessWidget {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
),
|
),
|
||||||
Text(
|
Row(
|
||||||
processContent(message.content),
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
style: TextStyle(color: Theme.of(context).textTheme.bodyLarge?.color?.withAlpha(160)),
|
children: [
|
||||||
overflow: TextOverflow.ellipsis,
|
Expanded(
|
||||||
maxLines: _lineCount,
|
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:provider/provider.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||||
import 'package:simplecloudnotifier/api/api_exception.dart';
|
|
||||||
import 'package:simplecloudnotifier/components/layout/scaffold.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/channel.dart';
|
||||||
import 'package:simplecloudnotifier/models/keytoken.dart';
|
import 'package:simplecloudnotifier/models/keytoken.dart';
|
||||||
import 'package:simplecloudnotifier/models/message.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_auth.dart';
|
||||||
|
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
||||||
import 'package:simplecloudnotifier/utils/toaster.dart';
|
import 'package:simplecloudnotifier/utils/toaster.dart';
|
||||||
import 'package:simplecloudnotifier/utils/ui.dart';
|
import 'package:simplecloudnotifier/utils/ui.dart';
|
||||||
|
|
||||||
@ -25,54 +25,46 @@ class MessageViewPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MessageViewPageState extends State<MessageViewPage> {
|
class _MessageViewPageState extends State<MessageViewPage> {
|
||||||
late Future<(Message, ChannelWithSubscription?, KeyToken?)>? mainFuture;
|
late Future<(Message, ChannelPreview, KeyTokenPreview, UserPreview)>? mainFuture;
|
||||||
(Message, ChannelWithSubscription?, KeyToken?)? mainFutureSnapshot = null;
|
(Message, ChannelPreview, KeyTokenPreview, UserPreview)? mainFutureSnapshot = null;
|
||||||
static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm');
|
static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm');
|
||||||
|
|
||||||
bool _monospaceMode = false;
|
bool _monospaceMode = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
|
||||||
mainFuture = fetchData();
|
mainFuture = fetchData();
|
||||||
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<(Message, ChannelWithSubscription?, KeyToken?)> fetchData() async {
|
Future<(Message, ChannelPreview, KeyTokenPreview, UserPreview)> fetchData() async {
|
||||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
|
||||||
|
|
||||||
final msg = await APIClient.getMessage(acc, widget.message.messageID);
|
|
||||||
|
|
||||||
ChannelWithSubscription? chn = null;
|
|
||||||
try {
|
try {
|
||||||
chn = await APIClient.getChannel(acc, msg.channelID); //TODO getShortChannel (?) -> no perm
|
await Future.delayed(const Duration(seconds: 0), () {}); // this is annoyingly important - otherwise we call setLoadingIndeterminate directly in initStat() and get an exception....
|
||||||
} on APIException catch (e) {
|
|
||||||
if (e.error == APIError.USER_AUTH_FAILED) {
|
AppBarState().setLoadingIndeterminate(true);
|
||||||
chn = null;
|
|
||||||
} else {
|
final acc = Provider.of<AppAuth>(context, listen: false);
|
||||||
rethrow;
|
|
||||||
}
|
final msg = await APIClient.getMessage(acc, widget.message.messageID);
|
||||||
|
|
||||||
|
final fut_chn = APIClient.getChannelPreview(acc, msg.channelID);
|
||||||
|
final fut_key = APIClient.getKeyTokenPreview(acc, msg.usedKeyID);
|
||||||
|
final fut_usr = APIClient.getUserPreview(acc, msg.senderUserID);
|
||||||
|
|
||||||
|
final chn = await fut_chn;
|
||||||
|
final key = await fut_key;
|
||||||
|
final usr = await fut_usr;
|
||||||
|
|
||||||
|
//await Future.delayed(const Duration(seconds: 10), () {});
|
||||||
|
|
||||||
|
final r = (msg, chn, key, usr);
|
||||||
|
|
||||||
|
mainFutureSnapshot = r;
|
||||||
|
|
||||||
|
return r;
|
||||||
|
} finally {
|
||||||
|
AppBarState().setLoadingIndeterminate(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO getShortUser for sender
|
|
||||||
|
|
||||||
//await Future.delayed(const Duration(seconds: 2), () {});
|
|
||||||
|
|
||||||
final r = (msg, chn, tok);
|
|
||||||
|
|
||||||
mainFutureSnapshot = r;
|
|
||||||
|
|
||||||
return r;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -87,16 +79,16 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
|||||||
showSearch: false,
|
showSearch: false,
|
||||||
showShare: true,
|
showShare: true,
|
||||||
onShare: _share,
|
onShare: _share,
|
||||||
child: FutureBuilder<(Message, ChannelWithSubscription?, KeyToken?)>(
|
child: FutureBuilder<(Message, ChannelPreview, KeyTokenPreview, UserPreview)>(
|
||||||
future: mainFuture,
|
future: mainFuture,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.hasData) {
|
if (snapshot.hasData) {
|
||||||
final (msg, chn, tok) = snapshot.data!;
|
final (msg, chn, tok, usr) = snapshot.data!;
|
||||||
return _buildMessageView(context, msg, chn, tok, false);
|
return _buildMessageView(context, msg, chn, tok, usr);
|
||||||
} else if (snapshot.hasError) {
|
} else if (snapshot.hasError) {
|
||||||
return Center(child: Text('${snapshot.error}')); //TODO nice error page
|
return Center(child: Text('${snapshot.error}')); //TODO nice error page
|
||||||
} else if (!widget.message.trimmed) {
|
} else if (!widget.message.trimmed) {
|
||||||
return _buildMessageView(context, widget.message, null, null, true);
|
return _buildMessageView(context, widget.message, null, null, null);
|
||||||
} else {
|
} else {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
@ -108,7 +100,7 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
|||||||
void _share() async {
|
void _share() async {
|
||||||
var msg = widget.message;
|
var msg = widget.message;
|
||||||
if (mainFutureSnapshot != null) {
|
if (mainFutureSnapshot != null) {
|
||||||
(msg, _, _) = mainFutureSnapshot!;
|
(msg, _, _, _) = mainFutureSnapshot!;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.content != null) {
|
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);
|
final userAccUserID = context.select<AppAuth, String?>((v) => v.userID);
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
@ -135,16 +127,16 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
..._buildMessageHeader(context, message, channel, token, loading),
|
..._buildMessageHeader(context, message, channel),
|
||||||
SizedBox(height: 8),
|
SizedBox(height: 8),
|
||||||
if (message.content != null) ..._buildMessageContent(context, message, channel, token),
|
if (message.content != null) ..._buildMessageContent(context, message),
|
||||||
SizedBox(height: 8),
|
SizedBox(height: 8),
|
||||||
if (message.senderName != null) _buildMetaCard(context, FontAwesomeIcons.solidSignature, 'Sender', [message.senderName!], () => {/*TODO*/}),
|
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.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.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.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]),
|
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) {
|
String _resolveChannelName(ChannelPreview? channel, Message message) {
|
||||||
return channel?.channel.displayName ?? message.channelInternalName;
|
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 [
|
return [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@ -171,28 +163,11 @@ class _MessageViewPageState extends State<MessageViewPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(height: 8),
|
SizedBox(height: 8),
|
||||||
if (!loading) Text(message.title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
Text(_preformatTitle(message), 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),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildMessageContent(BuildContext context, Message message, ChannelWithSubscription? channel, KeyToken? token) {
|
List<Widget> _buildMessageContent(BuildContext context, Message message) {
|
||||||
return [
|
return [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
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';
|
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||||
|
|
||||||
class SendRootPage extends StatefulWidget {
|
class SendRootPage extends StatefulWidget {
|
||||||
const SendRootPage({super.key});
|
const SendRootPage({super.key, required bool isVisiblePage});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SendRootPage> createState() => _SendRootPageState();
|
State<SendRootPage> createState() => _SendRootPageState();
|
||||||
@ -130,6 +130,8 @@ class _SendRootPageState extends State<SendRootPage> {
|
|||||||
try {
|
try {
|
||||||
final Uri uri = Uri.parse(url);
|
final Uri uri = Uri.parse(url);
|
||||||
|
|
||||||
|
ApplicationLog.debug('Opening URL: [ ${uri.toString()} ]');
|
||||||
|
|
||||||
if (await canLaunchUrl(uri)) {
|
if (await canLaunchUrl(uri)) {
|
||||||
await launchUrl(uri);
|
await launchUrl(uri);
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class SettingsRootPage extends StatefulWidget {
|
class SettingsRootPage extends StatefulWidget {
|
||||||
const SettingsRootPage({super.key});
|
const SettingsRootPage({super.key, required bool isVisiblePage});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SettingsRootPage> createState() => _SettingsRootPageState();
|
State<SettingsRootPage> createState() => _SettingsRootPageState();
|
||||||
|
@ -14,6 +14,7 @@ class AppAuth extends ChangeNotifier implements TokenSource {
|
|||||||
|
|
||||||
User? _user;
|
User? _user;
|
||||||
Client? _client;
|
Client? _client;
|
||||||
|
DateTime? _clientQueryTime;
|
||||||
|
|
||||||
String? get userID => _userID;
|
String? get userID => _userID;
|
||||||
String? get tokenAdmin => _tokenAdmin;
|
String? get tokenAdmin => _tokenAdmin;
|
||||||
@ -117,14 +118,17 @@ class AppAuth extends ChangeNotifier implements TokenSource {
|
|||||||
final user = await APIClient.getUser(this, _userID!);
|
final user = await APIClient.getUser(this, _userID!);
|
||||||
|
|
||||||
_user = user;
|
_user = user;
|
||||||
notifyListeners();
|
|
||||||
|
|
||||||
await save();
|
await save();
|
||||||
|
|
||||||
return user;
|
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) {
|
if (!force && _client != null && _client!.clientID == _clientID) {
|
||||||
return _client!;
|
return _client!;
|
||||||
}
|
}
|
||||||
@ -137,14 +141,12 @@ class AppAuth extends ChangeNotifier implements TokenSource {
|
|||||||
final client = await APIClient.getClient(this, _clientID!);
|
final client = await APIClient.getClient(this, _clientID!);
|
||||||
|
|
||||||
_client = client;
|
_client = client;
|
||||||
notifyListeners();
|
|
||||||
|
|
||||||
await save();
|
await save();
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
} on APIException catch (_) {
|
} on APIException catch (_) {
|
||||||
_client = null;
|
_client = null;
|
||||||
notifyListeners();
|
|
||||||
return null;
|
return null;
|
||||||
} catch (exc) {
|
} catch (exc) {
|
||||||
_client = null;
|
_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';
|
part 'application_log.g.dart';
|
||||||
|
|
||||||
class ApplicationLog {
|
class ApplicationLog {
|
||||||
|
//TODO max size, auto clear old
|
||||||
|
|
||||||
static void debug(String message, {String? additional, StackTrace? trace}) {
|
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(
|
Hive.box<SCNLog>('scn-logs').add(SCNLog(
|
||||||
id: Xid().toString(),
|
id: Xid().toString(),
|
||||||
@ -19,7 +21,7 @@ class ApplicationLog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void info(String message, {String? additional, StackTrace? trace}) {
|
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(
|
Hive.box<SCNLog>('scn-logs').add(SCNLog(
|
||||||
id: Xid().toString(),
|
id: Xid().toString(),
|
||||||
@ -32,7 +34,7 @@ class ApplicationLog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void warn(String message, {String? additional, StackTrace? trace}) {
|
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(
|
Hive.box<SCNLog>('scn-logs').add(SCNLog(
|
||||||
id: Xid().toString(),
|
id: Xid().toString(),
|
||||||
@ -45,7 +47,7 @@ class ApplicationLog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void error(String message, {String? additional, StackTrace? trace}) {
|
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(
|
Hive.box<SCNLog>('scn-logs').add(SCNLog(
|
||||||
id: Xid().toString(),
|
id: Xid().toString(),
|
||||||
@ -58,7 +60,7 @@ class ApplicationLog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void fatal(String message, {String? additional, StackTrace? trace}) {
|
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(
|
Hive.box<SCNLog>('scn-logs').add(SCNLog(
|
||||||
id: Xid().toString(),
|
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';
|
part 'request_log.g.dart';
|
||||||
|
|
||||||
class RequestLog {
|
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) {
|
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(
|
Hive.box<SCNRequest>('scn-requests').add(SCNRequest(
|
||||||
id: Xid().toString(),
|
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,
|
||||||
"size" : "16x16",
|
"author": "xcode"
|
||||||
"idiom" : "mac",
|
|
||||||
"filename" : "app_icon_16.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
},
|
||||||
{
|
"images": [
|
||||||
"size" : "16x16",
|
{
|
||||||
"idiom" : "mac",
|
"size": "16x16",
|
||||||
"filename" : "app_icon_32.png",
|
"idiom": "mac",
|
||||||
"scale" : "2x"
|
"filename": "app_icon_16.png",
|
||||||
},
|
"scale": "1x"
|
||||||
{
|
},
|
||||||
"size" : "32x32",
|
{
|
||||||
"idiom" : "mac",
|
"size": "16x16",
|
||||||
"filename" : "app_icon_32.png",
|
"idiom": "mac",
|
||||||
"scale" : "1x"
|
"filename": "app_icon_32.png",
|
||||||
},
|
"scale": "2x"
|
||||||
{
|
},
|
||||||
"size" : "32x32",
|
{
|
||||||
"idiom" : "mac",
|
"size": "32x32",
|
||||||
"filename" : "app_icon_64.png",
|
"idiom": "mac",
|
||||||
"scale" : "2x"
|
"filename": "app_icon_32.png",
|
||||||
},
|
"scale": "1x"
|
||||||
{
|
},
|
||||||
"size" : "128x128",
|
{
|
||||||
"idiom" : "mac",
|
"size": "32x32",
|
||||||
"filename" : "app_icon_128.png",
|
"idiom": "mac",
|
||||||
"scale" : "1x"
|
"filename": "app_icon_64.png",
|
||||||
},
|
"scale": "2x"
|
||||||
{
|
},
|
||||||
"size" : "128x128",
|
{
|
||||||
"idiom" : "mac",
|
"size": "128x128",
|
||||||
"filename" : "app_icon_256.png",
|
"idiom": "mac",
|
||||||
"scale" : "2x"
|
"filename": "app_icon_128.png",
|
||||||
},
|
"scale": "1x"
|
||||||
{
|
},
|
||||||
"size" : "256x256",
|
{
|
||||||
"idiom" : "mac",
|
"size": "128x128",
|
||||||
"filename" : "app_icon_256.png",
|
"idiom": "mac",
|
||||||
"scale" : "1x"
|
"filename": "app_icon_256.png",
|
||||||
},
|
"scale": "2x"
|
||||||
{
|
},
|
||||||
"size" : "256x256",
|
{
|
||||||
"idiom" : "mac",
|
"size": "256x256",
|
||||||
"filename" : "app_icon_512.png",
|
"idiom": "mac",
|
||||||
"scale" : "2x"
|
"filename": "app_icon_256.png",
|
||||||
},
|
"scale": "1x"
|
||||||
{
|
},
|
||||||
"size" : "512x512",
|
{
|
||||||
"idiom" : "mac",
|
"size": "256x256",
|
||||||
"filename" : "app_icon_512.png",
|
"idiom": "mac",
|
||||||
"scale" : "1x"
|
"filename": "app_icon_512.png",
|
||||||
},
|
"scale": "2x"
|
||||||
{
|
},
|
||||||
"size" : "512x512",
|
{
|
||||||
"idiom" : "mac",
|
"size": "512x512",
|
||||||
"filename" : "app_icon_1024.png",
|
"idiom": "mac",
|
||||||
"scale" : "2x"
|
"filename": "app_icon_512.png",
|
||||||
}
|
"scale": "1x"
|
||||||
],
|
},
|
||||||
"info" : {
|
{
|
||||||
"version" : 1,
|
"size": "512x512",
|
||||||
"author" : "xcode"
|
"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"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.4.1"
|
version: "6.4.1"
|
||||||
|
archive:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: archive
|
||||||
|
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.6.1"
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -129,6 +137,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.3"
|
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:
|
clock:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -253,18 +269,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_core_platform_interface
|
name: firebase_core_platform_interface
|
||||||
sha256: c437ae5d17e6b5cc7981cf6fd458a5db4d12979905f9aafd1fea930428a9fe63
|
sha256: "1003a5a03a61fc9a22ef49f37cbcb9e46c86313a7b2e7029b9390cf8c6fc32cb"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.0"
|
version: "5.1.0"
|
||||||
firebase_core_web:
|
firebase_core_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_core_web
|
name: firebase_core_web
|
||||||
sha256: "43d9e951ac52b87ae9cc38ecdcca1e8fa7b52a1dd26a96085ba41ce5108db8e9"
|
sha256: "6643fe3dbd021e6ccfb751f7882b39df355708afbdeb4130fc50f9305a9d1a3d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.17.0"
|
version: "2.17.2"
|
||||||
firebase_messaging:
|
firebase_messaging:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -302,6 +318,14 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
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:
|
flutter_lazy_indexed_stack:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -423,6 +447,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
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:
|
infinite_scroll_pagination:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -631,14 +663,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.0+3"
|
version: "3.1.0+3"
|
||||||
|
petitparser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: petitparser
|
||||||
|
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.2"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: platform
|
name: platform
|
||||||
sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
|
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.4"
|
version: "3.1.5"
|
||||||
plugin_platform_interface:
|
plugin_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -675,10 +715,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: pubspec_parse
|
name: pubspec_parse
|
||||||
sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367
|
sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.3"
|
version: "1.3.0"
|
||||||
qr:
|
qr:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -904,10 +944,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: url_launcher
|
name: url_launcher
|
||||||
sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e"
|
sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.2.6"
|
version: "6.3.0"
|
||||||
url_launcher_android:
|
url_launcher_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1044,6 +1084,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.1"
|
version: "1.2.1"
|
||||||
|
xml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xml
|
||||||
|
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.5.0"
|
||||||
yaml:
|
yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
name: simplecloudnotifier
|
name: simplecloudnotifier
|
||||||
description: "A new Flutter project."
|
description: "Receive push messages"
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 2.0.0+100
|
version: 2.0.0+100
|
||||||
@ -11,6 +11,8 @@ dependencies:
|
|||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|
||||||
|
flutter_launcher_icons: "^0.13.1"
|
||||||
|
|
||||||
font_awesome_flutter: '>= 4.7.0'
|
font_awesome_flutter: '>= 4.7.0'
|
||||||
cupertino_icons: ^1.0.2
|
cupertino_icons: ^1.0.2
|
||||||
http: ^1.2.0
|
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",
|
"short_name": "simplecloudnotifier",
|
||||||
"start_url": ".",
|
"start_url": ".",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"background_color": "#0175C2",
|
"background_color": "#hexcode",
|
||||||
"theme_color": "#0175C2",
|
"theme_color": "#hexcode",
|
||||||
"description": "A new Flutter project.",
|
"description": "A new Flutter project.",
|
||||||
"orientation": "portrait-primary",
|
"orientation": "portrait-primary",
|
||||||
"prefer_related_applications": false,
|
"prefer_related_applications": false,
|
||||||
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 3.2 KiB |
@ -11,8 +11,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
"gogs.mikescher.com/BlackForestBytes/goext/langext"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"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"
|
uri := "https://fcm.googleapis.com/v1/projects/" + fb.fbProject + "/messages:send"
|
||||||
|
|
||||||
jsonBody := gin.H{
|
jsonBody := gin.H{}
|
||||||
"token": client.FCMToken,
|
|
||||||
}
|
|
||||||
|
|
||||||
if client.Type == models.ClientTypeIOS {
|
if client.Type == models.ClientTypeIOS {
|
||||||
jsonBody["notification"] = gin.H{
|
jsonBody = gin.H{
|
||||||
"title": msg.Title,
|
"token": client.FCMToken,
|
||||||
"body": msg.ShortContent(),
|
|
||||||
}
|
|
||||||
jsonBody["apns"] = gin.H{}
|
|
||||||
} else if client.Type == models.ClientTypeAndroid {
|
|
||||||
jsonBody["android"] = gin.H{
|
|
||||||
"priority": "high",
|
|
||||||
"notification": gin.H{
|
"notification": gin.H{
|
||||||
"event_time": msg.Timestamp().Format(time.RFC3339),
|
"title": msg.Title,
|
||||||
"title": msg.FormatNotificationTitle(user, channel),
|
"body": msg.ShortContent(),
|
||||||
"body": msg.ShortContent(),
|
},
|
||||||
|
"apns": gin.H{},
|
||||||
|
}
|
||||||
|
} else if client.Type == models.ClientTypeAndroid {
|
||||||
|
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(), ""),
|
||||||
},
|
},
|
||||||
"fcm_options": gin.H{},
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
jsonBody["notification"] = gin.H{
|
jsonBody = gin.H{
|
||||||
"title": msg.FormatNotificationTitle(user, channel),
|
"token": client.FCMToken,
|
||||||
"body": msg.ShortContent(),
|
"notification": gin.H{
|
||||||
|
"title": msg.FormatNotificationTitle(user, channel),
|
||||||
|
"body": msg.ShortContent(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|