From 1c7724347fceb05755765e061e69557818693cac Mon Sep 17 00:00:00 2001 From: Inex Code Date: Fri, 26 Jul 2024 22:56:40 +0300 Subject: [PATCH] fix: Save the scroll position when new server logs added via websocket --- .../bloc/server_logs/server_logs_bloc.dart | 47 ++++-- .../bloc/server_logs/server_logs_state.dart | 36 +++-- .../server_details/logs/logs_screen.dart | 147 ++++++++++-------- 3 files changed, 142 insertions(+), 88 deletions(-) diff --git a/lib/logic/bloc/server_logs/server_logs_bloc.dart b/lib/logic/bloc/server_logs/server_logs_bloc.dart index e528600e..0741dfe8 100644 --- a/lib/logic/bloc/server_logs/server_logs_bloc.dart +++ b/lib/logic/bloc/server_logs/server_logs_bloc.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -16,7 +17,16 @@ class ServerLogsBloc extends Bloc { emit(ServerLogsLoading()); try { 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.empty(growable: true), + meta, + false, + ), + ); if (_apiLogsSubscription != null) { await _apiLogsSubscription?.cancel(); } @@ -41,10 +51,17 @@ class ServerLogsBloc extends Bloc { try { final (logsData, meta) = await _getLogs(limit: 50, downCursor: currentState.meta.upCursor); - final allEntries = currentState.entries + final allEntries = currentState.oldEntries ..addAll(logsData) ..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) { emit(ServerLogsError(e.toString())); } @@ -54,19 +71,19 @@ class ServerLogsBloc extends Bloc { on((final event, final emit) { final currentState = state; if (currentState is ServerLogsLoaded) { - final entries = currentState.entries; - if (!entries.any((final entry) => entry.cursor == event.entry.cursor)) { - entries.add(event.entry); - entries - .sort((final a, final b) => b.timestamp.compareTo(a.timestamp)); - emit( - ServerLogsLoaded( - entries, - currentState.meta, - currentState.loadingMore, - ), + final allEntries = currentState.newEntries + ..add(event.entry) + ..sort( + (final a, final b) => b.timestamp.compareTo(a.timestamp), ); - } + emit( + ServerLogsLoaded( + currentState.oldEntries, + allEntries.toSet().toList(), + currentState.meta, + currentState.loadingMore, + ), + ); } }); diff --git a/lib/logic/bloc/server_logs/server_logs_state.dart b/lib/logic/bloc/server_logs/server_logs_state.dart index 66d28314..9f8c6d15 100644 --- a/lib/logic/bloc/server_logs/server_logs_state.dart +++ b/lib/logic/bloc/server_logs/server_logs_state.dart @@ -15,30 +15,48 @@ final class ServerLogsLoading 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 entries; + final List oldEntries; + final List newEntries; final ServerLogsPageMeta meta; final bool loadingMore; + final String _lastCursor; - List get systemdUnits => entries + List get systemdUnits => oldEntries .map((final entry) => entry.systemdUnit ?? 'kernel') .toSet() .toList(); - (List, int) entriesForUnit(final String unit) { + List oldEntriesForUnit(final String unit) { if (unit == 'kernel') { final filteredEntries = - entries.where((final entry) => entry.systemdUnit == null).toList(); - return (filteredEntries, filteredEntries.length); + oldEntries.where((final entry) => entry.systemdUnit == null).toList(); + return filteredEntries; } final filteredEntries = - entries.where((final entry) => entry.systemdUnit == unit).toList(); - return (filteredEntries, filteredEntries.length); + oldEntries.where((final entry) => entry.systemdUnit == unit).toList(); + return filteredEntries; + } + + List 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 - List get props => [entries, meta]; + List get props => [oldEntries, newEntries, meta, _lastCursor]; } final class ServerLogsError extends ServerLogsState { diff --git a/lib/ui/pages/server_details/logs/logs_screen.dart b/lib/ui/pages/server_details/logs/logs_screen.dart index 3c6e11ec..89e94218 100644 --- a/lib/ui/pages/server_details/logs/logs_screen.dart +++ b/lib/ui/pages/server_details/logs/logs_screen.dart @@ -78,70 +78,89 @@ class _ServerLogsScreenState extends State { ); @override - Widget build(final BuildContext context) => Scaffold( - appBar: AppBar( - title: Text('server.logs'.tr()), - ), - endDrawer: BlocBuilder( - builder: (final context, final state) { - if (state is ServerLogsLoaded) { - return _buildDrawer(state.systemdUnits); - } - // Return an empty drawer if the state is not loaded - return const Drawer(child: SizedBox()); - }, - ), - body: BlocBuilder( - builder: (final context, final state) { - final isLoadingMore = - state is ServerLogsLoaded && state.loadingMore; - if (state is ServerLogsLoading) { - return const Center(child: CircularProgressIndicator()); - } else if (state is ServerLogsLoaded) { - if (_selectedSystemdUnit == null) { - return ListView.builder( - controller: _scrollController, - itemCount: state.entries.length + (isLoadingMore ? 1 : 0), - itemBuilder: (final context, final index) { - if (isLoadingMore && index == state.entries.length) { - return const Center(child: CircularProgressIndicator()); - } - final logEntry = state.entries[index]; - return LogEntryWidget( - logEntry: logEntry, - key: ValueKey(logEntry.cursor), - ); - }, - ); - } else { - final (filteredLogs, filteredLength) = - state.entriesForUnit(_selectedSystemdUnit!); - return ListView.builder( - controller: _scrollController, - itemCount: filteredLength + (isLoadingMore ? 1 : 0), - itemBuilder: (final context, final index) { - if (isLoadingMore && index == filteredLength) { - return const Center(child: CircularProgressIndicator()); - } - final logEntry = filteredLogs[index]; - return LogEntryWidget( - logEntry: logEntry, - key: ValueKey(logEntry.cursor), - ); - }, - ); - } - } 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())); - }, - ), - ); + Widget build(final BuildContext context) { + const Key centerKey = ValueKey('server-logs-center-key'); + return Scaffold( + appBar: AppBar( + title: Text('server.logs'.tr()), + ), + endDrawer: BlocBuilder( + builder: (final context, final state) { + if (state is ServerLogsLoaded) { + return _buildDrawer(state.systemdUnits); + } + // Return an empty drawer if the state is not loaded + return const Drawer(child: SizedBox()); + }, + ), + body: BlocBuilder( + builder: (final context, final state) { + final isLoadingMore = state is ServerLogsLoaded && state.loadingMore; + if (state is ServerLogsLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is ServerLogsLoaded) { + final List filteredNewLogs = + _selectedSystemdUnit == null + ? state.newEntries + : state.newEntriesForUnit(_selectedSystemdUnit!); + final List filteredOldLogs = + _selectedSystemdUnit == null + ? state.oldEntries + : state.oldEntriesForUnit(_selectedSystemdUnit!); + return CustomScrollView( + center: centerKey, + controller: _scrollController, + slivers: [ + SliverList( + delegate: SliverChildBuilderDelegate( + (final context, final index) { + final logEntry = + filteredNewLogs[(filteredNewLogs.length - 1) - index]; + return LogEntryWidget( + logEntry: logEntry, + key: ValueKey(logEntry.cursor), + ); + }, + childCount: filteredNewLogs.length, + ), + ), + SliverList( + key: centerKey, + delegate: SliverChildBuilderDelegate( + (final context, final index) { + if (isLoadingMore && index == filteredOldLogs.length) { + return const Center( + child: CircularProgressIndicator(), + ); + } + final logEntry = filteredOldLogs[index]; + return LogEntryWidget( + logEntry: logEntry, + key: ValueKey(logEntry.cursor), + ); + }, + 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 {