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 '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<ServerLogsEvent, ServerLogsState> {
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<ServerLogEntry>.empty(growable: true),
meta,
false,
),
);
if (_apiLogsSubscription != null) {
await _apiLogsSubscription?.cancel();
}
@ -41,10 +51,17 @@ class ServerLogsBloc extends Bloc<ServerLogsEvent, ServerLogsState> {
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<ServerLogsEvent, ServerLogsState> {
on<ServerLogsGotNewEntry>((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,
),
);
}
});

View file

@ -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<ServerLogEntry> entries;
final List<ServerLogEntry> oldEntries;
final List<ServerLogEntry> newEntries;
final ServerLogsPageMeta meta;
final bool loadingMore;
final String _lastCursor;
List<String> get systemdUnits => entries
List<String> get systemdUnits => oldEntries
.map((final entry) => entry.systemdUnit ?? 'kernel')
.toSet()
.toList();
(List<ServerLogEntry>, int) entriesForUnit(final String unit) {
List<ServerLogEntry> 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<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
List<Object> get props => [entries, meta];
List<Object> get props => [oldEntries, newEntries, meta, _lastCursor];
}
final class ServerLogsError extends ServerLogsState {

View file

@ -78,70 +78,89 @@ class _ServerLogsScreenState extends State<ServerLogsScreen> {
);
@override
Widget build(final BuildContext context) => Scaffold(
appBar: AppBar(
title: Text('server.logs'.tr()),
),
endDrawer: BlocBuilder<ServerLogsBloc, ServerLogsState>(
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<ServerLogsBloc, ServerLogsState>(
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<String>('server-logs-center-key');
return Scaffold(
appBar: AppBar(
title: Text('server.logs'.tr()),
),
endDrawer: BlocBuilder<ServerLogsBloc, ServerLogsState>(
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<ServerLogsBloc, ServerLogsState>(
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<ServerLogEntry> filteredNewLogs =
_selectedSystemdUnit == null
? state.newEntries
: state.newEntriesForUnit(_selectedSystemdUnit!);
final List<ServerLogEntry> 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 {