Add search to app logs

This commit is contained in:
Rafal Sroka
2026-03-19 21:47:57 +01:00
parent 538b2c06d0
commit 591cd6dbd6
3 changed files with 69 additions and 18 deletions
+19 -5
View File
@@ -10,20 +10,33 @@ part 'app_log_paginator.freezed.dart';
const _pageSize = 20;
/// A provider for [AppLogPaginator].
final appLogPaginatorProvider = AsyncNotifierProvider.autoDispose<AppLogPaginator, AppLogState>(
AppLogPaginator.new,
name: 'AppLogPaginatorProvider',
);
///
/// The family argument is the optional search query string.
final appLogPaginatorProvider = AsyncNotifierProvider.autoDispose
.family<AppLogPaginator, AppLogState, String?>(
AppLogPaginator.new,
name: 'AppLogPaginatorProvider',
);
/// A Riverpod controller for managing paginated app log entries.
class AppLogPaginator extends AsyncNotifier<AppLogState> {
AppLogPaginator(this._searchQuery);
final String? _searchQuery;
@override
Future<AppLogState> build() async {
final storage = await ref.read(appLogStorageProvider.future);
final minLevelValue = ref.watch(logPreferencesProvider.select((p) => p.level.value));
return AppLogState(
data: IList.new([
await AsyncValue.guard(() => storage.page(limit: _pageSize, minLevelValue: minLevelValue)),
await AsyncValue.guard(
() => storage.page(
limit: _pageSize,
minLevelValue: minLevelValue,
searchQuery: _searchQuery,
),
),
]),
);
}
@@ -38,6 +51,7 @@ class AppLogPaginator extends AsyncNotifier<AppLogState> {
limit: _pageSize,
cursor: state.requireValue.nextPage,
minLevelValue: minLevelValue,
searchQuery: _searchQuery,
),
);
state = AsyncValue.data(
+27 -8
View File
@@ -24,18 +24,37 @@ class AppLogStorage {
/// Retrieves a paginated list of [AppLogEntry] entries from the database.
///
/// If [minLevelValue] is provided, only entries with a level value greater than
/// or equal to [minLevelValue] are returned.
Future<AppLogPage> page({int? cursor, int? minLevelValue, int limit = 100}) async {
final whereClause = [
if (cursor != null) 'id <= $cursor',
if (minLevelValue != null) 'levelValue >= $minLevelValue',
];
/// [minLevelValue] filters entries at or above the given log level.
/// [searchQuery] filters entries whose message, logger name, or error contain the query string.
Future<AppLogPage> page({
int? cursor,
int? minLevelValue,
String? searchQuery,
int limit = 100,
}) async {
final whereParts = <String>[];
final args = <dynamic>[];
if (cursor != null) {
whereParts.add('id <= ?');
args.add(cursor);
}
if (minLevelValue != null) {
whereParts.add('levelValue >= ?');
args.add(minLevelValue);
}
if (searchQuery != null && searchQuery.isNotEmpty) {
whereParts.add('(message LIKE ? OR loggerName LIKE ? OR error LIKE ?)');
final pattern = '%$searchQuery%';
args.addAll([pattern, pattern, pattern]);
}
final res = await _db.query(
kAppLogStorageTable,
limit: limit + 1,
orderBy: 'id DESC',
where: whereClause.isNotEmpty ? whereClause.join(' AND ') : null,
where: whereParts.isNotEmpty ? whereParts.join(' AND ') : null,
whereArgs: args.isNotEmpty ? args : null,
);
return AppLogPage(
items: res.take(limit).map(AppLogEntry.fromJson).toIList(),
@@ -12,6 +12,7 @@ import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart';
import 'package:lichess_mobile/src/widgets/adaptive_choice_picker.dart';
import 'package:lichess_mobile/src/widgets/haptic_refresh_indicator.dart';
import 'package:lichess_mobile/src/widgets/list.dart';
import 'package:lichess_mobile/src/widgets/platform_search_bar.dart';
import 'package:lichess_mobile/src/widgets/settings.dart';
import 'package:logging/logging.dart';
import 'package:share_plus/share_plus.dart';
@@ -33,6 +34,8 @@ class AppLogSettingsScreen extends ConsumerStatefulWidget {
class _AppLogSettingsScreenState extends ConsumerState<AppLogSettingsScreen> {
final ScrollController _scrollController = ScrollController();
final TextEditingController _searchController = TextEditingController();
String? _searchQuery;
@override
void initState() {
@@ -44,27 +47,28 @@ class _AppLogSettingsScreenState extends ConsumerState<AppLogSettingsScreen> {
void dispose() {
_scrollController.removeListener(_scrollListener);
_scrollController.dispose();
_searchController.dispose();
super.dispose();
}
void _scrollListener() {
if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 300) {
final currentState = ref.read(appLogPaginatorProvider);
final currentState = ref.read(appLogPaginatorProvider(_searchQuery));
if (currentState.hasValue && !currentState.isLoading && currentState.requireValue.hasMore) {
ref.read(appLogPaginatorProvider.notifier).next();
ref.read(appLogPaginatorProvider(_searchQuery).notifier).next();
}
}
}
Future<void> _onRefresh() async {
await Future<void>.delayed(const Duration(milliseconds: 300));
return ref.read(appLogPaginatorProvider.notifier).refresh();
return ref.read(appLogPaginatorProvider(_searchQuery).notifier).refresh();
}
@override
Widget build(BuildContext context) {
final currentLevel = ref.watch(logPreferencesProvider.select((prefs) => prefs.level));
final asyncState = ref.watch(appLogPaginatorProvider);
final asyncState = ref.watch(appLogPaginatorProvider(_searchQuery));
final logs = asyncState.value?.logs ?? [];
return Scaffold(
@@ -97,7 +101,7 @@ class _AppLogSettingsScreenState extends ConsumerState<AppLogSettingsScreen> {
title: const Text('Delete all logs'),
onConfirm: () {
ref.read(appLogServiceProvider).clear();
ref.read(appLogPaginatorProvider.notifier).deleteAll();
ref.read(appLogPaginatorProvider(_searchQuery).notifier).deleteAll();
},
);
},
@@ -126,6 +130,20 @@ class _AppLogSettingsScreenState extends ConsumerState<AppLogSettingsScreen> {
),
],
),
Padding(
padding: Styles.bodySectionPadding,
child: PlatformSearchBar(
controller: _searchController,
hintText: 'Search logs...',
onChanged: (value) => setState(() {
_searchQuery = value.isEmpty ? null : value;
}),
onClear: () => setState(() {
_searchQuery = null;
_searchController.clear();
}),
),
),
Expanded(
child: switch (asyncState) {
AsyncData(:final value) when value.logs.isEmpty => Center(