mirror of
https://github.com/lichess-org/mobile.git
synced 2026-05-26 13:50:52 +00:00
Add search to app logs
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user