mirror of
https://git.selfprivacy.org/kherel/selfprivacy.org.app.git
synced 2024-11-10 19:03:12 +00:00
fix: Save the scroll position when new server logs added via websocket
This commit is contained in:
parent
58bfa6db93
commit
1c7724347f
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
@ -16,7 +17,16 @@ class ServerLogsBloc extends Bloc<ServerLogsEvent, ServerLogsState> {
|
||||||
emit(ServerLogsLoading());
|
emit(ServerLogsLoading());
|
||||||
try {
|
try {
|
||||||
final (logsData, meta) = await _getLogs(limit: 50);
|
final (logsData, meta) = await _getLogs(limit: 50);
|
||||||
emit(ServerLogsLoaded(logsData, meta, false));
|
emit(
|
||||||
|
ServerLogsLoaded(
|
||||||
|
logsData.sorted(
|
||||||
|
(final a, final b) => b.timestamp.compareTo(a.timestamp),
|
||||||
|
),
|
||||||
|
List<ServerLogEntry>.empty(growable: true),
|
||||||
|
meta,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
);
|
||||||
if (_apiLogsSubscription != null) {
|
if (_apiLogsSubscription != null) {
|
||||||
await _apiLogsSubscription?.cancel();
|
await _apiLogsSubscription?.cancel();
|
||||||
}
|
}
|
||||||
|
@ -41,10 +51,17 @@ class ServerLogsBloc extends Bloc<ServerLogsEvent, ServerLogsState> {
|
||||||
try {
|
try {
|
||||||
final (logsData, meta) =
|
final (logsData, meta) =
|
||||||
await _getLogs(limit: 50, downCursor: currentState.meta.upCursor);
|
await _getLogs(limit: 50, downCursor: currentState.meta.upCursor);
|
||||||
final allEntries = currentState.entries
|
final allEntries = currentState.oldEntries
|
||||||
..addAll(logsData)
|
..addAll(logsData)
|
||||||
..sort((final a, final b) => b.timestamp.compareTo(a.timestamp));
|
..sort((final a, final b) => b.timestamp.compareTo(a.timestamp));
|
||||||
emit(ServerLogsLoaded(allEntries.toSet().toList(), meta, false));
|
emit(
|
||||||
|
ServerLogsLoaded(
|
||||||
|
allEntries.toSet().toList(),
|
||||||
|
currentState.newEntries,
|
||||||
|
meta,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(ServerLogsError(e.toString()));
|
emit(ServerLogsError(e.toString()));
|
||||||
}
|
}
|
||||||
|
@ -54,19 +71,19 @@ class ServerLogsBloc extends Bloc<ServerLogsEvent, ServerLogsState> {
|
||||||
on<ServerLogsGotNewEntry>((final event, final emit) {
|
on<ServerLogsGotNewEntry>((final event, final emit) {
|
||||||
final currentState = state;
|
final currentState = state;
|
||||||
if (currentState is ServerLogsLoaded) {
|
if (currentState is ServerLogsLoaded) {
|
||||||
final entries = currentState.entries;
|
final allEntries = currentState.newEntries
|
||||||
if (!entries.any((final entry) => entry.cursor == event.entry.cursor)) {
|
..add(event.entry)
|
||||||
entries.add(event.entry);
|
..sort(
|
||||||
entries
|
(final a, final b) => b.timestamp.compareTo(a.timestamp),
|
||||||
.sort((final a, final b) => b.timestamp.compareTo(a.timestamp));
|
|
||||||
emit(
|
|
||||||
ServerLogsLoaded(
|
|
||||||
entries,
|
|
||||||
currentState.meta,
|
|
||||||
currentState.loadingMore,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
emit(
|
||||||
|
ServerLogsLoaded(
|
||||||
|
currentState.oldEntries,
|
||||||
|
allEntries.toSet().toList(),
|
||||||
|
currentState.meta,
|
||||||
|
currentState.loadingMore,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -15,30 +15,48 @@ final class ServerLogsLoading extends ServerLogsState {
|
||||||
}
|
}
|
||||||
|
|
||||||
final class ServerLogsLoaded extends ServerLogsState {
|
final class ServerLogsLoaded extends ServerLogsState {
|
||||||
const ServerLogsLoaded(this.entries, this.meta, this.loadingMore);
|
ServerLogsLoaded(
|
||||||
|
this.oldEntries,
|
||||||
|
this.newEntries,
|
||||||
|
this.meta,
|
||||||
|
this.loadingMore,
|
||||||
|
) : _lastCursor = newEntries.isEmpty ? '' : newEntries.first.cursor;
|
||||||
|
|
||||||
final List<ServerLogEntry> entries;
|
final List<ServerLogEntry> oldEntries;
|
||||||
|
final List<ServerLogEntry> newEntries;
|
||||||
final ServerLogsPageMeta meta;
|
final ServerLogsPageMeta meta;
|
||||||
final bool loadingMore;
|
final bool loadingMore;
|
||||||
|
final String _lastCursor;
|
||||||
|
|
||||||
List<String> get systemdUnits => entries
|
List<String> get systemdUnits => oldEntries
|
||||||
.map((final entry) => entry.systemdUnit ?? 'kernel')
|
.map((final entry) => entry.systemdUnit ?? 'kernel')
|
||||||
.toSet()
|
.toSet()
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
(List<ServerLogEntry>, int) entriesForUnit(final String unit) {
|
List<ServerLogEntry> oldEntriesForUnit(final String unit) {
|
||||||
if (unit == 'kernel') {
|
if (unit == 'kernel') {
|
||||||
final filteredEntries =
|
final filteredEntries =
|
||||||
entries.where((final entry) => entry.systemdUnit == null).toList();
|
oldEntries.where((final entry) => entry.systemdUnit == null).toList();
|
||||||
return (filteredEntries, filteredEntries.length);
|
return filteredEntries;
|
||||||
}
|
}
|
||||||
final filteredEntries =
|
final filteredEntries =
|
||||||
entries.where((final entry) => entry.systemdUnit == unit).toList();
|
oldEntries.where((final entry) => entry.systemdUnit == unit).toList();
|
||||||
return (filteredEntries, filteredEntries.length);
|
return filteredEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ServerLogEntry> newEntriesForUnit(final String unit) {
|
||||||
|
if (unit == 'kernel') {
|
||||||
|
final filteredEntries =
|
||||||
|
newEntries.where((final entry) => entry.systemdUnit == null).toList();
|
||||||
|
return filteredEntries;
|
||||||
|
}
|
||||||
|
final filteredEntries =
|
||||||
|
newEntries.where((final entry) => entry.systemdUnit == unit).toList();
|
||||||
|
return filteredEntries;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [entries, meta];
|
List<Object> get props => [oldEntries, newEntries, meta, _lastCursor];
|
||||||
}
|
}
|
||||||
|
|
||||||
final class ServerLogsError extends ServerLogsState {
|
final class ServerLogsError extends ServerLogsState {
|
||||||
|
|
|
@ -78,70 +78,89 @@ class _ServerLogsScreenState extends State<ServerLogsScreen> {
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(final BuildContext context) => Scaffold(
|
Widget build(final BuildContext context) {
|
||||||
appBar: AppBar(
|
const Key centerKey = ValueKey<String>('server-logs-center-key');
|
||||||
title: Text('server.logs'.tr()),
|
return Scaffold(
|
||||||
),
|
appBar: AppBar(
|
||||||
endDrawer: BlocBuilder<ServerLogsBloc, ServerLogsState>(
|
title: Text('server.logs'.tr()),
|
||||||
builder: (final context, final state) {
|
),
|
||||||
if (state is ServerLogsLoaded) {
|
endDrawer: BlocBuilder<ServerLogsBloc, ServerLogsState>(
|
||||||
return _buildDrawer(state.systemdUnits);
|
builder: (final context, final state) {
|
||||||
}
|
if (state is ServerLogsLoaded) {
|
||||||
// Return an empty drawer if the state is not loaded
|
return _buildDrawer(state.systemdUnits);
|
||||||
return const Drawer(child: SizedBox());
|
}
|
||||||
},
|
// Return an empty drawer if the state is not loaded
|
||||||
),
|
return const Drawer(child: SizedBox());
|
||||||
body: BlocBuilder<ServerLogsBloc, ServerLogsState>(
|
},
|
||||||
builder: (final context, final state) {
|
),
|
||||||
final isLoadingMore =
|
body: BlocBuilder<ServerLogsBloc, ServerLogsState>(
|
||||||
state is ServerLogsLoaded && state.loadingMore;
|
builder: (final context, final state) {
|
||||||
if (state is ServerLogsLoading) {
|
final isLoadingMore = state is ServerLogsLoaded && state.loadingMore;
|
||||||
return const Center(child: CircularProgressIndicator());
|
if (state is ServerLogsLoading) {
|
||||||
} else if (state is ServerLogsLoaded) {
|
return const Center(child: CircularProgressIndicator());
|
||||||
if (_selectedSystemdUnit == null) {
|
} else if (state is ServerLogsLoaded) {
|
||||||
return ListView.builder(
|
final List<ServerLogEntry> filteredNewLogs =
|
||||||
controller: _scrollController,
|
_selectedSystemdUnit == null
|
||||||
itemCount: state.entries.length + (isLoadingMore ? 1 : 0),
|
? state.newEntries
|
||||||
itemBuilder: (final context, final index) {
|
: state.newEntriesForUnit(_selectedSystemdUnit!);
|
||||||
if (isLoadingMore && index == state.entries.length) {
|
final List<ServerLogEntry> filteredOldLogs =
|
||||||
return const Center(child: CircularProgressIndicator());
|
_selectedSystemdUnit == null
|
||||||
}
|
? state.oldEntries
|
||||||
final logEntry = state.entries[index];
|
: state.oldEntriesForUnit(_selectedSystemdUnit!);
|
||||||
return LogEntryWidget(
|
return CustomScrollView(
|
||||||
logEntry: logEntry,
|
center: centerKey,
|
||||||
key: ValueKey(logEntry.cursor),
|
controller: _scrollController,
|
||||||
);
|
slivers: [
|
||||||
},
|
SliverList(
|
||||||
);
|
delegate: SliverChildBuilderDelegate(
|
||||||
} else {
|
(final context, final index) {
|
||||||
final (filteredLogs, filteredLength) =
|
final logEntry =
|
||||||
state.entriesForUnit(_selectedSystemdUnit!);
|
filteredNewLogs[(filteredNewLogs.length - 1) - index];
|
||||||
return ListView.builder(
|
return LogEntryWidget(
|
||||||
controller: _scrollController,
|
logEntry: logEntry,
|
||||||
itemCount: filteredLength + (isLoadingMore ? 1 : 0),
|
key: ValueKey(logEntry.cursor),
|
||||||
itemBuilder: (final context, final index) {
|
);
|
||||||
if (isLoadingMore && index == filteredLength) {
|
},
|
||||||
return const Center(child: CircularProgressIndicator());
|
childCount: filteredNewLogs.length,
|
||||||
}
|
),
|
||||||
final logEntry = filteredLogs[index];
|
),
|
||||||
return LogEntryWidget(
|
SliverList(
|
||||||
logEntry: logEntry,
|
key: centerKey,
|
||||||
key: ValueKey(logEntry.cursor),
|
delegate: SliverChildBuilderDelegate(
|
||||||
);
|
(final context, final index) {
|
||||||
},
|
if (isLoadingMore && index == filteredOldLogs.length) {
|
||||||
);
|
return const Center(
|
||||||
}
|
child: CircularProgressIndicator(),
|
||||||
} else if (state is ServerLogsError) {
|
);
|
||||||
return EmptyPagePlaceholder(
|
}
|
||||||
title: 'basis.error'.tr(),
|
final logEntry = filteredOldLogs[index];
|
||||||
iconData: Icons.error_outline,
|
return LogEntryWidget(
|
||||||
description: state.error.toString(),
|
logEntry: logEntry,
|
||||||
);
|
key: ValueKey(logEntry.cursor),
|
||||||
}
|
);
|
||||||
return Center(child: Text('server.no_logs'.tr()));
|
},
|
||||||
},
|
childCount:
|
||||||
),
|
filteredOldLogs.length + (isLoadingMore ? 1 : 0),
|
||||||
);
|
),
|
||||||
|
),
|
||||||
|
if (isLoadingMore)
|
||||||
|
const SliverFillRemaining(
|
||||||
|
child: Center(child: CircularProgressIndicator()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else if (state is ServerLogsError) {
|
||||||
|
return EmptyPagePlaceholder(
|
||||||
|
title: 'basis.error'.tr(),
|
||||||
|
iconData: Icons.error_outline,
|
||||||
|
description: state.error.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Center(child: Text('server.no_logs'.tr()));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LogEntryWidget extends StatelessWidget {
|
class LogEntryWidget extends StatelessWidget {
|
||||||
|
|
Loading…
Reference in a new issue