From 05f7792012af3ed0d694a2340be0acf46f072e23 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 11 Sep 2025 06:04:22 +0530 Subject: [PATCH 1/4] [mob] Fix incorrect casting --- mobile/apps/photos/lib/db/upload_locks_db.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/apps/photos/lib/db/upload_locks_db.dart b/mobile/apps/photos/lib/db/upload_locks_db.dart index b01a06584a..a60a185eed 100644 --- a/mobile/apps/photos/lib/db/upload_locks_db.dart +++ b/mobile/apps/photos/lib/db/upload_locks_db.dart @@ -183,7 +183,7 @@ class UploadLocksDB { return "No lock found for $id"; } final row = rows.first; - final time = row[_uploadLocksTable.columnTime] as int; + final time = int.tryParse(row[_uploadLocksTable.columnTime].toString()) ?? 0 ; final owner = row[_uploadLocksTable.columnOwner] as String; final duration = DateTime.now().millisecondsSinceEpoch - time; return "Lock for $id acquired by $owner since ${Duration(milliseconds: duration)}"; From 25287c64f59251a2a6eac326e9b02e7a3a00522b Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 11 Sep 2025 06:25:19 +0530 Subject: [PATCH 2/4] Update log_viewer docs to reflect simplified integration API - Add prefix parameter documentation to LogViewer.initialize() - Remove callback-based integration examples - Simplify SuperLogging integration to direct initialization - Update all code examples to use LogViewer.openViewer() - Correct database entry limit from 2000 to 10000 - Clarify automatic log capture via Logger.root.onRecord Co-Authored-By: Claude --- mobile/packages/log_viewer/README.md | 10 ++- .../log_viewer/example_integration.md | 88 ++++++------------- 2 files changed, 33 insertions(+), 65 deletions(-) diff --git a/mobile/packages/log_viewer/README.md b/mobile/packages/log_viewer/README.md index 677531c5cf..ae794d6649 100644 --- a/mobile/packages/log_viewer/README.md +++ b/mobile/packages/log_viewer/README.md @@ -10,6 +10,7 @@ A Flutter package that provides an in-app log viewer with advanced filtering cap - 📊 SQLite-based storage with automatic truncation - 📤 Export filtered logs as text - ⚡ Performance optimized with batch inserts and indexing +- 🏷️ Optional prefix support for multi-process logging ## Usage @@ -21,9 +22,12 @@ import 'package:log_viewer/log_viewer.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - // Initialize log viewer + // Initialize log viewer (basic) await LogViewer.initialize(); + // Or with a prefix for multi-process apps + await LogViewer.initialize(prefix: '[MAIN]'); + runApp(MyApp()); } ``` @@ -38,9 +42,9 @@ LogViewer.openViewer(context); LogViewer.getViewerPage() ``` -### 3. The log viewer will automatically capture all logs +### 3. Automatic log capture -The package integrates with the Ente logging system to automatically capture and store logs. +The log viewer automatically registers with `Logger.root.onRecord` to capture all logs from the logging package. No additional setup is required. ## Filtering Options diff --git a/mobile/packages/log_viewer/example_integration.md b/mobile/packages/log_viewer/example_integration.md index 8418413651..ea34540b3c 100644 --- a/mobile/packages/log_viewer/example_integration.md +++ b/mobile/packages/log_viewer/example_integration.md @@ -14,20 +14,13 @@ import 'package:logging/logging.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - // Initialize the log viewer with custom configuration - await LogViewer.initialize( - maxEntries: 5000, // Optional: default is 10000 - ); + // Initialize the log viewer + await LogViewer.initialize(); // Set up logging Logger.root.level = Level.ALL; - Logger.root.onRecord.listen((record) { - // Send logs to log viewer - LogViewer.addLog(record); - - // Also print to console - print('${record.level.name}: ${record.time}: ${record.message}'); - }); + + // Log viewer automatically captures all logs - no manual setup needed! runApp(MyApp()); } @@ -60,11 +53,7 @@ class _MyHomePageState extends State { icon: Icon(Icons.bug_report), onPressed: () { // Navigate to log viewer - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const LogViewerPage(), - ), - ); + LogViewer.openViewer(context); }, ), ], @@ -141,14 +130,10 @@ class SuperLogging { WidgetsFlutterBinding.ensureInitialized(); - // Initialize log viewer in debug mode only + // Initialize log viewer in debug mode with prefix if (kDebugMode) { try { - await LogViewer.initialize(); - - // Register LogViewer with SuperLogging to receive logs with process prefix - LogViewer.registerWithSuperLogging(SuperLogging.registerLogCallback); - + await LogViewer.initialize(prefix: appConfig.prefix); _logger.info("Log viewer initialized successfully"); } catch (e) { _logger.warning("Failed to initialize log viewer: $e"); @@ -171,23 +156,7 @@ class SuperLogging { print(str); } - // Send to log viewer callback if registered - try { - if (_logViewerCallback != null) { - _logViewerCallback!(rec, config.prefix); - } - } catch (_) { - // Silently ignore any errors from the log viewer - } - } - - // Callback that can be set by external packages (like log_viewer) - static void Function(LogRecord, String)? _logViewerCallback; - - /// Register a callback to receive log records - /// This is used by the log_viewer package to capture logs - static void registerLogCallback(void Function(LogRecord, String) callback) { - _logViewerCallback = callback; + // Log viewer automatically captures all logs - no manual integration needed! } } @@ -209,7 +178,7 @@ Future main() async { ### Ente Photos Integration Example -In your Ente Photos app's main.dart, add the log viewer initialization in the `runWithLogs` function: +In your Ente Photos app's main.dart or SuperLogging class, add the log viewer initialization: ```dart Future runWithLogs(Function() function, {String prefix = ""}) async { @@ -224,19 +193,16 @@ Future runWithLogs(Function() function, {String prefix = ""}) async { prefix: prefix, ), ); - - // Initialize log viewer in debug mode only - if (kDebugMode) { - try { - await LogViewer.initialize(); - - // Register LogViewer with SuperLogging to receive logs with process prefix - LogViewer.registerWithSuperLogging(SuperLogging.registerLogCallback); - - _logger.info("Log viewer initialized successfully"); - } catch (e) { - _logger.warning("Failed to initialize log viewer: $e"); - } +} + +// In SuperLogging.main(): +if (kDebugMode) { + try { + // Simply initialize with prefix - no callbacks needed! + await LogViewer.initialize(prefix: appConfig.prefix); + _logger.info("Log viewer initialized successfully"); + } catch (e) { + _logger.warning("Failed to initialize log viewer: $e"); } } ``` @@ -268,11 +234,7 @@ class SettingsPage extends StatelessWidget { if (kDebugMode) GestureDetector( onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const LogViewerPage(), - ), - ); + LogViewer.openViewer(context); }, child: Container( padding: const EdgeInsets.all(8), @@ -307,17 +269,19 @@ Once integrated, users will have access to: ## How It Works -1. The `log_viewer` package listens to all logs via `Logger.root.onRecord` -2. Logs are stored in a local SQLite database (auto-truncated to 2000 entries) +1. The `log_viewer` package automatically registers with `Logger.root.onRecord` on initialization +2. Logs are stored in a local SQLite database (auto-truncated to 10000 entries by default) 3. The UI provides filtering and search capabilities -4. The integration with `super_logging` is automatic - no changes needed +4. When a prefix is provided, it's automatically prepended to all log messages +5. No manual callback registration or integration needed - just initialize and go! ## Troubleshooting If logs aren't appearing: -1. Ensure `LogViewer.initialize()` is called after logging is set up +1. Ensure `LogViewer.initialize()` is called early in app initialization 2. Check that the app has write permissions for the database 3. Verify that `Logger.root.level` is set appropriately (not OFF) +4. If using a prefix, verify it's being passed correctly to `LogViewer.initialize(prefix: yourPrefix)` ## Performance Notes From cfada04396548f13745e4406e8bf7132423b5aab Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 11 Sep 2025 07:49:29 +0530 Subject: [PATCH 3/4] feat(log_viewer): Enhance search, filters, and UI - Add logger name filtering via search box with logger:name syntax - Support wildcard patterns (logger:Auth* matches all loggers starting with Auth) - Make logger cards in statistics page tappable for quick filtering - Set default filters to show WARNING, SEVERE, SHOUT levels - Improve Filter Dialog UI with modern design and better spacing - Reduce search box size with smaller font and padding - Use proper theme colors for buttons (FilledButton) - Remove Processes section from filter dialog for simplicity Co-Authored-By: Claude --- .../packages/log_viewer/lib/log_viewer.dart | 23 +- .../log_viewer/lib/src/core/log_database.dart | 36 ++- .../log_viewer/lib/src/core/log_store.dart | 30 +- .../lib/src/ui/log_filter_dialog.dart | 268 +++++++++++------- .../lib/src/ui/log_viewer_page.dart | 238 ++++++++++------ .../lib/src/ui/logger_statistics_page.dart | 118 ++++---- .../lib/src/ui/timeline_widget.dart | 101 ++++--- 7 files changed, 492 insertions(+), 322 deletions(-) diff --git a/mobile/packages/log_viewer/lib/log_viewer.dart b/mobile/packages/log_viewer/lib/log_viewer.dart index 9a2c3d30d4..86d5184864 100644 --- a/mobile/packages/log_viewer/lib/log_viewer.dart +++ b/mobile/packages/log_viewer/lib/log_viewer.dart @@ -16,12 +16,15 @@ export 'src/ui/log_viewer_page.dart'; /// Main entry point for the log viewer functionality class LogViewer { static bool _initialized = false; + static String _prefix = ''; /// Initialize the log viewer /// This should be called once during app startup - static Future initialize() async { + static Future initialize({String prefix = ''}) async { if (_initialized) return; + _prefix = prefix; + // Initialize the log store await LogStore.instance.initialize(); @@ -38,7 +41,7 @@ class LogViewer { // This will be called dynamically by the main app if SuperLogging is available // For now, fallback to direct logger listening without prefix log.Logger.root.onRecord.listen((record) { - LogStore.addLogRecord(record, ''); + LogStore.addLogRecord(record, _prefix); }); } catch (e) { // SuperLogging not available, fallback to direct logger @@ -48,24 +51,12 @@ class LogViewer { } } - /// Register with SuperLogging callback system (called by main app) - static void registerWithSuperLogging( - void Function(void Function(log.LogRecord, String)) registerCallback,) { - try { - registerCallback((record, processPrefix) { - LogStore.addLogRecord(record, processPrefix); - }); - } catch (e) { - // Fallback if registration fails - _registerWithSuperLogging(); - } - } - /// Get the log viewer page widget static Widget getViewerPage() { if (!_initialized) { throw StateError( - 'LogViewer not initialized. Call LogViewer.initialize() first.',); + 'LogViewer not initialized. Call LogViewer.initialize() first.', + ); } return const LogViewerPage(); } diff --git a/mobile/packages/log_viewer/lib/src/core/log_database.dart b/mobile/packages/log_viewer/lib/src/core/log_database.dart index 29824dd607..4db4af3f91 100644 --- a/mobile/packages/log_viewer/lib/src/core/log_database.dart +++ b/mobile/packages/log_viewer/lib/src/core/log_database.dart @@ -10,7 +10,7 @@ class LogDatabase { static const String _databaseName = 'log_viewer.db'; static const String _tableName = 'logs'; static const int _databaseVersion = 1; - + final int maxEntries; Database? _database; @@ -56,7 +56,6 @@ class LogDatabase { ); } - /// Called when database is opened Future _onOpen(Database db) async { // Enable write-ahead logging for better performance @@ -275,14 +274,17 @@ class LogDatabase { final toDelete = count - maxEntries; // Delete oldest entries - await db.execute(''' + await db.execute( + ''' DELETE FROM $_tableName WHERE id IN ( SELECT id FROM $_tableName ORDER BY timestamp ASC LIMIT ? ) - ''', [toDelete],); + ''', + [toDelete], + ); } } @@ -358,11 +360,13 @@ class LogDatabase { final results = await db.rawQuery(statsQuery, args); return results - .map((row) => LoggerStatistic( - loggerName: row['logger_name'] as String, - logCount: row['count'] as int, - percentage: row['percentage'] as double, - ),) + .map( + (row) => LoggerStatistic( + loggerName: row['logger_name'] as String, + logCount: row['count'] as int, + percentage: row['percentage'] as double, + ), + ) .toList(); } @@ -378,8 +382,12 @@ class LogDatabase { if (result.isNotEmpty && result.first['min_timestamp'] != null) { return TimeRange( - start: DateTime.fromMillisecondsSinceEpoch(result.first['min_timestamp'] as int), - end: DateTime.fromMillisecondsSinceEpoch(result.first['max_timestamp'] as int), + start: DateTime.fromMillisecondsSinceEpoch( + result.first['min_timestamp'] as int, + ), + end: DateTime.fromMillisecondsSinceEpoch( + result.first['max_timestamp'] as int, + ), ); } @@ -396,7 +404,11 @@ class LogDatabase { '''); return result - .map((row) => DateTime.fromMillisecondsSinceEpoch(row['timestamp'] as int)) + .map( + (row) => DateTime.fromMillisecondsSinceEpoch( + row['timestamp'] as int, + ), + ) .toList(); } diff --git a/mobile/packages/log_viewer/lib/src/core/log_store.dart b/mobile/packages/log_viewer/lib/src/core/log_store.dart index b64cba685c..e1470f976b 100644 --- a/mobile/packages/log_viewer/lib/src/core/log_store.dart +++ b/mobile/packages/log_viewer/lib/src/core/log_store.dart @@ -17,7 +17,7 @@ class LogStore { // Buffer for batch inserts - optimized for small entries final List _buffer = []; Timer? _flushTimer; - static const int _bufferSize = 10; + static const int _bufferSize = 10; static const int _maxBufferSize = 200; // Safety limit bool _initialized = false; @@ -83,10 +83,12 @@ class LogStore { _buffer.clear(); // Use non-blocking database insert for better write performance - unawaited(_database.insertLogs(toInsert).catchError((e) { - // ignore: avoid_print - print('Failed to insert logs to database: $e'); - }),); + unawaited( + _database.insertLogs(toInsert).catchError((e) { + // ignore: avoid_print + print('Failed to insert logs to database: $e'); + }), + ); } /// Get logs with filtering @@ -187,14 +189,16 @@ class LogStore { final logs = await getLogs(filter: filter, limit: 10000); final jsonLogs = logs - .map((log) => { - 'timestamp': log.timestamp.toIso8601String(), - 'level': log.level, - 'logger': log.loggerName, - 'message': log.message, - if (log.error != null) 'error': log.error, - if (log.stackTrace != null) 'stackTrace': log.stackTrace, - },) + .map( + (log) => { + 'timestamp': log.timestamp.toIso8601String(), + 'level': log.level, + 'logger': log.loggerName, + 'message': log.message, + if (log.error != null) 'error': log.error, + if (log.stackTrace != null) 'stackTrace': log.stackTrace, + }, + ) .toList(); // Manual JSON formatting for readability diff --git a/mobile/packages/log_viewer/lib/src/ui/log_filter_dialog.dart b/mobile/packages/log_viewer/lib/src/ui/log_filter_dialog.dart index 9907552f11..7c15cde3f8 100644 --- a/mobile/packages/log_viewer/lib/src/ui/log_filter_dialog.dart +++ b/mobile/packages/log_viewer/lib/src/ui/log_filter_dialog.dart @@ -55,7 +55,6 @@ class _LogFilterDialogState extends State { }); } - Widget _buildLevelChip(String level) { final isSelected = _selectedLevels.contains(level); final color = LogEntry( @@ -69,13 +68,20 @@ class _LogFilterDialogState extends State { label: Text( level, style: TextStyle( - color: isSelected ? Colors.white : null, - fontSize: 12, + color: isSelected ? Colors.white : color, + fontSize: 13, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, ), ), selected: isSelected, selectedColor: color, + backgroundColor: color.withValues(alpha: 0.15), checkmarkColor: Colors.white, + side: BorderSide( + color: isSelected ? color : color.withValues(alpha: 0.3), + width: isSelected ? 2 : 1, + ), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), onSelected: (selected) { setState(() { if (selected) { @@ -93,18 +99,22 @@ class _LogFilterDialogState extends State { final theme = Theme.of(context); return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), child: Container( constraints: const BoxConstraints(maxWidth: 400, maxHeight: 600), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Header Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: theme.primaryColor.withValues(alpha: 0.1), - borderRadius: - const BorderRadius.vertical(top: Radius.circular(4)), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + decoration: const BoxDecoration( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -112,12 +122,15 @@ class _LogFilterDialogState extends State { Text( 'Filter Logs', style: theme.textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, + fontWeight: FontWeight.w600, + fontSize: 20, ), ), IconButton( - icon: const Icon(Icons.close), + icon: const Icon(Icons.close, size: 22), onPressed: () => Navigator.pop(context), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), ), ], ), @@ -126,7 +139,8 @@ class _LogFilterDialogState extends State { // Content Flexible( child: SingleChildScrollView( - padding: const EdgeInsets.all(16), + padding: + const EdgeInsets.symmetric(horizontal: 20, vertical: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -134,10 +148,11 @@ class _LogFilterDialogState extends State { Text( 'Log Levels', style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, + fontWeight: FontWeight.w600, + fontSize: 16, ), ), - const SizedBox(height: 8), + const SizedBox(height: 12), Wrap( spacing: 8, runSpacing: 8, @@ -146,110 +161,103 @@ class _LogFilterDialogState extends State { .map(_buildLevelChip) .toList(), ), - const SizedBox(height: 24), + const SizedBox(height: 20), // Loggers if (widget.availableLoggers.isNotEmpty) ...[ Text( 'Loggers', style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, + fontWeight: FontWeight.w600, + fontSize: 16, ), ), - const SizedBox(height: 8), + const SizedBox(height: 12), Container( - constraints: const BoxConstraints(maxHeight: 150), + constraints: const BoxConstraints(maxHeight: 180), decoration: BoxDecoration( - border: Border.all(color: theme.dividerColor), - borderRadius: BorderRadius.circular(4), + border: Border.all( + color: theme.dividerColor.withValues(alpha: 0.5), + width: 1, + ), + borderRadius: BorderRadius.circular(8), + color: theme.cardColor, ), - child: ListView.builder( - shrinkWrap: true, - itemCount: widget.availableLoggers.length, - itemBuilder: (context, index) { - final logger = widget.availableLoggers[index]; - return CheckboxListTile( - title: Text( - logger, - style: const TextStyle(fontSize: 14), - ), - value: _selectedLoggers.contains(logger), - dense: true, - onChanged: (selected) { - setState(() { - if (selected == true) { - _selectedLoggers.add(logger); - } else { - _selectedLoggers.remove(logger); - } - }); - }, - ); - }, - ), - ), - const SizedBox(height: 24), - ], - - // Processes - if (widget.availableProcesses.isNotEmpty) ...[ - Text( - 'Processes', - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 8), - Container( - constraints: const BoxConstraints(maxHeight: 120), - decoration: BoxDecoration( - border: Border.all(color: theme.dividerColor), - borderRadius: BorderRadius.circular(4), - ), - child: ListView.builder( - shrinkWrap: true, - itemCount: widget.availableProcesses.length, - itemBuilder: (context, index) { - final process = widget.availableProcesses[index]; - final displayName = LogEntry( - message: '', - level: 'INFO', - timestamp: DateTime.now(), - loggerName: '', - processPrefix: process, - ).processDisplayName; - return CheckboxListTile( - title: Text( - displayName, - style: const TextStyle(fontSize: 14), - ), - subtitle: process.isNotEmpty - ? Text( - 'Prefix: $process', - style: TextStyle( - fontSize: 12, - color: theme.textTheme.bodySmall?.color, + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: ListView.separated( + shrinkWrap: true, + itemCount: widget.availableLoggers.length, + separatorBuilder: (context, index) => Divider( + height: 1, + thickness: 0.5, + color: theme.dividerColor.withValues(alpha: 0.3), + ), + itemBuilder: (context, index) { + final logger = widget.availableLoggers[index]; + final isSelected = + _selectedLoggers.contains(logger); + return InkWell( + onTap: () { + setState(() { + if (isSelected) { + _selectedLoggers.remove(logger); + } else { + _selectedLoggers.add(logger); + } + }); + }, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + child: Row( + children: [ + Expanded( + child: Text( + logger, + style: TextStyle( + fontSize: 14, + color: isSelected + ? theme.primaryColor + : theme + .textTheme.bodyLarge?.color, + fontWeight: isSelected + ? FontWeight.w500 + : FontWeight.normal, + ), + overflow: TextOverflow.ellipsis, + ), ), - ) - : null, - value: _selectedProcesses.contains(process), - dense: true, - onChanged: (selected) { - setState(() { - if (selected == true) { - _selectedProcesses.add(process); - } else { - _selectedProcesses.remove(process); - } - }); - }, - ); - }, + SizedBox( + width: 24, + height: 24, + child: Checkbox( + value: isSelected, + onChanged: (selected) { + setState(() { + if (selected == true) { + _selectedLoggers.add(logger); + } else { + _selectedLoggers.remove(logger); + } + }); + }, + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + ), + ), + ], + ), + ), + ); + }, + ), ), ), - const SizedBox(height: 24), + const SizedBox(height: 20), ], - ], ), ), @@ -259,27 +267,69 @@ class _LogFilterDialogState extends State { Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: theme.cardColor, + border: Border( + top: BorderSide( + color: theme.dividerColor.withValues(alpha: 0.2), + width: 1, + ), + ), borderRadius: - const BorderRadius.vertical(bottom: Radius.circular(4)), + const BorderRadius.vertical(bottom: Radius.circular(16)), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ TextButton( onPressed: _clearFilters, - child: const Text('Clear All'), + style: TextButton.styleFrom( + foregroundColor: theme.colorScheme.error, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 10, + ), + ), + child: const Text( + 'Clear All', + style: TextStyle(fontSize: 14), + ), ), Row( children: [ TextButton( onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 10, + ), + ), + child: Text( + 'Cancel', + style: TextStyle( + fontSize: 14, + color: theme.textTheme.bodyLarge?.color, + ), + ), ), - const SizedBox(width: 8), - ElevatedButton( + const SizedBox(width: 12), + FilledButton( onPressed: _applyFilters, - child: const Text('Apply'), + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 10, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: const Text( + 'Apply', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), ), ], ), diff --git a/mobile/packages/log_viewer/lib/src/ui/log_viewer_page.dart b/mobile/packages/log_viewer/lib/src/ui/log_viewer_page.dart index ea90385ac1..db0f0ef768 100644 --- a/mobile/packages/log_viewer/lib/src/ui/log_viewer_page.dart +++ b/mobile/packages/log_viewer/lib/src/ui/log_viewer_page.dart @@ -25,7 +25,9 @@ class _LogViewerPageState extends State { List _logs = []; List _availableLoggers = []; List _availableProcesses = []; - LogFilter _filter = const LogFilter(); + LogFilter _filter = const LogFilter( + selectedLevels: {'WARNING', 'SEVERE', 'SHOUT'}, + ); bool _isLoading = true; bool _isLoadingMore = false; bool _hasMoreLogs = true; @@ -35,7 +37,7 @@ class _LogViewerPageState extends State { // Time filtering state bool _timeFilterEnabled = false; - + // Timeline state DateTime? _overallStartTime; DateTime? _overallEndTime; @@ -178,10 +180,55 @@ class _LogViewerPageState extends State { } void _onSearchChanged(String query) { + // Parse query for special syntax like "logger:SomeName" + String? searchText = query; + Set? loggerFilters; + + if (query.isNotEmpty) { + // Regular expression to match logger:name patterns + final loggerPattern = RegExp(r'logger:(\S+)'); + final matches = loggerPattern.allMatches(query); + + if (matches.isNotEmpty) { + loggerFilters = {}; + for (final match in matches) { + final loggerName = match.group(1); + if (loggerName != null) { + // Support wildcards (e.g., Auth* matches AuthService, Authentication, etc.) + if (loggerName.endsWith('*')) { + final prefix = loggerName.substring(0, loggerName.length - 1); + // Find all loggers that start with this prefix + for (final logger in _availableLoggers) { + if (logger.startsWith(prefix)) { + loggerFilters.add(logger); + } + } + } else { + loggerFilters.add(loggerName); + } + } + } + + // Remove logger:name patterns from search text + searchText = query.replaceAll(loggerPattern, '').trim(); + if (searchText.isEmpty) { + searchText = null; + } + } + } else { + // Clear logger filters when search is empty + loggerFilters = {}; + } + setState(() { + // Only update logger filters if logger: syntax was found or query is empty + final newLoggerFilters = loggerFilters ?? + (query.isEmpty ? {} : _filter.selectedLoggers); + _filter = _filter.copyWith( - searchQuery: query.isEmpty ? null : query, + searchQuery: searchText, clearSearchQuery: query.isEmpty, + selectedLoggers: newLoggerFilters, ); }); _loadLogs(); @@ -189,7 +236,9 @@ class _LogViewerPageState extends State { void _updateTimeFilter() { setState(() { - if (_timeFilterEnabled && _timelineStartTime != null && _timelineEndTime != null) { + if (_timeFilterEnabled && + _timelineStartTime != null && + _timelineEndTime != null) { _filter = _filter.copyWith( startTime: _timelineStartTime, endTime: _timelineEndTime, @@ -264,7 +313,7 @@ class _LogViewerPageState extends State { Future _exportLogs() async { try { final logText = await _logStore.exportLogs(filter: _filter); - + await Share.share(logText, subject: 'App Logs'); } catch (e) { if (mounted) { @@ -284,13 +333,19 @@ class _LogViewerPageState extends State { _loadLogs(); } - void _showAnalytics() { - Navigator.push( + void _showAnalytics() async { + final result = await Navigator.push( context, MaterialPageRoute( builder: (context) => LoggerStatisticsPage(filter: _filter), ), ); + + // If a logger filter was returned, apply it to the search box + if (result != null && mounted) { + _searchController.text = result; + _onSearchChanged(result); + } } void _showLogDetail(LogEntry log) { @@ -302,7 +357,6 @@ class _LogViewerPageState extends State { ); } - @override void dispose() { _searchController.dispose(); @@ -349,9 +403,11 @@ class _LogViewerPageState extends State { tooltip: 'Filters', ), IconButton( - icon: Icon(_filter.sortNewestFirst - ? Icons.arrow_downward - : Icons.arrow_upward,), + icon: Icon( + _filter.sortNewestFirst + ? Icons.arrow_downward + : Icons.arrow_upward, + ), onPressed: _toggleSort, tooltip: _filter.sortNewestFirst ? 'Sort oldest first' @@ -406,15 +462,17 @@ class _LogViewerPageState extends State { // Search bar Container( color: theme.appBarTheme.backgroundColor, - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0), child: TextField( controller: _searchController, + style: const TextStyle(fontSize: 14), decoration: InputDecoration( hintText: 'Search logs...', - prefixIcon: const Icon(Icons.search), + hintStyle: const TextStyle(fontSize: 14), + prefixIcon: const Icon(Icons.search, size: 20), suffixIcon: _searchController.text.isNotEmpty ? IconButton( - icon: const Icon(Icons.clear), + icon: const Icon(Icons.clear, size: 20), onPressed: () { _searchController.clear(); _onSearchChanged(''); @@ -424,7 +482,9 @@ class _LogViewerPageState extends State { border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), ), - contentPadding: const EdgeInsets.symmetric(horizontal: 16), + contentPadding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + isDense: true, ), onChanged: _onSearchChanged, ), @@ -470,7 +530,9 @@ class _LogViewerPageState extends State { }); _updateTimeFilter(); }, - tooltip: _timeFilterEnabled ? 'Disable Timeline Filter' : 'Enable Timeline Filter', + tooltip: _timeFilterEnabled + ? 'Disable Timeline Filter' + : 'Enable Timeline Filter', ), ], ), @@ -496,76 +558,89 @@ class _LogViewerPageState extends State { scrollDirection: Axis.horizontal, children: [ if (_filter.selectedLoggers.isNotEmpty) - ..._filter.selectedLoggers.map((logger) => Padding( - padding: const EdgeInsets.only(right: 4), - child: Chip( - label: Text(logger, - style: const TextStyle(fontSize: 12),), - deleteIcon: const Icon(Icons.close, size: 16), - onDeleted: () { - setState(() { - final newLoggers = - Set.from(_filter.selectedLoggers); - newLoggers.remove(logger); - _filter = _filter.copyWith( - selectedLoggers: newLoggers,); - }); - _loadLogs(); - }, + ..._filter.selectedLoggers.map( + (logger) => Padding( + padding: const EdgeInsets.only(right: 4), + child: Chip( + label: Text( + logger, + style: const TextStyle(fontSize: 12), ), - ),), + deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () { + setState(() { + final newLoggers = + Set.from(_filter.selectedLoggers); + newLoggers.remove(logger); + _filter = _filter.copyWith( + selectedLoggers: newLoggers, + ); + }); + _loadLogs(); + }, + ), + ), + ), if (_filter.selectedLevels.isNotEmpty) - ..._filter.selectedLevels.map((level) => Padding( - padding: const EdgeInsets.only(right: 4), - child: Chip( - label: Text(level, - style: const TextStyle(fontSize: 12),), - backgroundColor: LogEntry( + ..._filter.selectedLevels.map( + (level) => Padding( + padding: const EdgeInsets.only(right: 4), + child: Chip( + label: Text( + level, + style: const TextStyle(fontSize: 12), + ), + backgroundColor: LogEntry( + message: '', + level: level, + timestamp: DateTime.now(), + loggerName: '', + ).levelColor.withValues(alpha: 0.2), + deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () { + setState(() { + final newLevels = + Set.from(_filter.selectedLevels); + newLevels.remove(level); + _filter = + _filter.copyWith(selectedLevels: newLevels); + }); + _loadLogs(); + }, + ), + ), + ), + if (_filter.selectedProcesses.isNotEmpty) + ..._filter.selectedProcesses.map( + (process) => Padding( + padding: const EdgeInsets.only(right: 4), + child: Chip( + label: Text( + LogEntry( message: '', - level: level, + level: 'INFO', timestamp: DateTime.now(), loggerName: '', - ).levelColor.withValues(alpha: 0.2), - deleteIcon: const Icon(Icons.close, size: 16), - onDeleted: () { - setState(() { - final newLevels = - Set.from(_filter.selectedLevels); - newLevels.remove(level); - _filter = - _filter.copyWith(selectedLevels: newLevels); - }); - _loadLogs(); - }, + processPrefix: process, + ).processDisplayName, + style: const TextStyle(fontSize: 12), ), - ),), - if (_filter.selectedProcesses.isNotEmpty) - ..._filter.selectedProcesses.map((process) => Padding( - padding: const EdgeInsets.only(right: 4), - child: Chip( - label: Text( - LogEntry( - message: '', - level: 'INFO', - timestamp: DateTime.now(), - loggerName: '', - processPrefix: process, - ).processDisplayName, - style: const TextStyle(fontSize: 12),), - backgroundColor: Colors.purple.withValues(alpha: 0.2), - deleteIcon: const Icon(Icons.close, size: 16), - onDeleted: () { - setState(() { - final newProcesses = - Set.from(_filter.selectedProcesses); - newProcesses.remove(process); - _filter = - _filter.copyWith(selectedProcesses: newProcesses); - }); - _loadLogs(); - }, - ), - ),), + backgroundColor: Colors.purple.withValues(alpha: 0.2), + deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () { + setState(() { + final newProcesses = + Set.from(_filter.selectedProcesses); + newProcesses.remove(process); + _filter = _filter.copyWith( + selectedProcesses: newProcesses, + ); + }); + _loadLogs(); + }, + ), + ), + ), ], ), ), @@ -611,7 +686,8 @@ class _LogViewerPageState extends State { return const Padding( padding: EdgeInsets.all(16), child: Center( - child: CircularProgressIndicator(),), + child: CircularProgressIndicator(), + ), ); } else { // Trigger loading more when reaching the end diff --git a/mobile/packages/log_viewer/lib/src/ui/logger_statistics_page.dart b/mobile/packages/log_viewer/lib/src/ui/logger_statistics_page.dart index 383177513e..79b11b3686 100644 --- a/mobile/packages/log_viewer/lib/src/ui/logger_statistics_page.dart +++ b/mobile/packages/log_viewer/lib/src/ui/logger_statistics_page.dart @@ -167,64 +167,78 @@ class _LoggerStatisticsPageState extends State { return Card( margin: const EdgeInsets.only(bottom: 8), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - stat.loggerName, + child: InkWell( + onTap: () { + // Navigate back to log viewer with logger filter in search + Navigator.pop( + context, + 'logger:${stat.loggerName}', + ); + }, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + stat.loggerName, + style: theme + .textTheme.titleMedium + ?.copyWith( + fontWeight: FontWeight.w500, + ), + overflow: + TextOverflow.ellipsis, + ), + ), + Text( + stat.formattedPercentage, style: theme .textTheme.titleMedium ?.copyWith( + color: color, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: LinearProgressIndicator( + value: stat.percentage / 100, + backgroundColor: color + .withValues(alpha: 0.2), + valueColor: + AlwaysStoppedAnimation( + color, + ), + minHeight: 6, + ), + ), + const SizedBox(width: 12), + Text( + '${stat.count} logs', + style: theme + .textTheme.bodyMedium + ?.copyWith( + color: theme.colorScheme + .onSurfaceVariant, fontWeight: FontWeight.w500, ), - overflow: TextOverflow.ellipsis, ), - ), - Text( - stat.formattedPercentage, - style: theme.textTheme.titleMedium - ?.copyWith( - color: color, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const SizedBox(height: 8), - Row( - children: [ - Expanded( - child: LinearProgressIndicator( - value: stat.percentage / 100, - backgroundColor: - color.withValues(alpha: 0.2), - valueColor: - AlwaysStoppedAnimation( - color,), - minHeight: 6, - ), - ), - const SizedBox(width: 12), - Text( - '${stat.count} logs', - style: theme.textTheme.bodyMedium - ?.copyWith( - color: theme.colorScheme - .onSurfaceVariant, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ], + ], + ), + ], + ), ), ), ); diff --git a/mobile/packages/log_viewer/lib/src/ui/timeline_widget.dart b/mobile/packages/log_viewer/lib/src/ui/timeline_widget.dart index bd54f410d0..4824ab980e 100644 --- a/mobile/packages/log_viewer/lib/src/ui/timeline_widget.dart +++ b/mobile/packages/log_viewer/lib/src/ui/timeline_widget.dart @@ -27,7 +27,7 @@ class _TimelineWidgetState extends State { late double _rightPosition; bool _isDraggingLeft = false; bool _isDraggingRight = false; - + @override void initState() { super.initState(); @@ -46,10 +46,13 @@ class _TimelineWidgetState extends State { } void _updatePositions() { - final totalDuration = widget.endTime.difference(widget.startTime).inMilliseconds; - final startOffset = widget.currentStart.difference(widget.startTime).inMilliseconds; - final endOffset = widget.currentEnd.difference(widget.startTime).inMilliseconds; - + final totalDuration = + widget.endTime.difference(widget.startTime).inMilliseconds; + final startOffset = + widget.currentStart.difference(widget.startTime).inMilliseconds; + final endOffset = + widget.currentEnd.difference(widget.startTime).inMilliseconds; + _leftPosition = startOffset / totalDuration; _rightPosition = endOffset / totalDuration; } @@ -57,11 +60,12 @@ class _TimelineWidgetState extends State { void _onPanUpdate(DragUpdateDetails details, bool isLeft) { final RenderBox renderBox = context.findRenderObject() as RenderBox; final double width = renderBox.size.width - 40; // Account for handle width - + // Convert global position to local position within the timeline track final Offset globalPosition = details.globalPosition; final Offset localPosition = renderBox.globalToLocal(globalPosition); - final double localX = localPosition.dx - 20; // Account for left handle width + final double localX = + localPosition.dx - 20; // Account for left handle width final double position = (localX / width).clamp(0.0, 1.0); setState(() { @@ -72,17 +76,20 @@ class _TimelineWidgetState extends State { } }); - final totalDuration = widget.endTime.difference(widget.startTime).inMilliseconds; - final newStart = widget.startTime.add(Duration(milliseconds: (_leftPosition * totalDuration).round())); - final newEnd = widget.startTime.add(Duration(milliseconds: (_rightPosition * totalDuration).round())); - + final totalDuration = + widget.endTime.difference(widget.startTime).inMilliseconds; + final newStart = widget.startTime + .add(Duration(milliseconds: (_leftPosition * totalDuration).round())); + final newEnd = widget.startTime + .add(Duration(milliseconds: (_rightPosition * totalDuration).round())); + widget.onTimeRangeChanged(newStart, newEnd); } @override Widget build(BuildContext context) { final theme = Theme.of(context); - + return Container( height: 120, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), @@ -112,47 +119,57 @@ class _TimelineWidgetState extends State { color: theme.colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(4), border: Border.all( - color: theme.colorScheme.outline.withValues(alpha: 0.3), + color: theme.colorScheme.outline + .withValues(alpha: 0.3), width: 1, ), ), - child: _buildLogDensityIndicator(constraints.maxWidth - 40), + child: _buildLogDensityIndicator( + constraints.maxWidth - 40, + ), ), ), - + // Selected range Positioned( left: 20 + (_leftPosition * (constraints.maxWidth - 40)), - right: constraints.maxWidth - 20 - (_rightPosition * (constraints.maxWidth - 40)), + right: constraints.maxWidth - + 20 - + (_rightPosition * (constraints.maxWidth - 40)), top: 20, bottom: 20, child: Container( decoration: BoxDecoration( - color: theme.colorScheme.primary.withValues(alpha: 0.4), + color: + theme.colorScheme.primary.withValues(alpha: 0.4), borderRadius: BorderRadius.circular(4), border: Border.all( - color: theme.colorScheme.primary.withValues(alpha: 0.7), + color: theme.colorScheme.primary + .withValues(alpha: 0.7), width: 1, ), ), ), ), - + // Left handle Positioned( left: (_leftPosition * (constraints.maxWidth - 40)), top: 12, child: GestureDetector( onPanUpdate: (details) => _onPanUpdate(details, true), - onPanStart: (_) => setState(() => _isDraggingLeft = true), - onPanEnd: (_) => setState(() => _isDraggingLeft = false), + onPanStart: (_) => + setState(() => _isDraggingLeft = true), + onPanEnd: (_) => + setState(() => _isDraggingLeft = false), child: Container( width: 20, height: 32, decoration: BoxDecoration( - color: _isDraggingLeft - ? theme.colorScheme.primary - : theme.colorScheme.primary.withValues(alpha: 0.8), + color: _isDraggingLeft + ? theme.colorScheme.primary + : theme.colorScheme.primary + .withValues(alpha: 0.8), borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow( @@ -170,22 +187,25 @@ class _TimelineWidgetState extends State { ), ), ), - + // Right handle Positioned( left: (_rightPosition * (constraints.maxWidth - 40)), top: 12, child: GestureDetector( onPanUpdate: (details) => _onPanUpdate(details, false), - onPanStart: (_) => setState(() => _isDraggingRight = true), - onPanEnd: (_) => setState(() => _isDraggingRight = false), + onPanStart: (_) => + setState(() => _isDraggingRight = true), + onPanEnd: (_) => + setState(() => _isDraggingRight = false), child: Container( width: 20, height: 32, decoration: BoxDecoration( - color: _isDraggingRight - ? theme.colorScheme.primary - : theme.colorScheme.primary.withValues(alpha: 0.8), + color: _isDraggingRight + ? theme.colorScheme.primary + : theme.colorScheme.primary + .withValues(alpha: 0.8), borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow( @@ -208,7 +228,7 @@ class _TimelineWidgetState extends State { }, ), ), - + // Time labels Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -230,25 +250,27 @@ class _TimelineWidgetState extends State { Widget _buildLogDensityIndicator(double width) { if (widget.logTimestamps.isEmpty) return const SizedBox.shrink(); - + final theme = Theme.of(context); - final totalDuration = widget.endTime.difference(widget.startTime).inMilliseconds; + final totalDuration = + widget.endTime.difference(widget.startTime).inMilliseconds; const bucketCount = 50; final bucketDuration = totalDuration / bucketCount; final buckets = List.filled(bucketCount, 0); - + // Count logs in each bucket for (final timestamp in widget.logTimestamps) { final offset = timestamp.difference(widget.startTime).inMilliseconds; if (offset >= 0 && offset <= totalDuration) { - final bucketIndex = (offset / bucketDuration).floor().clamp(0, bucketCount - 1); + final bucketIndex = + (offset / bucketDuration).floor().clamp(0, bucketCount - 1); buckets[bucketIndex]++; } } - + final maxCount = buckets.reduce((a, b) => a > b ? a : b); if (maxCount == 0) return const SizedBox.shrink(); - + return Row( children: buckets.map((count) { final intensity = count / maxCount; @@ -257,7 +279,8 @@ class _TimelineWidgetState extends State { height: double.infinity, margin: const EdgeInsets.symmetric(horizontal: 0.5), decoration: BoxDecoration( - color: theme.colorScheme.primary.withValues(alpha: intensity * 0.6), + color: + theme.colorScheme.primary.withValues(alpha: intensity * 0.6), borderRadius: BorderRadius.circular(1), ), ), @@ -269,4 +292,4 @@ class _TimelineWidgetState extends State { String _formatTime(DateTime time) { return '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}:${time.second.toString().padLeft(2, '0')}'; } -} \ No newline at end of file +} From 936c6f1b6117a8e8bf491f7adf5fc38518778220 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 11 Sep 2025 07:53:13 +0530 Subject: [PATCH 4/4] Clean up --- .../core/error-reporting/super_logging.dart | 42 ++++--------------- mobile/apps/photos/pubspec.lock | 16 +++++++ .../log_viewer/example_logger_filter.md | 40 ++++++++++++++++++ 3 files changed, 65 insertions(+), 33 deletions(-) create mode 100644 mobile/packages/log_viewer/example_logger_filter.md diff --git a/mobile/apps/photos/lib/core/error-reporting/super_logging.dart b/mobile/apps/photos/lib/core/error-reporting/super_logging.dart index 13cc1cace5..90809cebf3 100644 --- a/mobile/apps/photos/lib/core/error-reporting/super_logging.dart +++ b/mobile/apps/photos/lib/core/error-reporting/super_logging.dart @@ -189,6 +189,15 @@ class SuperLogging { Logger.root.level = kDebugMode ? Level.ALL : Level.INFO; Logger.root.onRecord.listen(onLogRecord); + if (_preferences.getBool("enable_db_logging") ?? kDebugMode) { + try { + await LogViewer.initialize(prefix: appConfig.prefix); + $.info("Log viewer initialized successfully"); + } catch (e) { + $.warning("Failed to initialize log viewer: $e"); + } + } + if (isFDroidClient) { assert( sentryIsEnabled == false, @@ -219,19 +228,6 @@ class SuperLogging { }), ); - // Initialize log viewer integration in debug mode - // Initialize log viewer in debug mode only - if (_preferences.getBool("enable_db_logging") ?? kDebugMode) { - try { - await LogViewer.initialize(); - // Register LogViewer with SuperLogging to receive logs with process prefix - LogViewer.registerWithSuperLogging(SuperLogging.registerLogCallback); - $.info("Log viewer initialized successfully"); - } catch (e) { - $.warning("Failed to initialize log viewer: $e"); - } - } - if (appConfig.body == null) return; if (enable && sentryIsEnabled) { @@ -311,17 +307,6 @@ class SuperLogging { printLog(str); saveLogString(str, rec.error); - // Hook for external log viewer (if available) - // This allows the log_viewer package to capture logs without creating a dependency - if(_logViewerCallback != null) { - try { - if (_logViewerCallback != null) { - _logViewerCallback!(rec, config.prefix); - } - } catch (_) { - // Silently ignore any errors from the log viewer - } - } } static void saveLogString(String str, Object? error) { @@ -339,15 +324,6 @@ class SuperLogging { } } - // Callback that can be set by external packages (like log_viewer) - static void Function(LogRecord, String)? _logViewerCallback; - - /// Register a callback to receive log records - /// This is used by the log_viewer package to capture logs - static void registerLogCallback(void Function(LogRecord, String) callback) { - _logViewerCallback = callback; - } - static final Queue fileQueueEntries = Queue(); static bool isFlushing = false; diff --git a/mobile/apps/photos/pubspec.lock b/mobile/apps/photos/pubspec.lock index bc4d4cf1d1..25c1d1af0b 100644 --- a/mobile/apps/photos/pubspec.lock +++ b/mobile/apps/photos/pubspec.lock @@ -2147,6 +2147,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + qr: + dependency: transitive + description: + name: qr + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + qr_flutter: + dependency: "direct main" + description: + name: qr_flutter + sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" + url: "https://pub.dev" + source: hosted + version: "4.1.0" quiver: dependency: transitive description: diff --git a/mobile/packages/log_viewer/example_logger_filter.md b/mobile/packages/log_viewer/example_logger_filter.md new file mode 100644 index 0000000000..13230eedd2 --- /dev/null +++ b/mobile/packages/log_viewer/example_logger_filter.md @@ -0,0 +1,40 @@ +# Logger Filter Feature Usage + +The log viewer now supports filtering logs by logger names directly through the search box, without any UI changes. + +## Search Syntax + +### Basic Logger Filtering +- `logger:AuthService` - Shows only logs from the AuthService logger +- `logger:UserService` - Shows only logs from the UserService logger + +### Wildcard Support +- `logger:Auth*` - Shows logs from all loggers starting with "Auth" (e.g., AuthService, Authentication, AuthManager) +- `logger:*Service` - Not supported yet (only prefix wildcards are supported) + +### Combined Search +- `logger:AuthService error` - Shows logs from AuthService that contain "error" in the message +- `login logger:UserService` - Shows logs from UserService that contain "login" +- `logger:Auth* failed` - Shows logs from loggers starting with "Auth" that contain "failed" + +## Quick Access from Analytics + +1. Navigate to Logger Analytics (via the menu in the log viewer) +2. Tap on any logger name card +3. The log viewer will automatically populate the search box with `logger:LoggerName` and filter the logs + +## Implementation Details + +- The search box hint text now shows "Search logs or use logger:name..." +- When logger: syntax is detected, it's parsed and converted to logger filters +- The remaining text (after removing logger: patterns) is used for message search +- Multiple logger patterns can be used: `logger:Auth* logger:User*` +- Clearing the search box removes all filters + +## Benefits + +1. **No UI Changes**: The existing search box is enhanced with new functionality +2. **Intuitive Syntax**: Similar to GitHub and Google search operators +3. **Quick Navigation**: Tap logger names in analytics to instantly filter +4. **Powerful Combinations**: Mix logger filters with text search +5. **Wildcard Support**: Filter multiple related loggers with prefix patterns \ No newline at end of file