mirror of
https://git.selfprivacy.org/kherel/selfprivacy.org.app.git
synced 2024-09-21 10:57:52 +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 '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,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue