From 338547203d1046ea1615c79d7e25c7c6ad2891e0 Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Mon, 27 Nov 2023 11:34:21 -0600 Subject: [PATCH] Remove extraneous print from sign in class --- lib/main.dart | 17 ++- lib/routes.dart | 9 ++ lib/screens/logviewer_screen.dart | 165 ++++++++++++++++++++++++++++ lib/screens/settings_screen.dart | 28 +++++ lib/services/log_service.dart | 26 +++++ lib/services/setting_service.dart | 30 +++++ lib/utils/logrecord_extensions.dart | 11 ++ 7 files changed, 281 insertions(+), 5 deletions(-) create mode 100644 lib/screens/logviewer_screen.dart create mode 100644 lib/services/log_service.dart create mode 100644 lib/utils/logrecord_extensions.dart diff --git a/lib/main.dart b/lib/main.dart index 7643ec5..7e1a85d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -19,6 +19,7 @@ import 'services/follow_requests_manager.dart'; import 'services/gallery_service.dart'; import 'services/hashtag_service.dart'; import 'services/interactions_manager.dart'; +import 'services/log_service.dart'; import 'services/notifications_manager.dart'; import 'services/setting_service.dart'; import 'services/status_service.dart'; @@ -32,14 +33,14 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); MediaKit.ensureInitialized(); + final logService = LogService(); + getIt.registerSingleton(logService); + // await dotenv.load(fileName: '.env'); const enablePreview = false; - Logger.root.level = Level.FINER; + Logger.root.level = Level.OFF; Logger.root.onRecord.listen((event) { - final logName = event.loggerName.isEmpty ? 'ROOT' : event.loggerName; - final msg = - '${event.level.name} - $logName @ ${event.time}: ${event.message}'; - print(msg); + logService.add(event); }); await fixLetsEncryptCertOnOldAndroid(); @@ -60,9 +61,15 @@ class App extends StatelessWidget { final settingsService = getIt(); return AnimatedBuilder( builder: (context, child) { + Logger.root.level = settingsService.logLevel; + print('Log level: ${settingsService.logLevel}'); return Portal( child: MultiProvider( providers: [ + ChangeNotifierProvider( + create: (_) => getIt(), + lazy: true, + ), ChangeNotifierProvider( create: (_) => getIt(), lazy: true, diff --git a/lib/routes.dart b/lib/routes.dart index 2bbeb8a..afb0ac7 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -17,6 +17,7 @@ import 'screens/gallery_screen.dart'; import 'screens/home.dart'; import 'screens/image_editor_screen.dart'; import 'screens/interactions_viewer_screen.dart'; +import 'screens/logviewer_screen.dart'; import 'screens/message_thread_screen.dart'; import 'screens/message_threads_browser_screen.dart'; import 'screens/messages_new_thread.dart'; @@ -51,6 +52,7 @@ class ScreenPaths { static String likes = '/likes'; static String reshares = '/reshares'; static String search = '/search'; + static String logViewer = '/logViewer'; } bool needAuthChangeInitialized = true; @@ -314,4 +316,11 @@ final appRouter = GoRouter( child: const SearchScreen(), ), ), + GoRoute( + path: ScreenPaths.logViewer, + name: ScreenPaths.logViewer, + pageBuilder: (context, state) => const NoTransitionPage( + child: LogViewerScreen(), + ), + ), ]); diff --git a/lib/screens/logviewer_screen.dart b/lib/screens/logviewer_screen.dart new file mode 100644 index 0000000..56af3b4 --- /dev/null +++ b/lib/screens/logviewer_screen.dart @@ -0,0 +1,165 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; +import 'package:provider/provider.dart'; + +import '../controls/padding.dart'; +import '../controls/responsive_max_width.dart'; +import '../controls/standard_appbar.dart'; +import '../services/log_service.dart'; +import '../services/setting_service.dart'; +import '../utils/clipboard_utils.dart'; +import '../utils/logrecord_extensions.dart'; + +class LogViewerScreen extends StatefulWidget { + const LogViewerScreen({super.key}); + + @override + State createState() => _LogViewerScreenState(); +} + +class _LogViewerScreenState extends State { + var filterText = ''; + var filterByText = false; + var filterByModule = false; + var filterModuleName = ''; + + @override + Widget build(BuildContext context) { + final settings = context.watch(); + final logService = context.watch(); + final events = logService.events; + + return Scaffold( + appBar: StandardAppBar.build(context, 'Log Viewer'), + body: Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: ResponsiveMaxWidth( + child: Column( + children: [ + buildLogPanel(settings), + buildModuleFilter(settings, events), + buildTextSearchPanel(), + const VerticalPadding(), + buildLogList(settings.logLevel, events), + ], + ), + ), + ), + )); + } + + Widget buildLogPanel(SettingsService settings) { + return ListTile( + title: const Text('Log Level'), + trailing: DropdownButton( + value: settings.logLevel, + items: Level.LEVELS + .map((c) => DropdownMenuItem(value: c, child: Text(c.name))) + .toList(), + onChanged: (value) { + settings.logLevel = value ?? Level.OFF; + }), + ); + } + + Widget buildModuleFilter(SettingsService settings, List events) { + final modules = events.map((e) => e.loggerName).toSet().toList(); + modules.sort(); + modules.add(''); + return ListTile( + leading: Checkbox( + value: filterByModule, + onChanged: (bool? value) { + setState(() { + filterByModule = value ?? false; + }); + }, + ), + title: const Text('Filter by module:'), + trailing: !filterByModule + ? null + : DropdownButton( + value: filterModuleName, + items: modules + .map((c) => DropdownMenuItem(value: c, child: Text(c))) + .toList(), + onChanged: (value) { + setState(() { + filterModuleName = value ?? ''; + }); + }), + ); + } + + Widget buildTextSearchPanel() { + return ListTile( + leading: Checkbox( + value: filterByText, + onChanged: (value) { + setState(() { + filterByText = value ?? false; + }); + }, + ), + title: TextField( + onChanged: (value) { + setState(() { + filterText = value.toLowerCase(); + }); + }, + decoration: InputDecoration( + labelText: 'Filter by text', + alignLabelWithHint: true, + border: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).highlightColor, + ), + borderRadius: BorderRadius.circular(5.0), + ), + ), + ), + ); + } + + Widget buildLogList(Level logLevel, List allEvents) { + final events = allEvents.where((e) { + final levelFilterPasses = e.level >= logLevel; + final passesTextFilter = filterByText + ? e.message.toLowerCase().contains(filterText.toLowerCase()) + : true; + final passesModuleFilter = filterByModule + ? filterModuleName.isEmpty || e.loggerName == filterModuleName + : true; + + return levelFilterPasses && passesTextFilter && passesModuleFilter; + }).toList(); + return Expanded( + child: ListView.separated( + itemBuilder: (context, index) { + final event = events[index]; + return ListTile( + onLongPress: () { + copyToClipboard( + context: context, + text: jsonEncode(event.toJson()), + ); + }, + titleAlignment: ListTileTitleAlignment.titleHeight, + leading: Text(event.level.toString()), + title: + Text('${event.loggerName} at ${event.time.toIso8601String()}'), + subtitle: Text( + event.message, + softWrap: true, + ), + ); + }, + separatorBuilder: (_, __) => const Divider(), + itemCount: events.length, + ), + ); + } +} diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index e3c93d1..b66baa3 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -1,12 +1,16 @@ import 'package:color_blindness/color_blindness.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:logging/logging.dart'; import 'package:provider/provider.dart'; +import '../controls/padding.dart'; import '../controls/responsive_max_width.dart'; import '../controls/standard_appbar.dart'; import '../di_initialization.dart'; import '../globals.dart'; +import '../routes.dart'; import '../services/setting_service.dart'; import '../utils/theme_mode_extensions.dart'; @@ -28,6 +32,7 @@ class SettingsScreen extends StatelessWidget { buildThemeWidget(settings), if (!kReleaseMode) buildColorBlindnessTestSettings(settings), buildClearCaches(context), + buildLogPanel(context, settings), ], ), ), @@ -96,4 +101,27 @@ class SettingsScreen extends StatelessWidget { ), ); } + + Widget buildLogPanel(BuildContext context, SettingsService settings) { + return ListTile( + title: Row( + children: [ + const Text('Log Level'), + const HorizontalPadding(), + ElevatedButton( + onPressed: () => context.pushNamed(ScreenPaths.logViewer), + child: const Text('Open Log Viewer'), + ) + ], + ), + trailing: DropdownButton( + value: settings.logLevel, + items: Level.LEVELS + .map((c) => DropdownMenuItem(value: c, child: Text(c.name))) + .toList(), + onChanged: (value) { + settings.logLevel = value ?? Level.OFF; + }), + ); + } } diff --git a/lib/services/log_service.dart b/lib/services/log_service.dart new file mode 100644 index 0000000..224f6f7 --- /dev/null +++ b/lib/services/log_service.dart @@ -0,0 +1,26 @@ +import 'dart:collection'; + +import 'package:flutter/foundation.dart'; +import 'package:logging/logging.dart'; + +const _defaultMaxItems = 1000; + +class LogService extends ChangeNotifier { + var maxItems = _defaultMaxItems; + + final _events = Queue(); + + List get events => UnmodifiableListView(_events); + + void add(LogRecord event) { + // final logName = event.loggerName.isEmpty ? 'ROOT' : event.loggerName; + // final msg = + // '${event.level.name} - $logName @ ${event.time}: ${event.message}'; + // print(msg); + _events.add(event); + if (_events.length > maxItems) { + _events.removeFirst(); + } + notifyListeners(); + } +} diff --git a/lib/services/setting_service.dart b/lib/services/setting_service.dart index 44b0220..75bbc9f 100644 --- a/lib/services/setting_service.dart +++ b/lib/services/setting_service.dart @@ -1,5 +1,6 @@ import 'package:color_blindness/color_blindness.dart'; import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../utils/theme_mode_extensions.dart'; @@ -40,6 +41,16 @@ class SettingsService extends ChangeNotifier { notifyListeners(); } + Level _logLevel = Level.SEVERE; + + Level get logLevel => _logLevel; + + set logLevel(Level level) { + _logLevel = level; + _prefs.setString(_logLevelKey, level.name); + notifyListeners(); + } + Future initialize() async { if (_initialized) { return; @@ -48,6 +59,7 @@ class SettingsService extends ChangeNotifier { _lowBandwidthMode = _prefs.getBool(_lowBandwidthModeKey) ?? false; _themeMode = ThemeModeExtensions.parse(_prefs.getString(_themeModeKey)); _colorBlindnessType = _colorBlindnessTypeFromPrefs(_prefs); + _logLevel = _levelFromPrefs(_prefs); _initialized = true; } } @@ -55,6 +67,7 @@ class SettingsService extends ChangeNotifier { const _lowBandwidthModeKey = 'LowBandwidthMode'; const _themeModeKey = 'ThemeMode'; const _colorBlindnessTestingModeKey = 'ColorBlindnessTestingMode'; +const _logLevelKey = 'LogLevel'; ColorBlindnessType _colorBlindnessTypeFromPrefs(SharedPreferences prefs) { final cbString = prefs.getString(_colorBlindnessTestingModeKey); @@ -66,3 +79,20 @@ ColorBlindnessType _colorBlindnessTypeFromPrefs(SharedPreferences prefs) { orElse: () => ColorBlindnessType.none, ); } + +Level _levelFromPrefs(SharedPreferences prefs) { + final levelString = prefs.getString(_logLevelKey); + return switch (levelString) { + 'ALL' => Level.ALL, + 'FINEST' => Level.FINEST, + 'FINER' => Level.FINER, + 'FINE' => Level.FINE, + 'CONFIG' => Level.CONFIG, + 'INFO' => Level.INFO, + 'WARNING' => Level.WARNING, + 'SEVERE' => Level.SEVERE, + 'SHOUT' => Level.SHOUT, + 'OFF' => Level.OFF, + _ => Level.OFF, + }; +} diff --git a/lib/utils/logrecord_extensions.dart b/lib/utils/logrecord_extensions.dart new file mode 100644 index 0000000..e7d4037 --- /dev/null +++ b/lib/utils/logrecord_extensions.dart @@ -0,0 +1,11 @@ +import 'package:logging/logging.dart'; + +extension LogRecordExtensions on LogRecord { + Map toJson() => { + 'logger': loggerName, + 'level': level.toString(), + 'time': time.toIso8601String(), + 'message': message, + 'stackTrace': stackTrace?.toString() ?? 'NONE', + }; +}