diff --git a/lib/controls/audio_video/media_kit_av_control.dart b/lib/controls/audio_video/media_kit_av_control.dart index 73d868b..fd04e9b 100644 --- a/lib/controls/audio_video/media_kit_av_control.dart +++ b/lib/controls/audio_video/media_kit_av_control.dart @@ -57,7 +57,7 @@ class _MediaKitAvControlState extends State { @override Widget build(BuildContext context) { - _logger.finer('Building MediaKit Control for ${widget.videoUrl}'); + _logger.finest('Building MediaKit Control for ${widget.videoUrl}'); if (controller == null) { return Container( width: widget.width, diff --git a/lib/controls/timeline/interactions_bar_control.dart b/lib/controls/timeline/interactions_bar_control.dart index c16cbeb..810dd93 100644 --- a/lib/controls/timeline/interactions_bar_control.dart +++ b/lib/controls/timeline/interactions_bar_control.dart @@ -51,14 +51,14 @@ class _InteractionsBarControlState extends State { isProcessing = true; }); final newState = !isFavorited; - _logger.finest('Trying to toggle favorite from $isFavorited to $newState'); + _logger.fine('Trying to toggle favorite from $isFavorited to $newState'); final result = await getIt>() .activeEntry .andThenAsync( (tm) async => await tm.toggleFavorited(widget.entry.id, newState)); result.match(onSuccess: (update) { setState(() { - _logger.finest( + _logger.fine( 'Success toggling! $isFavorited -> ${update.entry.isFavorited}'); }); }, onError: (error) { @@ -93,14 +93,14 @@ class _InteractionsBarControlState extends State { } final id = widget.entry.id; - _logger.finest('Trying to reshare $id'); + _logger.fine('Trying to reshare $id'); final result = await getIt>() .activeEntry .andThenAsync((tm) async => await tm.resharePost(id)); result.match(onSuccess: (update) { setState(() { - _logger.finest('Success resharing post by ${widget.entry.author}'); + _logger.fine('Success resharing post by ${widget.entry.author}'); }); }, onError: (error) { buildSnackbar(context, 'Error resharing post by ${widget.entry.author}'); @@ -122,13 +122,13 @@ class _InteractionsBarControlState extends State { isProcessing = true; }); final id = widget.entry.id; - _logger.finest('Trying to un-reshare $id'); + _logger.fine('Trying to un-reshare $id'); final result = await getIt>() .activeEntry .andThenAsync((tm) async => await tm.unResharePost(id)); result.match(onSuccess: (update) { setState(() { - _logger.finest('Success un-resharing post by ${widget.entry.author}'); + _logger.fine('Success un-resharing post by ${widget.entry.author}'); }); }, onError: (error) { buildSnackbar( diff --git a/lib/controls/timeline/post_control.dart b/lib/controls/timeline/post_control.dart index 5eef249..145486b 100644 --- a/lib/controls/timeline/post_control.dart +++ b/lib/controls/timeline/post_control.dart @@ -74,7 +74,7 @@ class _PostControlState extends State { final int scrollToIndex = _scrollToIndexCalc(items); // TODO Figure out why doesn't scroll to correct position on loading - _logger.finer('Building view with initial position at $scrollToIndex'); + _logger.finest('Building view with initial position at $scrollToIndex'); return ScrollablePositionedList.builder( physics: const AlwaysScrollableScrollPhysics(), diff --git a/lib/friendica_client/friendica_client.dart b/lib/friendica_client/friendica_client.dart index d627af0..7a0d6b7 100644 --- a/lib/friendica_client/friendica_client.dart +++ b/lib/friendica_client/friendica_client.dart @@ -817,6 +817,8 @@ class StatusesClient extends FriendicaClient { ...descendants .map((d) => TimelineEntryMastodonExtensions.fromJson(d)) ]; + _logger.finer(() => + 'Got status $id with full context which returned ${ancestors.length} ancestors and ${descendants.length} descendants'); return items; } else { return [TimelineEntryMastodonExtensions.fromJson(json)]; 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/editor.dart b/lib/screens/editor.dart index 8d6d9ab..99ce270 100644 --- a/lib/screens/editor.dart +++ b/lib/screens/editor.dart @@ -84,7 +84,7 @@ class _EditorScreenState extends State { parentEntry = entry; visibility = entry.visibility; }, onError: (error) { - _logger.finest('Error trying to get parent entry: $error'); + _logger.severe('Error trying to get parent entry: $error'); }); } @@ -96,13 +96,13 @@ class _EditorScreenState extends State { } void restoreStatusData() async { - _logger.fine('Attempting to load status for editing'); + _logger.finer('Attempting to load status for editing'); loaded = false; final result = await getIt>() .activeEntry .andThenAsync((manager) async => manager.getEntryById(widget.id)); result.match(onSuccess: (entry) { - _logger.fine('Loading status ${widget.id} information into fields'); + _logger.finer('Loading status ${widget.id} information into fields'); contentController.text = htmlToSimpleText(entry.body); spoilerController.text = entry.spoilerText; existingMediaItems @@ -227,7 +227,7 @@ class _EditorScreenState extends State { @override Widget build(BuildContext context) { - _logger.finest('Build editor $isComment $parentEntry'); + _logger.finer('Build editor $isComment $parentEntry'); final manager = context .read>() .activeEntry diff --git a/lib/screens/filter_editor_screen.dart b/lib/screens/filter_editor_screen.dart index 9434b9a..244a9d8 100644 --- a/lib/screens/filter_editor_screen.dart +++ b/lib/screens/filter_editor_screen.dart @@ -67,7 +67,7 @@ class _FilterEditorScreenState extends State { @override Widget build(BuildContext context) { - _logger.finer('Build for filter ${widget.id}'); + _logger.finest('Build for filter ${widget.id}'); final fieldWidth = MediaQuery.of(context).size.width * 0.8; final service = context .watch>() diff --git a/lib/screens/gallery_browsers_screen.dart b/lib/screens/gallery_browsers_screen.dart index 2b85628..b44e21a 100644 --- a/lib/screens/gallery_browsers_screen.dart +++ b/lib/screens/gallery_browsers_screen.dart @@ -84,7 +84,7 @@ class GalleryBrowsersScreen extends StatelessWidget { @override Widget build(BuildContext context) { - _logger.finest('Building'); + _logger.finer('Building'); final service = context .watch>() .activeEntry diff --git a/lib/screens/gallery_screen.dart b/lib/screens/gallery_screen.dart index d002f2f..0dafe95 100644 --- a/lib/screens/gallery_screen.dart +++ b/lib/screens/gallery_screen.dart @@ -24,7 +24,7 @@ class GalleryScreen extends StatelessWidget { @override Widget build(BuildContext context) { - _logger.finest('Building $galleryName'); + _logger.finer('Building $galleryName'); final nss = getIt(); return Scaffold( appBar: StandardAppBar.build(context, galleryName, actions: [ @@ -59,7 +59,7 @@ class _GalleryScreenBody extends StatelessWidget { @override Widget build(BuildContext context) { - _logger.finest('Building'); + _logger.finer('Building'); final service = context .watch>() .activeEntry diff --git a/lib/screens/home.dart b/lib/screens/home.dart index 1f51d00..82fb0d3 100644 --- a/lib/screens/home.dart +++ b/lib/screens/home.dart @@ -29,7 +29,7 @@ class _HomeScreenState extends State { TimelineIdentifiers currentTimeline = TimelineIdentifiers.home(); void updateTimeline(TimelineManager manager) { - _logger.finest('Updating timeline: $currentTimeline'); + _logger.finer('Updating timeline: $currentTimeline'); Future.delayed(const Duration(milliseconds: 100), () async { await manager.updateTimeline( currentTimeline, TimelineRefreshType.refresh); diff --git a/lib/screens/logviewer_screen.dart b/lib/screens/logviewer_screen.dart new file mode 100644 index 0000000..5260f60 --- /dev/null +++ b/lib/screens/logviewer_screen.dart @@ -0,0 +1,224 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_file_dialog/flutter_file_dialog.dart'; +import 'package:logging/logging.dart'; +import 'package:path_provider/path_provider.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/dateutils.dart'; +import '../utils/json_printer.dart'; +import '../utils/logrecord_extensions.dart'; +import '../utils/snackbar_builder.dart'; + +final _logger = Logger('LogViewerScreen'); + +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 = ''; + var attemptingWrite = false; + + Future _writeEventsLog(List events) async { + if (attemptingWrite) { + return; + } + + attemptingWrite = true; + try { + final json = + PrettyJsonEncoder().convert(events.map((e) => e.toJson()).toList()); + final filename = 'EventsLog_${DateTime.now().toFileNameString()}.json'; + if (Platform.isAndroid || Platform.isIOS) { + final params = SaveFileDialogParams( + data: Uint8List.fromList(json.codeUnits), + fileName: filename, + ); + await FlutterFileDialog.saveFile(params: params); + if (mounted) { + buildSnackbar(context, 'Wrote Events Log to: $filename'); + } + } else { + final appsDir = await getApplicationDocumentsDirectory(); + final location = await FilePicker.platform.saveFile( + dialogTitle: 'Save Events Log', + fileName: filename, + initialDirectory: appsDir.path, + ); + if (location != null) { + await File(location).writeAsString(json); + if (mounted) { + buildSnackbar(context, 'Wrote Events Log to: $location'); + } + } + } + } catch (e) { + _logger.severe('Error attempting to write out log: $e'); + if (mounted) { + buildSnackbar(context, 'Error attempting to write out log: $e'); + } + } + attemptingWrite = false; + } + + @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), + ], + ), + ), + ), + ), + floatingActionButton: FloatingActionButton.small( + onPressed: () => _writeEventsLog(events), + child: const Icon(Icons.save), + ), + ); + } + + 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: filterByModule ? null : 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( + enabled: filterByText, + 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/notifications_screen.dart b/lib/screens/notifications_screen.dart index 712af08..2c52a27 100644 --- a/lib/screens/notifications_screen.dart +++ b/lib/screens/notifications_screen.dart @@ -27,7 +27,7 @@ class NotificationsScreen extends StatelessWidget { @override Widget build(BuildContext context) { - _logger.finest('Building'); + _logger.finer('Building'); final nss = getIt(); final managerResult = context .watch>() diff --git a/lib/screens/search_screen.dart b/lib/screens/search_screen.dart index 47f2c7d..21f2dcd 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -101,7 +101,7 @@ class _SearchScreenState extends State { @override Widget build(BuildContext context) { - _logger.info('Build'); + _logger.finer('Build'); final nss = getIt(); final profileService = context.watch(); final profile = profileService.currentProfile; @@ -205,7 +205,7 @@ class _SearchScreenState extends State { } Widget buildResultBody(Profile profile) { - _logger.fine('Building search result body with: $searchResult'); + _logger.finer('Building search result body with: $searchResult'); switch (searchType) { case SearchTypes.hashTag: return buildHashtagResultWidget(profile); diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index e3c93d1..4fe2f86 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: Wrap( + 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/blocks_manager.dart b/lib/services/blocks_manager.dart index a0b2c05..d7f7d51 100644 --- a/lib/services/blocks_manager.dart +++ b/lib/services/blocks_manager.dart @@ -51,7 +51,7 @@ class BlocksManager extends ChangeNotifier { ); }).match( onSuccess: (blockedUser) { - _logger.finest( + _logger.fine( 'Successfully blocked ${blockedUser.name}: ${blockedUser.status}'); final existingIndex = _blocks.indexOf(connection); if (existingIndex < 0) { @@ -71,8 +71,8 @@ class BlocksManager extends ChangeNotifier { } Future unblockConnection(Connection connection) async { - _logger.finest( - 'Attempting to unblock ${connection.name}: ${connection.status}'); + _logger + .fine('Attempting to unblock ${connection.name}: ${connection.status}'); await RelationshipsClient(profile) .unblockConnection(connection) .withResult((blockedUser) { @@ -83,7 +83,7 @@ class BlocksManager extends ChangeNotifier { ); }).match( onSuccess: (unblockedUser) { - _logger.finest( + _logger.fine( 'Successfully unblocked ${unblockedUser.name}: ${unblockedUser.status}'); final existingIndex = _blocks.indexOf(connection); if (existingIndex >= 0) { 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/services/timeline_manager.dart b/lib/services/timeline_manager.dart index 407c101..b82601c 100644 --- a/lib/services/timeline_manager.dart +++ b/lib/services/timeline_manager.dart @@ -53,7 +53,7 @@ class TimelineManager extends ChangeNotifier { } Future _refreshCircleData() async { - _logger.finest('Refreshing member circle data '); + _logger.finer('Refreshing member circle data '); await CirclesClient(profile).getCircles().match( onSuccess: (circles) { circlesRepo.addAllCircles(circles); @@ -217,10 +217,10 @@ class TimelineManager extends ChangeNotifier { FutureResult toggleFavorited( String id, bool newStatus) async { - _logger.finest('Attempting toggling favorite $id to $newStatus'); + _logger.finer('Attempting toggling favorite $id to $newStatus'); final result = await entryManagerService.toggleFavorited(id, newStatus); if (result.isFailure) { - _logger.finest('Error toggling favorite $id: ${result.error}'); + _logger.info('Error toggling favorite $id: ${result.error}'); return result; } diff --git a/lib/utils/dateutils.dart b/lib/utils/dateutils.dart index b2ee82e..92c3cb9 100644 --- a/lib/utils/dateutils.dart +++ b/lib/utils/dateutils.dart @@ -58,3 +58,9 @@ class ElapsedDateUtils { return 'seconds ago'; } } + +const _separator = '_'; + +extension DateTimeExtensions on DateTime { + String toFileNameString() => '$year$month$day$_separator$hour$minute$second'; +} 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', + }; +} diff --git a/lib/utils/network_utils.dart b/lib/utils/network_utils.dart index dcded55..cbfa09c 100644 --- a/lib/utils/network_utils.dart +++ b/lib/utils/network_utils.dart @@ -17,7 +17,7 @@ FutureResult, ExecError> getUrl( Map? headers, Duration? timeout, }) async { - _logger.finer('GET: $url'); + _logger.fine('GET: $url'); final requestHeaders = headers ?? {}; if (usePhpDebugging) { requestHeaders['Cookie'] = 'XDEBUG_SESSION=PHPSTORM;path=/'; @@ -54,7 +54,7 @@ FutureResult postUrl( Map? headers, Duration? timeout, }) async { - _logger.finer('POST: $url \n Body: $body'); + _logger.fine('POST: $url \n Body: $body'); final requestHeaders = headers ?? {}; if (usePhpDebugging) { requestHeaders['Cookie'] = 'XDEBUG_SESSION=PHPSTORM;path=/'; @@ -88,7 +88,7 @@ FutureResult putUrl( Map body, { Map? headers, }) async { - _logger.finer('PUT: $url \n Body: $body'); + _logger.fine('PUT: $url \n Body: $body'); try { final request = http.put( url, @@ -118,7 +118,7 @@ FutureResult deleteUrl( Map body, { Map? headers, }) async { - _logger.finer('DELETE: $url'); + _logger.fine('DELETE: $url'); try { final request = http.delete( url,