Cache messages, use cache if exists, load in background
This commit is contained in:
parent
9c366399df
commit
35ab9a26c0
@ -1,7 +1,7 @@
|
||||
|
||||
|
||||
run:
|
||||
dart run build_runner build
|
||||
flutter pub run build_runner build
|
||||
flutter run
|
||||
|
||||
test:
|
||||
@ -11,7 +11,7 @@ fix:
|
||||
dart fix --apply
|
||||
|
||||
gen:
|
||||
dart run build_runner build
|
||||
flutter pub run build_runner build
|
||||
|
||||
autoreload:
|
||||
@# run `make run` in another terminal (or another variant of flutter run)
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/components/layout/app_bar_progress_indicator.dart';
|
||||
@ -28,6 +29,16 @@ class SCNAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
Widget build(BuildContext context) {
|
||||
var actions = <Widget>[];
|
||||
|
||||
if (showDebug) {
|
||||
actions.add(IconButton(
|
||||
icon: const Icon(FontAwesomeIcons.solidSpiderBlackWidow),
|
||||
tooltip: 'Debug',
|
||||
onPressed: () {
|
||||
Navi.push(context, () => DebugMainPage());
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
if (showThemeSwitch) {
|
||||
actions.add(Consumer<AppTheme>(
|
||||
builder: (context, appTheme, child) => IconButton(
|
||||
@ -37,19 +48,16 @@ class SCNAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
),
|
||||
));
|
||||
} else {
|
||||
actions.add(SizedBox.square(dimension: 40));
|
||||
}
|
||||
|
||||
if (showDebug) {
|
||||
actions.add(IconButton(
|
||||
icon: const Icon(FontAwesomeIcons.solidSpiderBlackWidow),
|
||||
tooltip: 'Debug',
|
||||
onPressed: () {
|
||||
Navi.push(context, () => DebugMainPage());
|
||||
},
|
||||
actions.add(Visibility(
|
||||
visible: false,
|
||||
maintainSize: true,
|
||||
maintainAnimation: true,
|
||||
maintainState: true,
|
||||
child: IconButton(
|
||||
icon: const Icon(FontAwesomeIcons.square),
|
||||
onPressed: () {/*TODO*/},
|
||||
),
|
||||
));
|
||||
} else {
|
||||
actions.add(SizedBox.square(dimension: 40));
|
||||
}
|
||||
|
||||
if (showSearch) {
|
||||
@ -65,7 +73,16 @@ class SCNAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
onPressed: onShare ?? () {},
|
||||
));
|
||||
} else {
|
||||
actions.add(SizedBox.square(dimension: 40));
|
||||
actions.add(Visibility(
|
||||
visible: false,
|
||||
maintainSize: true,
|
||||
maintainAnimation: true,
|
||||
maintainState: true,
|
||||
child: IconButton(
|
||||
icon: const Icon(FontAwesomeIcons.square),
|
||||
onPressed: () {/*TODO*/},
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
return AppBar(
|
||||
|
@ -5,7 +5,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/models/channel.dart';
|
||||
import 'package:simplecloudnotifier/models/client.dart';
|
||||
import 'package:simplecloudnotifier/models/message.dart';
|
||||
import 'package:simplecloudnotifier/nav_layout.dart';
|
||||
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
||||
import 'package:simplecloudnotifier/state/app_theme.dart';
|
||||
@ -36,6 +38,8 @@ void main() async {
|
||||
Hive.registerAdapter(SCNRequestAdapter());
|
||||
Hive.registerAdapter(SCNLogAdapter());
|
||||
Hive.registerAdapter(SCNLogLevelAdapter());
|
||||
Hive.registerAdapter(MessageAdapter());
|
||||
Hive.registerAdapter(ChannelAdapter());
|
||||
|
||||
print('[INIT] Load Hive<scn-requests>...');
|
||||
|
||||
@ -57,6 +61,26 @@ void main() async {
|
||||
ApplicationLog.error('Failed to open Hive-Box: scn-logs: ' + exc.toString(), trace: trace);
|
||||
}
|
||||
|
||||
print('[INIT] Load Hive<scn-message-cache>...');
|
||||
|
||||
try {
|
||||
await Hive.openBox<Message>('scn-message-cache');
|
||||
} catch (exc, trace) {
|
||||
Hive.deleteBoxFromDisk('scn-message-cache');
|
||||
await Hive.openBox<Message>('scn-message-cache');
|
||||
ApplicationLog.error('Failed to open Hive-Box: scn-message-cache' + exc.toString(), trace: trace);
|
||||
}
|
||||
|
||||
print('[INIT] Load Hive<scn-channel-cache>...');
|
||||
|
||||
try {
|
||||
await Hive.openBox<Channel>('scn-channel-cache');
|
||||
} catch (exc, trace) {
|
||||
Hive.deleteBoxFromDisk('scn-channel-cache');
|
||||
await Hive.openBox<Channel>('scn-channel-cache');
|
||||
ApplicationLog.error('Failed to open Hive-Box: scn-channel-cache' + exc.toString(), trace: trace);
|
||||
}
|
||||
|
||||
print('[INIT] Load AppAuth...');
|
||||
|
||||
final appAuth = AppAuth(); // ensure UserAccount is loaded
|
||||
@ -135,7 +159,7 @@ void setFirebaseToken(String fcmToken) async {
|
||||
|
||||
Client? client;
|
||||
try {
|
||||
client = await acc.loadClient(force: true);
|
||||
client = await acc.loadClient(forceIfOlder: Duration(seconds: 60));
|
||||
} catch (exc, trace) {
|
||||
ApplicationLog.error('Failed to get client: ' + exc.toString(), trace: trace);
|
||||
return;
|
||||
@ -172,7 +196,7 @@ class SCNApp extends StatelessWidget {
|
||||
child: Consumer<AppTheme>(
|
||||
builder: (context, appTheme, child) => MaterialApp(
|
||||
title: 'SimpleCloudNotifier',
|
||||
navigatorObservers: [Navi.routeObserver],
|
||||
navigatorObservers: [Navi.routeObserver, Navi.modalRouteObserver],
|
||||
theme: ThemeData(
|
||||
//TODO color settings
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue, brightness: appTheme.darkMode ? Brightness.dark : Brightness.light),
|
||||
|
@ -1,17 +1,32 @@
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:simplecloudnotifier/models/subscription.dart';
|
||||
import 'package:simplecloudnotifier/state/interfaces.dart';
|
||||
|
||||
class Channel {
|
||||
part 'channel.g.dart';
|
||||
|
||||
@HiveType(typeId: 104)
|
||||
class Channel extends HiveObject implements FieldDebuggable {
|
||||
@HiveField(0)
|
||||
final String channelID;
|
||||
|
||||
@HiveField(10)
|
||||
final String ownerUserID;
|
||||
@HiveField(11)
|
||||
final String internalName;
|
||||
@HiveField(12)
|
||||
final String displayName;
|
||||
@HiveField(13)
|
||||
final String? descriptionName;
|
||||
@HiveField(14)
|
||||
final String? subscribeKey;
|
||||
@HiveField(15)
|
||||
final String timestampCreated;
|
||||
@HiveField(16)
|
||||
final String? timestampLastSent;
|
||||
@HiveField(17)
|
||||
final int messagesSent;
|
||||
|
||||
const Channel({
|
||||
Channel({
|
||||
required this.channelID,
|
||||
required this.ownerUserID,
|
||||
required this.internalName,
|
||||
@ -36,6 +51,25 @@ class Channel {
|
||||
messagesSent: json['messages_sent'] as int,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Channel[${this.channelID}]';
|
||||
}
|
||||
|
||||
List<(String, String)> debugFieldList() {
|
||||
return [
|
||||
('channelID', this.channelID),
|
||||
('ownerUserID', this.ownerUserID),
|
||||
('internalName', this.internalName),
|
||||
('displayName', this.displayName),
|
||||
('descriptionName', this.descriptionName ?? ''),
|
||||
('subscribeKey', this.subscribeKey ?? ''),
|
||||
('timestampCreated', this.timestampCreated),
|
||||
('timestampLastSent', this.timestampLastSent ?? ''),
|
||||
('messagesSent', '${this.messagesSent}'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelWithSubscription {
|
||||
|
65
flutter/lib/models/channel.g.dart
Normal file
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;
|
||||
}
|
@ -1,19 +1,39 @@
|
||||
class Message {
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:simplecloudnotifier/state/interfaces.dart';
|
||||
|
||||
part 'message.g.dart';
|
||||
|
||||
@HiveType(typeId: 105)
|
||||
class Message extends HiveObject implements FieldDebuggable {
|
||||
@HiveField(0)
|
||||
final String messageID;
|
||||
|
||||
@HiveField(10)
|
||||
final String senderUserID;
|
||||
@HiveField(11)
|
||||
final String channelInternalName;
|
||||
@HiveField(12)
|
||||
final String channelID;
|
||||
@HiveField(13)
|
||||
final String? senderName;
|
||||
@HiveField(14)
|
||||
final String senderIP;
|
||||
@HiveField(15)
|
||||
final String timestamp;
|
||||
@HiveField(16)
|
||||
final String title;
|
||||
@HiveField(17)
|
||||
final String? content;
|
||||
@HiveField(18)
|
||||
final int priority;
|
||||
@HiveField(19)
|
||||
final String? userMessageID;
|
||||
@HiveField(20)
|
||||
final String usedKeyID;
|
||||
@HiveField(21)
|
||||
final bool trimmed;
|
||||
|
||||
const Message({
|
||||
Message({
|
||||
required this.messageID,
|
||||
required this.senderUserID,
|
||||
required this.channelInternalName,
|
||||
@ -54,4 +74,27 @@ class Message {
|
||||
|
||||
return (npt, messages);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Message[${this.messageID}]';
|
||||
}
|
||||
|
||||
List<(String, String)> debugFieldList() {
|
||||
return [
|
||||
('messageID', this.messageID),
|
||||
('senderUserID', this.senderUserID),
|
||||
('channelInternalName', this.channelInternalName),
|
||||
('channelID', this.channelID),
|
||||
('senderName', this.senderName ?? ''),
|
||||
('senderIP', this.senderIP),
|
||||
('timestamp', this.timestamp),
|
||||
('title', this.title),
|
||||
('content', this.content ?? ''),
|
||||
('priority', '${this.priority}'),
|
||||
('userMessageID', this.userMessageID ?? ''),
|
||||
('usedKeyID', this.usedKeyID),
|
||||
('trimmed', '${this.trimmed}'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
77
flutter/lib/models/message.g.dart
Normal file
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;
|
||||
}
|
@ -66,11 +66,11 @@ class _SCNNavLayoutState extends State<SCNNavLayout> {
|
||||
),
|
||||
body: IndexedStack(
|
||||
children: [
|
||||
ExcludeFocus(excluding: _selectedIndex != 0, child: MessageListPage()),
|
||||
ExcludeFocus(excluding: _selectedIndex != 1, child: ChannelRootPage()),
|
||||
ExcludeFocus(excluding: _selectedIndex != 2, child: AccountRootPage()),
|
||||
ExcludeFocus(excluding: _selectedIndex != 3, child: SettingsRootPage()),
|
||||
ExcludeFocus(excluding: _selectedIndex != 4, child: SendRootPage()),
|
||||
ExcludeFocus(excluding: _selectedIndex != 0, child: MessageListPage(isVisiblePage: _selectedIndex == 0)),
|
||||
ExcludeFocus(excluding: _selectedIndex != 1, child: ChannelRootPage(isVisiblePage: _selectedIndex == 1)),
|
||||
ExcludeFocus(excluding: _selectedIndex != 2, child: AccountRootPage(isVisiblePage: _selectedIndex == 2)),
|
||||
ExcludeFocus(excluding: _selectedIndex != 3, child: SettingsRootPage(isVisiblePage: _selectedIndex == 3)),
|
||||
ExcludeFocus(excluding: _selectedIndex != 4, child: SendRootPage(isVisiblePage: _selectedIndex == 4)),
|
||||
],
|
||||
index: _selectedIndex,
|
||||
),
|
||||
|
@ -16,7 +16,9 @@ import 'package:simplecloudnotifier/utils/ui.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class AccountRootPage extends StatefulWidget {
|
||||
const AccountRootPage({super.key});
|
||||
const AccountRootPage({super.key, required this.isVisiblePage});
|
||||
|
||||
final bool isVisiblePage;
|
||||
|
||||
@override
|
||||
State<AccountRootPage> createState() => _AccountRootPageState();
|
||||
@ -33,13 +35,34 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
||||
|
||||
bool loading = false;
|
||||
|
||||
bool _isInitialized = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
userAcc = Provider.of<AppAuth>(context, listen: false);
|
||||
userAcc.addListener(_onAuthStateChanged);
|
||||
|
||||
if (widget.isVisiblePage && !_isInitialized) realInitState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(AccountRootPage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (oldWidget.isVisiblePage != widget.isVisiblePage && widget.isVisiblePage) {
|
||||
if (!_isInitialized) {
|
||||
realInitState();
|
||||
} else {
|
||||
//TODO background refresh
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void realInitState() {
|
||||
_onAuthStateChanged();
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -92,6 +115,8 @@ class _AccountRootPageState extends State<AccountRootPage> {
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<AppAuth>(
|
||||
builder: (context, acc, child) {
|
||||
if (!_isInitialized) return SizedBox();
|
||||
|
||||
if (!userAcc.isAuth()) {
|
||||
return _buildNoAuth(context);
|
||||
} else {
|
||||
|
@ -8,21 +8,26 @@ import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/pages/channel_list/channel_list_item.dart';
|
||||
|
||||
class ChannelRootPage extends StatefulWidget {
|
||||
const ChannelRootPage({super.key});
|
||||
const ChannelRootPage({super.key, required this.isVisiblePage});
|
||||
|
||||
final bool isVisiblePage;
|
||||
|
||||
@override
|
||||
State<ChannelRootPage> createState() => _ChannelRootPageState();
|
||||
}
|
||||
|
||||
class _ChannelRootPageState extends State<ChannelRootPage> {
|
||||
final PagingController<int, Channel> _pagingController = PagingController(firstPageKey: 0);
|
||||
final PagingController<int, Channel> _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: 0);
|
||||
|
||||
bool _isInitialized = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_pagingController.addPageRequestListener((pageKey) {
|
||||
_fetchPage(pageKey);
|
||||
});
|
||||
super.initState();
|
||||
|
||||
_pagingController.addPageRequestListener(_fetchPage);
|
||||
|
||||
if (widget.isVisiblePage && !_isInitialized) realInitState();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -31,9 +36,29 @@ class _ChannelRootPageState extends State<ChannelRootPage> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ChannelRootPage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (oldWidget.isVisiblePage != widget.isVisiblePage && widget.isVisiblePage) {
|
||||
if (!_isInitialized) {
|
||||
realInitState();
|
||||
} else {
|
||||
//TODO background refresh
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void realInitState() {
|
||||
_pagingController.refresh();
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
Future<void> _fetchPage(int pageKey) async {
|
||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
||||
|
||||
ApplicationLog.debug('Start ChannelList::_pagingController::_fetchPage [ ${pageKey} ]');
|
||||
|
||||
if (!acc.isAuth()) {
|
||||
_pagingController.error = 'Not logged in';
|
||||
return;
|
||||
|
@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:simplecloudnotifier/models/channel.dart';
|
||||
import 'package:simplecloudnotifier/models/message.dart';
|
||||
import 'package:simplecloudnotifier/pages/debug/debug_persistence_hive.dart';
|
||||
import 'package:simplecloudnotifier/pages/debug/debug_persistence_sharedprefs.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
@ -83,6 +85,42 @@ class _DebugPersistencePageState extends State<DebugPersistencePage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
Card.outlined(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
Navi.push(context, () => DebugHiveBoxPage(boxName: 'scn-message-cache', box: Hive.box<Message>('scn-message-cache')));
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SizedBox(width: 30, child: Text('')),
|
||||
Expanded(child: Text('Hive [scn-message-cache]', style: TextStyle(fontWeight: FontWeight.bold), textAlign: TextAlign.center)),
|
||||
SizedBox(width: 30, child: Text('${Hive.box<Message>('scn-message-cache').length.toString()}', textAlign: TextAlign.end)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Card.outlined(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
Navi.push(context, () => DebugHiveBoxPage(boxName: 'scn-channel-cache', box: Hive.box<Channel>('scn-channel-cache')));
|
||||
},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SizedBox(width: 30, child: Text('')),
|
||||
Expanded(child: Text('Hive [scn-channel-cache]', style: TextStyle(fontWeight: FontWeight.bold), textAlign: TextAlign.center)),
|
||||
SizedBox(width: 30, child: Text('${Hive.box<Channel>('scn-channel-cache').length.toString()}', textAlign: TextAlign.end)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -1,17 +1,21 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:simplecloudnotifier/api/api_client.dart';
|
||||
import 'package:simplecloudnotifier/models/channel.dart';
|
||||
import 'package:simplecloudnotifier/models/message.dart';
|
||||
import 'package:simplecloudnotifier/pages/message_view/message_view.dart';
|
||||
import 'package:simplecloudnotifier/state/app_bar_state.dart';
|
||||
import 'package:simplecloudnotifier/state/application_log.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
import 'package:simplecloudnotifier/pages/message_list/message_list_item.dart';
|
||||
import 'package:simplecloudnotifier/utils/navi.dart';
|
||||
|
||||
class MessageListPage extends StatefulWidget {
|
||||
const MessageListPage({super.key});
|
||||
const MessageListPage({super.key, required this.isVisiblePage});
|
||||
|
||||
final bool isVisiblePage;
|
||||
|
||||
//TODO reload on switch to tab
|
||||
//TODO reload on app to foreground
|
||||
@ -20,31 +24,90 @@ class MessageListPage extends StatefulWidget {
|
||||
State<MessageListPage> createState() => _MessageListPageState();
|
||||
}
|
||||
|
||||
class _MessageListPageState extends State<MessageListPage> {
|
||||
class _MessageListPageState extends State<MessageListPage> with RouteAware {
|
||||
static const _pageSize = 128;
|
||||
|
||||
final PagingController<String, Message> _pagingController = PagingController(firstPageKey: '@start');
|
||||
PagingController<String, Message> _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: '@start');
|
||||
|
||||
Map<String, Channel>? _channels = null;
|
||||
|
||||
bool _isInitialized = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
//TODO init with state from cache - also allow tho show cache on error
|
||||
_pagingController.addPageRequestListener((pageKey) {
|
||||
_fetchPage(pageKey);
|
||||
});
|
||||
super.initState();
|
||||
|
||||
_pagingController.addPageRequestListener(_fetchPage);
|
||||
|
||||
if (widget.isVisiblePage && !_isInitialized) realInitState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(MessageListPage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (oldWidget.isVisiblePage != widget.isVisiblePage && widget.isVisiblePage) {
|
||||
if (!_isInitialized) {
|
||||
realInitState();
|
||||
} else {
|
||||
_backgroundRefresh(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void realInitState() {
|
||||
final chnCache = Hive.box<Channel>('scn-channel-cache');
|
||||
final msgCache = Hive.box<Message>('scn-message-cache');
|
||||
|
||||
if (chnCache.isNotEmpty && msgCache.isNotEmpty) {
|
||||
// ==== Use cache values - and refresh in background
|
||||
|
||||
_channels = <String, Channel>{for (var v in chnCache.values) v.channelID: v};
|
||||
|
||||
final cacheMessages = msgCache.values.toList();
|
||||
cacheMessages.sort((a, b) => -1 * a.timestamp.compareTo(b.timestamp));
|
||||
|
||||
_pagingController.value = PagingState(nextPageKey: null, itemList: cacheMessages, error: null);
|
||||
|
||||
_backgroundRefresh(true);
|
||||
} else {
|
||||
// ==== Full refresh - no cache available
|
||||
_pagingController.refresh();
|
||||
}
|
||||
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
Navi.modalRouteObserver.subscribe(this, ModalRoute.of(context)!);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
Navi.modalRouteObserver.unsubscribe(this);
|
||||
_pagingController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didPush() {
|
||||
// Route was pushed onto navigator and is now the topmost route.
|
||||
ApplicationLog.debug('[MessageList::RouteObserver] --> didPush');
|
||||
}
|
||||
|
||||
@override
|
||||
void didPopNext() {
|
||||
// Covering route was popped off the navigator.
|
||||
ApplicationLog.debug('[MessageList::RouteObserver] --> didPopNext');
|
||||
}
|
||||
|
||||
Future<void> _fetchPage(String thisPageToken) async {
|
||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
||||
|
||||
ApplicationLog.debug('Start MessageList::_pagingController::_fetchPage [ ${thisPageToken} ]');
|
||||
|
||||
if (!acc.isAuth()) {
|
||||
_pagingController.error = 'Not logged in';
|
||||
return;
|
||||
@ -54,10 +117,16 @@ class _MessageListPageState extends State<MessageListPage> {
|
||||
if (_channels == null) {
|
||||
final channels = await APIClient.getChannelList(acc, ChannelSelector.allAny);
|
||||
_channels = <String, Channel>{for (var v in channels) v.channel.channelID: v.channel};
|
||||
|
||||
_setChannelCache(channels); // no await
|
||||
}
|
||||
|
||||
final (npt, newItems) = await APIClient.getMessageList(acc, thisPageToken, pageSize: _pageSize);
|
||||
|
||||
_addToMessageCache(newItems); // no await
|
||||
|
||||
ApplicationLog.debug('Finished MessageList::_pagingController::_fetchPage [ ${newItems.length} items and npt: ${thisPageToken} --> ${npt} ]');
|
||||
|
||||
if (npt == '@end') {
|
||||
_pagingController.appendLastPage(newItems);
|
||||
} else {
|
||||
@ -69,6 +138,71 @@ class _MessageListPageState extends State<MessageListPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _backgroundRefresh(bool fullReplaceState) async {
|
||||
final acc = Provider.of<AppAuth>(context, listen: false);
|
||||
|
||||
ApplicationLog.debug('Start background refresh of message list (fullReplaceState: $fullReplaceState)');
|
||||
|
||||
try {
|
||||
await Future.delayed(const Duration(seconds: 0), () {}); // this is annoyingly important - otherwise we call setLoadingIndeterminate directly in initStat() and get an exception....
|
||||
|
||||
AppBarState().setLoadingIndeterminate(true);
|
||||
|
||||
if (_channels == null || fullReplaceState) {
|
||||
final channels = await APIClient.getChannelList(acc, ChannelSelector.allAny);
|
||||
setState(() {
|
||||
_channels = <String, Channel>{for (var v in channels) v.channel.channelID: v.channel};
|
||||
});
|
||||
_setChannelCache(channels); // no await
|
||||
}
|
||||
|
||||
final (npt, newItems) = await APIClient.getMessageList(acc, '@start', pageSize: _pageSize);
|
||||
|
||||
_addToMessageCache(newItems); // no await
|
||||
|
||||
if (fullReplaceState) {
|
||||
// fully replace/reset state
|
||||
ApplicationLog.debug('Background-refresh finished (fullReplaceState) - replace state with ${newItems.length} items and npt: [ $npt ]');
|
||||
setState(() {
|
||||
if (npt == '@end')
|
||||
_pagingController.value = PagingState(nextPageKey: null, itemList: newItems, error: null);
|
||||
else
|
||||
_pagingController.value = PagingState(nextPageKey: npt, itemList: newItems, error: null);
|
||||
});
|
||||
} else {
|
||||
final itemsToBeAdded = newItems.where((p1) => !(_pagingController.itemList ?? []).any((p2) => p1.messageID == p2.messageID)).toList();
|
||||
if (itemsToBeAdded.isEmpty) {
|
||||
// nothing to do - no new items...
|
||||
// ....
|
||||
ApplicationLog.debug('Background-refresh returned no new items - nothing to do.');
|
||||
} else if (itemsToBeAdded.length == newItems.length) {
|
||||
// all items are new ?!?, the current state is completely fucked - full replace
|
||||
ApplicationLog.debug('Background-refresh found only new items ?!? - fully replace state with ${newItems.length} items');
|
||||
setState(() {
|
||||
if (npt == '@end')
|
||||
_pagingController.value = PagingState(nextPageKey: null, itemList: newItems, error: null);
|
||||
else
|
||||
_pagingController.value = PagingState(nextPageKey: npt, itemList: newItems, error: null);
|
||||
_pagingController.itemList = null;
|
||||
});
|
||||
} else {
|
||||
// add new items to the front
|
||||
ApplicationLog.debug('Background-refresh found ${newItems.length} new items - add to front');
|
||||
setState(() {
|
||||
_pagingController.itemList = itemsToBeAdded + (_pagingController.itemList ?? []);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (exc, trace) {
|
||||
setState(() {
|
||||
_pagingController.error = exc.toString();
|
||||
});
|
||||
ApplicationLog.error('Failed to list messages: ' + exc.toString(), trace: trace);
|
||||
} finally {
|
||||
AppBarState().setLoadingIndeterminate(false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
@ -92,4 +226,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:simplecloudnotifier/state/app_auth.dart';
|
||||
|
||||
class SendRootPage extends StatefulWidget {
|
||||
const SendRootPage({super.key});
|
||||
const SendRootPage({super.key, required bool isVisiblePage});
|
||||
|
||||
@override
|
||||
State<SendRootPage> createState() => _SendRootPageState();
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SettingsRootPage extends StatefulWidget {
|
||||
const SettingsRootPage({super.key});
|
||||
const SettingsRootPage({super.key, required bool isVisiblePage});
|
||||
|
||||
@override
|
||||
State<SettingsRootPage> createState() => _SettingsRootPageState();
|
||||
|
@ -14,6 +14,7 @@ class AppAuth extends ChangeNotifier implements TokenSource {
|
||||
|
||||
User? _user;
|
||||
Client? _client;
|
||||
DateTime? _clientQueryTime;
|
||||
|
||||
String? get userID => _userID;
|
||||
String? get tokenAdmin => _tokenAdmin;
|
||||
@ -124,7 +125,11 @@ class AppAuth extends ChangeNotifier implements TokenSource {
|
||||
return user;
|
||||
}
|
||||
|
||||
Future<Client?> loadClient({bool force = false}) async {
|
||||
Future<Client?> loadClient({bool force = false, Duration? forceIfOlder = null}) async {
|
||||
if (forceIfOlder != null && _clientQueryTime != null && _clientQueryTime!.difference(DateTime.now()) > forceIfOlder) {
|
||||
force = true;
|
||||
}
|
||||
|
||||
if (!force && _client != null && _client!.clientID == _clientID) {
|
||||
return _client!;
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ 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);
|
||||
|
Loading…
Reference in New Issue
Block a user