fix: Save the scroll position when new server logs added via websocket

This commit is contained in:
Inex Code 2024-07-26 22:56:40 +03:00
parent 58bfa6db93
commit 1c7724347f
3 changed files with 142 additions and 88 deletions

View file

@ -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,
),
);
} }
}); });

View file

@ -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 {

View file

@ -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 {