diff --git a/lib/core/logging/logger.dart b/lib/core/logging/logger.dart new file mode 100644 index 0000000..1a5fbe4 --- /dev/null +++ b/lib/core/logging/logger.dart @@ -0,0 +1,103 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:flutter/widgets.dart'; +import 'package:intl/intl.dart'; +import 'package:logger/logger.dart'; +import 'package:paperless_mobile/core/service/file_service.dart'; +import 'package:path/path.dart' as p; +import 'package:rxdart/rxdart.dart'; + +late Logger logger; + +class MirroredFileOutput extends LogOutput { + late final File file; + final Completer _initCompleter = Completer(); + MirroredFileOutput(); + + @override + Future init() async { + final today = DateFormat("yyyy-MM-dd").format(DateTime.now()); + final logDir = await FileService.logDirectory; + file = File(p.join(logDir.path, '$today.log')); + debugPrint("Logging files to ${file.path}."); + _initCompleter.complete(); + try { + final oldLogs = await logDir.list().whereType().toList(); + if (oldLogs.length > 10) { + oldLogs + .sortedBy((file) => file.lastModifiedSync()) + .reversed + .skip(10) + .forEach((log) => log.delete()); + } + } catch (e) { + debugPrint("Failed to delete old logs..."); + } + } + + @override + void output(OutputEvent event) async { + for (var line in event.lines) { + debugPrint(line); + if (_initCompleter.isCompleted) { + await file.writeAsString( + "$line\n", + mode: FileMode.append, + ); + } + } + } +} + +class SpringBootLikePrinter extends LogPrinter { + SpringBootLikePrinter(); + static final _timestampFormat = DateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + + @override + List log(LogEvent event) { + final level = _buildLeftAligned(event.level.name.toUpperCase(), + Level.values.map((e) => e.name.length).max); + String message = _stringifyMessage(event.message); + final timestamp = + _buildLeftAligned(_timestampFormat.format(event.time), 23); + final traceRegex = RegExp(r"(.*)#(.*)\(\): (.*)"); + final match = traceRegex.firstMatch(message); + if (match != null) { + final className = match.group(1)!; + final methodName = match.group(2)!; + final remainingMessage = match.group(3)!; + final formattedClassName = _buildRightAligned(className, 25); + final formattedMethodName = _buildLeftAligned(methodName, 25); + message = message.replaceFirst(traceRegex, + "[$formattedClassName] - $formattedMethodName: $remainingMessage"); + } else { + message = List.filled(55, " ").join("") + ": " + message; + } + return [ + '$timestamp\t$level --- $message', + if (event.error != null) '\t\t${event.error}', + if (event.stackTrace != null) '\t\t${event.stackTrace.toString()}', + ]; + } + + String _buildLeftAligned(String message, int maxLength) { + return message.padRight(maxLength, ' '); + } + + String _buildRightAligned(String message, int maxLength) { + return message.padLeft(maxLength, ' '); + } + + String _stringifyMessage(dynamic message) { + final finalMessage = message is Function ? message() : message; + if (finalMessage is Map || finalMessage is Iterable) { + var encoder = const JsonEncoder.withIndent(null); + return encoder.convert(finalMessage); + } else { + return finalMessage.toString(); + } + } +} diff --git a/lib/core/logging/view/app_logs_page.dart b/lib/core/logging/view/app_logs_page.dart new file mode 100644 index 0000000..3f127b9 --- /dev/null +++ b/lib/core/logging/view/app_logs_page.dart @@ -0,0 +1,360 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:intl/intl.dart'; +import 'package:logger/logger.dart'; +import 'package:paperless_mobile/core/service/file_service.dart'; +import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +import 'package:path/path.dart' as p; +import 'package:rxdart/subjects.dart'; + +final _fileNameFormat = DateFormat("yyyy-MM-dd"); + +class AppLogsPage extends StatefulWidget { + const AppLogsPage({super.key}); + + @override + State createState() => _AppLogsPageState(); +} + +class _AppLogsPageState extends State { + final _fileContentStream = BehaviorSubject(); + final ScrollController _scrollController = ScrollController(); + + StreamSubscription? _fileChangesSubscription; + + late DateTime _date; + File? file; + bool autoScroll = true; + List? _availableLogs; + + Future _initFile() async { + final logDir = await FileService.logDirectory; + // logDir.listSync().whereType().forEach((element) { + // element.deleteSync(); + // }); + if (logDir.listSync().isEmpty) { + return; + } + + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + final filename = _fileNameFormat.format(_date); + setState(() { + file = File(p.join(logDir.path, '$filename.log')); + }); + _scrollController.addListener(_initialScrollListener); + _updateFileContent(); + _fileChangesSubscription?.cancel(); + _fileChangesSubscription = file!.watch().listen((event) async { + await _updateFileContent(); + }); + }); + } + + void _initialScrollListener() { + if (_scrollController.positions.isNotEmpty) { + _scrollController.animateTo( + 0, + duration: 500.milliseconds, + curve: Curves.easeIn, + ); + _scrollController.removeListener(_initialScrollListener); + } + } + + @override + void initState() { + super.initState(); + _date = DateTime.now().copyWith( + minute: 0, + hour: 0, + second: 0, + millisecond: 0, + microsecond: 0, + ); + _initFile(); + () async { + final logDir = await FileService.logDirectory; + final files = logDir.listSync(followLinks: false).whereType(); + final fileNames = files.map((e) => p.basenameWithoutExtension(e.path)); + final dates = + fileNames.map((filename) => _fileNameFormat.parseStrict(filename)); + _availableLogs = dates.toList(); + }(); + } + + @override + void dispose() { + _fileChangesSubscription?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final locale = Localizations.localeOf(context).toString(); + final theme = Theme.of(context); + return Scaffold( + appBar: AppBar( + title: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text("Logs"), + SizedBox(width: 16), + DropdownButton( + + value: _date, + items: [ + for (var date in _availableLogs ?? []) + DropdownMenuItem( + child: Text(DateFormat.yMMMd(locale).format(date)), + value: date, + ), + ], + onChanged: (value) { + if (value != null) { + setState(() { + _date = value; + }); + _initFile(); + } + }, + ), + ], + ), + actions: file != null + ? [ + IconButton( + tooltip: "Save log file to selected directory", + onPressed: () => _saveFile(locale), + icon: const Icon(Icons.download), + ), + IconButton( + tooltip: "Copy logs to clipboard", + onPressed: _copyToClipboard, + icon: const Icon(Icons.copy), + ).padded(), + ] + : null, + ), + body: Builder( + builder: (context) { + if (_availableLogs == null) { + return Center( + child: Text("No logs available."), + ); + } + return StreamBuilder( + stream: _fileContentStream, + builder: (context, snapshot) { + if (!snapshot.hasData || file == null) { + return const Center( + child: Text( + "Initializing logs...", + ), + ); + } + final messages = _transformLog(snapshot.data!).reversed.toList(); + return ColoredBox( + color: theme.colorScheme.background, + child: Column( + children: [ + Expanded( + child: ListView.builder( + reverse: true, + controller: _scrollController, + itemBuilder: (context, index) { + if (index == 0) { + return Center( + child: Text( + "End of logs.", + style: theme.textTheme.labelLarge?.copyWith( + fontStyle: FontStyle.italic, + ), + ), + ).padded(24); + } + final logMessage = messages[index - 1]; + final altColor = CupertinoDynamicColor.withBrightness( + color: Colors.grey.shade200, + darkColor: Colors.grey.shade800, + ).resolveFrom(context); + return _LogMessageWidget( + message: logMessage, + backgroundColor: (index % 2 == 0) + ? theme.colorScheme.background + : altColor, + ); + }, + itemCount: messages.length + 1, + ), + ), + ], + ), + ); + }, + ); + }, + ), + ); + } + + Future _saveFile(String locale) async { + assert(file != null); + var formattedDate = _fileNameFormat.format(_date); + final filename = 'paperless_mobile_logs_$formattedDate.log'; + final parentDir = await FilePicker.platform.getDirectoryPath( + dialogTitle: "Save log from ${DateFormat.yMd(locale).format(_date)}", + initialDirectory: + Platform.isAndroid ? "/storage/emulated/0/Download/" : null, + ); + if (parentDir != null) { + await file!.copy(p.join(parentDir, filename)); + } + } + + Future _copyToClipboard() async { + assert(file != null); + final content = await file!.readAsString(); + await Clipboard.setData(ClipboardData(text: content)); + } + + List<_LogMessage> _transformLog(String log) { + List<_LogMessage> messages = []; + List currentCoherentLines = []; + final lines = log.split("\n"); + for (var line in lines) { + final isMatch = _LogMessage.hasMatch(line); + if (currentCoherentLines.isNotEmpty && isMatch) { + messages.add(_LogMessage(message: currentCoherentLines.join("\n"))); + currentCoherentLines.clear(); + messages.add(_LogMessage.fromMessage(line)); + } + if (_LogMessage.hasMatch(line)) { + messages.add(_LogMessage.fromMessage(line)); + } else { + currentCoherentLines.add(line); + } + } + + return messages; + } + + Future _updateFileContent() async { + final content = await file!.readAsString(); + _fileContentStream.add(content); + Future.delayed(400.milliseconds, () { + _scrollController.animateTo( + 0, + duration: 500.milliseconds, + curve: Curves.easeIn, + ); + }); + } +} + +class _LogMessage { + static final RegExp pattern = RegExp( + r'(?\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\s*(?[A-Z]*)' + r'\s+---\s*(?:\[\s*(?.*)\]\s*-\s*(?.*)\s*)?:\s*(?.+)', + ); + final Level? level; + final String message; + final String? className; + final String? methodName; + final DateTime? timestamp; + + bool get isFormatted => level != null; + const _LogMessage({ + this.level, + required this.message, + this.className, + this.methodName, + this.timestamp, + }); + + static bool hasMatch(String message) => pattern.hasMatch(message); + + factory _LogMessage.fromMessage(String message) { + final match = pattern.firstMatch(message); + if (match == null) { + return _LogMessage(message: message); + } + return _LogMessage( + level: Level.values.byName(match.namedGroup('level')!.toLowerCase()), + message: match.namedGroup('message')!, + className: match.namedGroup('className'), + methodName: match.namedGroup('methodName'), + timestamp: DateTime.tryParse(match.namedGroup('timestamp') ?? ''), + ); + } +} + +class _LogMessageWidget extends StatelessWidget { + final _LogMessage message; + final Color backgroundColor; + const _LogMessageWidget({ + required this.message, + required this.backgroundColor, + }); + + @override + Widget build(BuildContext context) { + final c = Theme.of(context).colorScheme; + if (!message.isFormatted) { + return Text( + message.message, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + fontSize: 5, + color: c.onBackground.withOpacity(0.7), + ), + ); + } + final color = switch (message.level) { + Level.trace => c.onBackground.withOpacity(0.75), + Level.warning => Colors.yellow.shade600, + Level.error => Colors.red, + Level.fatal => Colors.red.shade900, + _ => c.onBackground, + }; + final icon = switch (message.level) { + Level.trace => Icons.troubleshoot, + Level.debug => Icons.bug_report, + Level.info => Icons.info_outline, + Level.warning => Icons.warning, + Level.error => Icons.error, + Level.fatal => Icons.error_outline, + _ => null, + }; + return Material( + child: ListTile( + trailing: Icon( + icon, + color: color, + ), + tileColor: backgroundColor, + title: Text( + message.message, + style: TextStyle(color: color), + ), + subtitle: message.className != null + ? Text( + "${message.className ?? ''} ${message.methodName ?? ''}", + style: TextStyle( + color: color.withOpacity(0.75), + fontSize: 10, + fontFamily: "monospace", + ), + ) + : null, + leading: message.timestamp != null + ? Text(DateFormat("HH:mm:ss.SSS").format(message.timestamp!)) + : null, + ), + ); + } +} diff --git a/lib/core/notifier/document_changed_notifier.dart b/lib/core/notifier/document_changed_notifier.dart index e1c2bba..f992ca3 100644 --- a/lib/core/notifier/document_changed_notifier.dart +++ b/lib/core/notifier/document_changed_notifier.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:rxdart/subjects.dart'; @@ -17,12 +16,10 @@ class DocumentChangedNotifier { Stream get $deleted => _deleted.asBroadcastStream(); void notifyUpdated(DocumentModel updated) { - debugPrint("Notifying updated document ${updated.id}"); _updated.add(updated); } void notifyDeleted(DocumentModel deleted) { - debugPrint("Notifying deleted document ${deleted.id}"); _deleted.add(deleted); } diff --git a/lib/core/repository/label_repository.dart b/lib/core/repository/label_repository.dart index e7b73dc..4e068df 100644 --- a/lib/core/repository/label_repository.dart +++ b/lib/core/repository/label_repository.dart @@ -11,19 +11,14 @@ class LabelRepository extends PersistentRepository { LabelRepository(this._api) : super(const LabelRepositoryState()); Future initialize() async { - debugPrint("[LabelRepository] initialize() called."); - try { + await Future.wait([ findAllCorrespondents(), findAllDocumentTypes(), findAllStoragePaths(), findAllTags(), ]); - } catch (error, stackTrace) { - debugPrint( - "[LabelRepository] An error occurred in initialize(): ${error.toString()}"); - debugPrintStack(stackTrace: stackTrace); - } + } Future createTag(Tag object) async { @@ -95,9 +90,7 @@ class LabelRepository extends PersistentRepository { Future> findAllCorrespondents( [Iterable? ids]) async { - debugPrint("Loading correspondents..."); final correspondents = await _api.getCorrespondents(ids); - debugPrint("${correspondents.length} correspondents successfully loaded."); final updatedState = { ...state.correspondents, }..addAll({for (var element in correspondents) element.id!: element}); diff --git a/lib/core/security/session_manager.dart b/lib/core/security/session_manager.dart index 8f2aba3..2244d34 100644 --- a/lib/core/security/session_manager.dart +++ b/lib/core/security/session_manager.dart @@ -39,14 +39,6 @@ class SessionManager extends ValueNotifier { DioUnauthorizedInterceptor(), DioHttpErrorInterceptor(), DioOfflineInterceptor(), - PrettyDioLogger( - compact: true, - responseBody: false, - responseHeader: false, - request: false, - requestBody: false, - requestHeader: false, - ), RetryOnConnectionChangeInterceptor(dio: dio) ]); return dio; diff --git a/lib/core/service/file_service.dart b/lib/core/service/file_service.dart index 32482a2..81c53b7 100644 --- a/lib/core/service/file_service.dart +++ b/lib/core/service/file_service.dart @@ -1,6 +1,8 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; +import 'package:paperless_mobile/core/logging/logger.dart'; +import 'package:paperless_mobile/helpers/format_helpers.dart'; import 'package:path_provider/path_provider.dart'; import 'package:rxdart/rxdart.dart'; import 'package:uuid/uuid.dart'; @@ -54,9 +56,24 @@ class FileService { } } + static Future get logDirectory async { + if (Platform.isAndroid) { + return getExternalStorageDirectories(type: StorageDirectory.documents) + .then((directory) async => + directory?.firstOrNull ?? + await getApplicationDocumentsDirectory()) + .then((directory) => + Directory('${directory.path}/logs').create(recursive: true)); + } else if (Platform.isIOS) { + return getApplicationDocumentsDirectory().then( + (value) => Directory('${value.path}/logs').create(recursive: true)); + } + throw UnsupportedError("Platform not supported."); + } + static Future get downloadsDirectory async { if (Platform.isAndroid) { - Directory directory = Directory('/storage/emulated/0/Download'); + var directory = Directory('/storage/emulated/0/Download'); if (!directory.existsSync()) { final downloadsDir = await getExternalStorageDirectories( type: StorageDirectory.downloads, @@ -93,12 +110,30 @@ class FileService { } static Future clearUserData({required String userId}) async { + logger.t("FileService#clearUserData(): Clearing data for user $userId..."); + final scanDir = await temporaryScansDirectory; + final scanDirSize = formatBytes(await getDirSizeInBytes(scanDir)); final tempDir = await temporaryDirectory; + final tempDirSize = formatBytes(await getDirSizeInBytes(tempDir)); final consumptionDir = await getConsumptionDirectory(userId: userId); + final consumptionDirSize = + formatBytes(await getDirSizeInBytes(consumptionDir)); + + logger.t("FileService#clearUserData(): Removing scans..."); await scanDir.delete(recursive: true); + logger.t("FileService#clearUserData(): Removed $scanDirSize..."); + + logger.t( + "FileService#clearUserData(): Removing temporary files and cache content..."); + await tempDir.delete(recursive: true); + logger.t("FileService#clearUserData(): Removed $tempDirSize..."); + + logger.t( + "FileService#clearUserData(): Removing files waiting for consumption..."); await consumptionDir.delete(recursive: true); + logger.t("FileService#clearUserData(): Removed $consumptionDirSize..."); } static Future clearDirectoryContent(PaperlessDirectoryType type) async { @@ -120,6 +155,12 @@ class FileService { static Future> getAllSubdirectories(Directory directory) { return directory.list().whereType().toList(); } + + static Future getDirSizeInBytes(Directory dir) async { + return dir + .list(recursive: true) + .fold(0, (previous, element) => previous + element.statSync().size); + } } enum PaperlessDirectoryType { diff --git a/lib/features/app_drawer/view/app_drawer.dart b/lib/features/app_drawer/view/app_drawer.dart index ad7bed7..9144dec 100644 --- a/lib/features/app_drawer/view/app_drawer.dart +++ b/lib/features/app_drawer/view/app_drawer.dart @@ -6,6 +6,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:paperless_mobile/constants.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/global/asset_images.dart'; +import 'package:paperless_mobile/core/logging/view/app_logs_page.dart'; import 'package:paperless_mobile/core/widgets/hint_card.dart'; import 'package:paperless_mobile/core/widgets/paperless_logo.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; @@ -181,6 +182,17 @@ class AppDrawer extends StatelessWidget { .fade(duration: 1.seconds, begin: 1, end: 0.3); }, ), + ListTile( + dense: true, + leading: const Icon(Icons.subject), + title: const Text('Logs'), //TODO: INTL + onTap: () { + Navigator.of(context) + .push(MaterialPageRoute(builder: (context) { + return const AppLogsPage(); + })); + }, + ), ListTile( dense: true, leading: const Icon(Icons.settings_outlined), diff --git a/lib/features/document_details/cubit/document_details_cubit.dart b/lib/features/document_details/cubit/document_details_cubit.dart index 30f0eba..e5c10cb 100644 --- a/lib/features/document_details/cubit/document_details_cubit.dart +++ b/lib/features/document_details/cubit/document_details_cubit.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:open_filex/open_filex.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/logging/logger.dart'; import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/service/file_service.dart'; @@ -169,7 +170,7 @@ class DocumentDetailsCubit extends Cubit { locale: locale, userId: userId, ); - debugPrint("Downloaded file to $targetPath"); + logger.i("Document '${state.document.title}' saved to $targetPath."); } Future shareDocument({bool shareOriginal = false}) async { diff --git a/lib/features/document_scan/cubit/document_scanner_cubit.dart b/lib/features/document_scan/cubit/document_scanner_cubit.dart index de54d25..9471dc0 100644 --- a/lib/features/document_scan/cubit/document_scanner_cubit.dart +++ b/lib/features/document_scan/cubit/document_scanner_cubit.dart @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/logging/logger.dart'; import 'package:paperless_mobile/core/model/info_message_exception.dart'; import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart'; @@ -18,13 +19,13 @@ class DocumentScannerCubit extends Cubit { : super(const InitialDocumentScannerState()); Future initialize() async { - debugPrint("Restoring scans..."); + logger.t("Restoring scans..."); emit(const RestoringDocumentScannerState()); final tempDir = await FileService.temporaryScansDirectory; final allFiles = tempDir.list().whereType(); final scans = await allFiles.where((event) => event.path.endsWith(".jpeg")).toList(); - debugPrint("Restored ${scans.length} scans."); + logger.t("Restored ${scans.length} scans."); emit( scans.isEmpty ? const InitialDocumentScannerState() diff --git a/lib/features/document_upload/view/document_upload_preparation_page.dart b/lib/features/document_upload/view/document_upload_preparation_page.dart index 5770464..b120fd9 100644 --- a/lib/features/document_upload/view/document_upload_preparation_page.dart +++ b/lib/features/document_upload/view/document_upload_preparation_page.dart @@ -12,6 +12,7 @@ import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/config/hive/hive_config.dart'; import 'package:paperless_mobile/core/database/tables/global_settings.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; +import 'package:paperless_mobile/core/logging/logger.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/widgets/future_or_builder.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; @@ -376,10 +377,17 @@ class _DocumentUploadPreparationPageState showErrorMessage(context, error, stackTrace); } on PaperlessFormValidationException catch (exception) { setState(() => _errors = exception.validationMessages); - } catch (unknownError, stackTrace) { - debugPrint(unknownError.toString()); + } catch (error, stackTrace) { + logger.e( + "An unknown error occurred during document upload.", + error: error, + stackTrace: stackTrace, + ); showErrorMessage( - context, const PaperlessApiException.unknown(), stackTrace); + context, + const PaperlessApiException.unknown(), + stackTrace, + ); } finally { setState(() { _isUploadLoading = false; diff --git a/lib/features/documents/cubit/documents_cubit.dart b/lib/features/documents/cubit/documents_cubit.dart index b5fb1a8..de482c9 100644 --- a/lib/features/documents/cubit/documents_cubit.dart +++ b/lib/features/documents/cubit/documents_cubit.dart @@ -74,7 +74,6 @@ class DocumentsCubit extends Cubit } Future bulkDelete(List documents) async { - debugPrint("[DocumentsCubit] bulkRemove"); await api.bulkAction( BulkDeleteAction(documents.map((doc) => doc.id)), ); @@ -85,7 +84,6 @@ class DocumentsCubit extends Cubit } void toggleDocumentSelection(DocumentModel model) { - debugPrint("[DocumentsCubit] toggleSelection"); if (state.selectedIds.contains(model.id)) { emit( state.copyWith( @@ -100,12 +98,10 @@ class DocumentsCubit extends Cubit } void resetSelection() { - debugPrint("[DocumentsCubit] resetSelection"); emit(state.copyWith(selection: [])); } void reset() { - debugPrint("[DocumentsCubit] reset"); emit(const DocumentsState()); } diff --git a/lib/features/inbox/cubit/inbox_cubit.dart b/lib/features/inbox/cubit/inbox_cubit.dart index 7ab9bb8..c4c3430 100644 --- a/lib/features/inbox/cubit/inbox_cubit.dart +++ b/lib/features/inbox/cubit/inbox_cubit.dart @@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/logging/logger.dart'; import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository_state.dart'; @@ -83,11 +84,17 @@ class InboxCubit extends HydratedCubit } Future refreshItemsInInboxCount([bool shouldLoadInbox = true]) async { - debugPrint("Checking for new items in inbox..."); + logger.t( + "InboxCubit#refreshItemsInInboxCount(): Checking for new documents in inbox..."); final stats = await _statsApi.getServerStatistics(); if (stats.documentsInInbox != state.itemsInInboxCount && shouldLoadInbox) { + logger.t( + "InboxCubit#refreshItemsInInboxCount(): New documents found in inbox, reloading inbox."); await loadInbox(); + } else { + logger.t( + "InboxCubit#refreshItemsInInboxCount(): No new documents found in inbox."); } emit(state.copyWith(itemsInInboxCount: stats.documentsInInbox)); } @@ -97,7 +104,6 @@ class InboxCubit extends HydratedCubit /// Future loadInbox() async { if (!isClosed) { - debugPrint("Initializing inbox..."); final inboxTags = await _labelRepository.findAllTags().then( (tags) => tags.where((t) => t.isInboxTag).map((t) => t.id!), ); diff --git a/lib/features/labels/view/pages/labels_page.dart b/lib/features/labels/view/pages/labels_page.dart index fe2a6a7..12368d2 100644 --- a/lib/features/labels/view/pages/labels_page.dart +++ b/lib/features/labels/view/pages/labels_page.dart @@ -7,6 +7,7 @@ import 'package:paperless_mobile/core/config/hive/hive_config.dart'; import 'package:paperless_mobile/core/database/tables/global_settings.dart'; import 'package:paperless_mobile/core/database/tables/local_user_account.dart'; import 'package:paperless_mobile/core/delegate/customizable_sliver_persistent_header_delegate.dart'; +import 'package:paperless_mobile/core/logging/logger.dart'; import 'package:paperless_mobile/core/widgets/material/colored_tab_bar.dart'; import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart'; import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart'; @@ -212,17 +213,16 @@ class _LabelsPageState extends State ][_currentIndex] .call(); } catch (error, stackTrace) { - debugPrint( - "[LabelsPage] RefreshIndicator.onRefresh " + logger.e( + "An error ocurred while reloading " "${[ "correspondents", "document types", "tags", "storage paths" - ][_currentIndex]}: " - "An error occurred (${error.toString()})", + ][_currentIndex]}: ${error.toString()}", + stackTrace: stackTrace, ); - debugPrintStack(stackTrace: stackTrace); } }, child: TabBarView( diff --git a/lib/features/landing/view/landing_page.dart b/lib/features/landing/view/landing_page.dart index 78c7deb..08d31b1 100644 --- a/lib/features/landing/view/landing_page.dart +++ b/lib/features/landing/view/landing_page.dart @@ -18,12 +18,6 @@ import 'package:paperless_mobile/routes/typed/shells/authenticated_route.dart'; import 'package:paperless_mobile/routes/typed/top_level/changelog_route.dart'; import 'package:shared_preferences/shared_preferences.dart'; -class Changelog { - final int buildNumber; - final String? changelog; - Changelog(this.buildNumber, this.changelog); -} - class LandingPage extends StatefulWidget { const LandingPage({super.key}); diff --git a/lib/features/login/cubit/authentication_cubit.dart b/lib/features/login/cubit/authentication_cubit.dart index d4ba6e2..2631069 100644 --- a/lib/features/login/cubit/authentication_cubit.dart +++ b/lib/features/login/cubit/authentication_cubit.dart @@ -13,6 +13,7 @@ import 'package:paperless_mobile/core/database/tables/local_user_settings.dart'; import 'package:paperless_mobile/core/database/tables/user_credentials.dart'; import 'package:paperless_mobile/core/factory/paperless_api_factory.dart'; import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart'; +import 'package:paperless_mobile/core/logging/logger.dart'; import 'package:paperless_mobile/core/model/info_message_exception.dart'; import 'package:paperless_mobile/core/security/session_manager.dart'; import 'package:paperless_mobile/core/service/connectivity_status_service.dart'; @@ -55,10 +56,7 @@ class AuthenticationCubit extends Cubit { } emit(const AuthenticatingState(AuthenticatingStage.authenticating)); final localUserId = "${credentials.username}@$serverUrl"; - _debugPrintMessage( - "login", - "Trying to login $localUserId...", - ); + logger.t("AuthenticationCubit#login(): Trying to log in $localUserId..."); try { await _addUser( localUserId, @@ -97,35 +95,26 @@ class AuthenticationCubit extends Cubit { await globalSettings.save(); emit(AuthenticatedState(localUserId: localUserId)); - _debugPrintMessage( - "login", - "User successfully logged in.", - ); + logger.t( + 'AuthenticationCubit#login(): User $localUserId successfully logged in.'); } /// Switches to another account if it exists. Future switchAccount(String localUserId) async { emit(const SwitchingAccountsState()); - _debugPrintMessage( - "switchAccount", - "Trying to switch to user $localUserId...", - ); + logger.t( + 'AuthenticationCubit#switchAccount(): Trying to switch to user $localUserId...'); final globalSettings = Hive.box(HiveBoxes.globalSettings).getValue()!; - // if (globalSettings.loggedInUserId == localUserId) { - // _debugPrintMessage( - // "switchAccount", - // "User $localUserId is already logged in.", - // ); - // emit(AuthenticatedState(localUserId: localUserId)); - // return; - // } final userAccountBox = Hive.localUserAccountBox; if (!userAccountBox.containsKey(localUserId)) { - debugPrint("User $localUserId not yet registered."); + logger.w( + 'AuthenticationCubit#switchAccount(): User $localUserId not yet registered. ' + 'This should never be the case!', + ); return; } @@ -135,10 +124,8 @@ class AuthenticationCubit extends Cubit { final authenticated = await _localAuthService .authenticateLocalUser("Authenticate to switch your account."); if (!authenticated) { - _debugPrintMessage( - "switchAccount", - "User could not be authenticated.", - ); + logger.w( + "AuthenticationCubit#switchAccount(): User could not be authenticated."); emit(VerifyIdentityState(userId: localUserId)); return; } @@ -151,7 +138,8 @@ class AuthenticationCubit extends Cubit { HiveBoxes.localUserCredentials, (credentialsBox) async { if (!credentialsBox.containsKey(localUserId)) { await credentialsBox.close(); - debugPrint("Invalid authentication for $localUserId"); + logger.w( + "AuthenticationCubit#switchAccount(): Invalid authentication for $localUserId."); return; } final credentials = credentialsBox.get(localUserId); @@ -188,6 +176,8 @@ class AuthenticationCubit extends Cubit { }) async { assert(credentials.password != null && credentials.username != null); final localUserId = "${credentials.username}@$serverUrl"; + logger + .d("AuthenticationCubit#addAccount(): Adding account $localUserId..."); final sessionManager = SessionManager([ LanguageHeaderInterceptor(locale), @@ -204,8 +194,11 @@ class AuthenticationCubit extends Cubit { } Future removeAccount(String userId) async { + logger + .t("AuthenticationCubit#removeAccount(): Removing account $userId..."); final userAccountBox = Hive.localUserAccountBox; final userAppStateBox = Hive.localUserAppStateBox; + await FileService.clearUserData(userId: userId); await userAccountBox.delete(userId); await userAppStateBox.delete(userId); @@ -220,19 +213,15 @@ class AuthenticationCubit extends Cubit { /// Future restoreSession([String? userId]) async { emit(const RestoringSessionState()); - _debugPrintMessage( - "restoreSessionState", - "Trying to restore previous session...", - ); + logger.t( + "AuthenticationCubit#restoreSessionState(): Trying to restore previous session..."); final globalSettings = Hive.box(HiveBoxes.globalSettings).getValue()!; final restoreSessionForUser = userId ?? globalSettings.loggedInUserId; // final localUserId = globalSettings.loggedInUserId; if (restoreSessionForUser == null) { - _debugPrintMessage( - "restoreSessionState", - "There is nothing to restore.", - ); + logger.t( + "AuthenticationCubit#restoreSessionState(): There is nothing to restore."); final otherAccountsExist = Hive.localUserAccountBox.isNotEmpty; // If there is nothing to restore, we can quit here. emit( @@ -243,42 +232,25 @@ class AuthenticationCubit extends Cubit { final localUserAccountBox = Hive.box(HiveBoxes.localUserAccount); final localUserAccount = localUserAccountBox.get(restoreSessionForUser)!; - _debugPrintMessage( - "restoreSessionState", - "Checking if biometric authentication is required...", - ); if (localUserAccount.settings.isBiometricAuthenticationEnabled) { - _debugPrintMessage( - "restoreSessionState", - "Biometric authentication required, waiting for user to authenticate...", - ); + logger.t( + "AuthenticationCubit#restoreSessionState(): Verifying user identity..."); final authenticationMesage = (await S.delegate.load(Locale(globalSettings.preferredLocaleSubtag))) .verifyYourIdentity; final localAuthSuccess = await _localAuthService.authenticateLocalUser(authenticationMesage); if (!localAuthSuccess) { + logger.w( + "AuthenticationCubit#restoreSessionState(): Identity could not be verified."); emit(VerifyIdentityState(userId: restoreSessionForUser)); - _debugPrintMessage( - "restoreSessionState", - "User could not be authenticated.", - ); return; } - _debugPrintMessage( - "restoreSessionState", - "User successfully autheticated.", - ); - } else { - _debugPrintMessage( - "restoreSessionState", - "Biometric authentication not configured, skipping.", - ); + logger.t( + "AuthenticationCubit#restoreSessionState(): Identity successfully verified."); } - _debugPrintMessage( - "restoreSessionState", - "Trying to retrieve authentication credentials...", - ); + logger.t( + "AuthenticationCubit#restoreSessionState(): Reading encrypted credentials..."); final authentication = await withEncryptedBox( HiveBoxes.localUserCredentials, (box) { @@ -286,80 +258,62 @@ class AuthenticationCubit extends Cubit { }); if (authentication == null) { - _debugPrintMessage( - "restoreSessionState", - "Could not retrieve existing authentication credentials.", - ); + logger.e( + "AuthenticationCubit#restoreSessionState(): Credentials could not be read!"); throw Exception( "User should be authenticated but no authentication information was found.", ); } + logger.t( + "AuthenticationCubit#restoreSessionState(): Credentials successfully retrieved."); - _debugPrintMessage( - "restoreSessionState", - "Authentication credentials successfully retrieved.", - ); - - _debugPrintMessage( - "restoreSessionState", - "Updating current session state...", - ); + logger.t( + "AuthenticationCubit#restoreSessionState(): Updating security context..."); _sessionManager.updateSettings( clientCertificate: authentication.clientCertificate, authToken: authentication.token, baseUrl: localUserAccount.serverUrl, ); - _debugPrintMessage( - "restoreSessionState", - "Current session state successfully updated.", - ); + logger.t( + "AuthenticationCubit#restoreSessionState(): Security context successfully updated."); final isPaperlessServerReachable = await _connectivityService.isPaperlessServerReachable( localUserAccount.serverUrl, authentication.clientCertificate, ) == ReachabilityStatus.reachable; + logger.t( + "AuthenticationCubit#restoreSessionState(): Trying to update remote paperless user..."); if (isPaperlessServerReachable) { - _debugPrintMessage( - "restoreSessionMState", - "Updating server user...", - ); final apiVersion = await _getApiVersion(_sessionManager.client); await _updateRemoteUser( _sessionManager, localUserAccount, apiVersion, ); - _debugPrintMessage( - "restoreSessionMState", - "Successfully updated server user.", - ); + logger.t( + "AuthenticationCubit#restoreSessionState(): Successfully updated remote paperless user."); } else { - _debugPrintMessage( - "restoreSessionMState", - "Skipping update of server user (server could not be reached).", - ); + logger.w( + "AuthenticationCubit#restoreSessionState(): Could not update remote paperless user. Server could not be reached. The app might behave unexpected!"); } globalSettings.loggedInUserId = restoreSessionForUser; await globalSettings.save(); emit(AuthenticatedState(localUserId: restoreSessionForUser)); - _debugPrintMessage( - "restoreSessionState", - "Session was successfully restored.", - ); + logger.t( + "AuthenticationCubit#restoreSessionState(): Previous session successfully restored."); } Future logout([bool removeAccount = false]) async { emit(const LoggingOutState()); - _debugPrintMessage( - "logout", - "Trying to log out current user...", - ); - await _resetExternalState(); final globalSettings = Hive.globalSettingsBox.getValue()!; final userId = globalSettings.loggedInUserId!; + logger.t( + "AuthenticationCubit#logout(): Logging out current user ($userId)..."); + + await _resetExternalState(); await _notificationService.cancelUserNotifications(userId); final otherAccountsExist = Hive.localUserAccountBox.length > 1; @@ -370,15 +324,19 @@ class AuthenticationCubit extends Cubit { globalSettings.loggedInUserId = null; await globalSettings.save(); - _debugPrintMessage( - "logout", - "User successfully logged out.", - ); + logger.t("AuthenticationCubit#logout(): User successfully logged out."); } Future _resetExternalState() async { + logger.t( + "AuthenticationCubit#_resetExternalState(): Resetting security context..."); _sessionManager.resetSettings(); + logger.t( + "AuthenticationCubit#_resetExternalState(): Security context reset."); + logger.t( + "AuthenticationCubit#_resetExternalState(): Clearing local state..."); await HydratedBloc.storage.clear(); + logger.t("AuthenticationCubit#_resetExternalState(): Local state cleard."); } Future _addUser( @@ -392,7 +350,8 @@ class AuthenticationCubit extends Cubit { _FutureVoidCallback? onFetchUserInformation, }) async { assert(credentials.username != null && credentials.password != null); - _debugPrintMessage("_addUser", "Adding new user $localUserId..."); + logger + .t("AuthenticationCubit#_addUser(): Adding new user $localUserId...."); sessionManager.updateSettings( baseUrl: serverUrl, @@ -401,10 +360,8 @@ class AuthenticationCubit extends Cubit { final authApi = _apiFactory.createAuthenticationApi(sessionManager.client); - _debugPrintMessage( - "_addUser", - "Trying to login user ${credentials.username} on $serverUrl...", - ); + logger.t( + "AuthenticationCubit#_addUser(): Fetching bearer token from the server..."); await onPerformLogin?.call(); @@ -413,10 +370,8 @@ class AuthenticationCubit extends Cubit { password: credentials.password!, ); - _debugPrintMessage( - "_addUser", - "Successfully acquired token.", - ); + logger.t( + "AuthenticationCubit#_addUser(): Bearer token successfully retrieved."); sessionManager.updateSettings( baseUrl: serverUrl, @@ -430,18 +385,15 @@ class AuthenticationCubit extends Cubit { Hive.box(HiveBoxes.localUserAppState); if (userAccountBox.containsKey(localUserId)) { - _debugPrintMessage( - "_addUser", - "An error occurred! The user $localUserId already exists.", - ); + logger.w( + "AuthenticationCubit#_addUser(): The user $localUserId already exists."); throw InfoMessageException(code: ErrorCode.userAlreadyExists); } await onFetchUserInformation?.call(); final apiVersion = await _getApiVersion(sessionManager.client); - _debugPrintMessage( - "_addUser", - "Trying to fetch user object for $localUserId...", - ); + logger.t( + "AuthenticationCubit#_addUser(): Trying to fetch remote paperless user for $localUserId."); + late UserModel serverUser; try { serverUser = await _apiFactory @@ -451,21 +403,20 @@ class AuthenticationCubit extends Cubit { ) .findCurrentUser(); } on DioException catch (error, stackTrace) { - _debugPrintMessage( - "_addUser", - "An error occurred: ${error.message}", + logger.e( + "AuthenticationCubit#_addUser(): An error occurred while fetching the remote paperless user.", + error: error, stackTrace: stackTrace, ); + rethrow; } - _debugPrintMessage( - "_addUser", - "User object successfully fetched.", - ); - _debugPrintMessage( - "_addUser", - "Persisting local user account...", - ); + logger.t( + "AuthenticationCubit#_addUser(): Remote paperless user successfully fetched."); + + logger.t( + "AuthenticationCubit#_addUser(): Persisting user account information..."); + await onPersistLocalUserData?.call(); // Create user account await userAccountBox.put( @@ -478,29 +429,21 @@ class AuthenticationCubit extends Cubit { apiVersion: apiVersion, ), ); - _debugPrintMessage( - "_addUser", - "Local user account successfully persisted.", - ); - _debugPrintMessage( - "_addUser", - "Persisting user state...", - ); + logger.t( + "AuthenticationCubit#_addUser(): User account information successfully persisted."); + logger.t("AuthenticationCubit#_addUser(): Persisting user app state..."); // Create user state await userStateBox.put( localUserId, LocalUserAppState(userId: localUserId), ); - _debugPrintMessage( - "_addUser", - "User state successfully persisted.", - ); + logger.t( + "AuthenticationCubit#_addUser(): User state successfully persisted."); // Save credentials in encrypted box await withEncryptedBox(HiveBoxes.localUserCredentials, (box) async { - _debugPrintMessage( - "_addUser", - "Saving user credentials inside encrypted storage...", - ); + logger.t( + "AuthenticationCubit#_addUser(): Saving user credentials inside encrypted storage..."); + await box.put( localUserId, UserCredentials( @@ -508,10 +451,8 @@ class AuthenticationCubit extends Cubit { clientCertificate: clientCert, ), ); - _debugPrintMessage( - "_addUser", - "User credentials successfully saved.", - ); + logger.t( + "AuthenticationCubit#_addUser(): User credentials successfully saved."); }); final hostsBox = Hive.box(HiveBoxes.hosts); if (!hostsBox.values.contains(serverUrl)) { @@ -526,10 +467,8 @@ class AuthenticationCubit extends Cubit { Duration? timeout, int defaultValue = 2, }) async { - _debugPrintMessage( - "_getApiVersion", - "Trying to fetch API version...", - ); + logger.t( + "AuthenticationCubit#_getApiVersion(): Trying to fetch API version..."); try { final response = await dio.get( "/api/", @@ -539,12 +478,13 @@ class AuthenticationCubit extends Cubit { ); final apiVersion = int.parse(response.headers.value('x-api-version') ?? "3"); - _debugPrintMessage( - "_getApiVersion", - "API version ($apiVersion) successfully retrieved.", - ); + logger.t( + "AuthenticationCubit#_getApiVersion(): Successfully retrieved API version ($apiVersion)."); + return apiVersion; } on DioException catch (_) { + logger.w( + "AuthenticationCubit#_getApiVersion(): Could not retrieve API version."); return defaultValue; } } @@ -555,10 +495,8 @@ class AuthenticationCubit extends Cubit { LocalUserAccount localUserAccount, int apiVersion, ) async { - _debugPrintMessage( - "_updateRemoteUser", - "Updating paperless user object...", - ); + logger.t( + "AuthenticationCubit#_updateRemoteUser(): Trying to update remote user object..."); final updatedPaperlessUser = await _apiFactory .createUserApi( sessionManager.client, @@ -568,24 +506,7 @@ class AuthenticationCubit extends Cubit { localUserAccount.paperlessUser = updatedPaperlessUser; await localUserAccount.save(); - _debugPrintMessage( - "_updateRemoteUser", - "Paperless user object successfully updated.", - ); - } - - void _debugPrintMessage( - String methodName, - String message, { - Object? error, - StackTrace? stackTrace, - }) { - debugPrint("AuthenticationCubit#$methodName: $message"); - if (error != null) { - debugPrint(error.toString()); - } - if (stackTrace != null) { - debugPrintStack(stackTrace: stackTrace); - } + logger.t( + "AuthenticationCubit#_updateRemoteUser(): Successfully updated remote user object."); } } diff --git a/lib/helpers/format_helpers.dart b/lib/helpers/format_helpers.dart index d93ca57..7c52aec 100644 --- a/lib/helpers/format_helpers.dart +++ b/lib/helpers/format_helpers.dart @@ -7,7 +7,7 @@ String formatMaxCount(int? count, [int maxCount = 99]) { return (count ?? 0).toString(); } -String formatBytes(int bytes, int decimals) { +String formatBytes(int bytes, [int decimals = 2]) { if (bytes <= 0) return "0 B"; const suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; var i = (log(bytes) / log(1024)).floor(); diff --git a/lib/main.dart b/lib/main.dart index ef06c89..80c2948 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,6 +16,7 @@ import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/intl_standalone.dart'; import 'package:local_auth/local_auth.dart'; +import 'package:logger/logger.dart' as l; import 'package:package_info_plus/package_info_plus.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/constants.dart'; @@ -28,6 +29,7 @@ import 'package:paperless_mobile/core/exception/server_message_exception.dart'; import 'package:paperless_mobile/core/factory/paperless_api_factory.dart'; import 'package:paperless_mobile/core/factory/paperless_api_factory_impl.dart'; import 'package:paperless_mobile/core/interceptor/language_header.interceptor.dart'; +import 'package:paperless_mobile/core/logging/logger.dart'; import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; import 'package:paperless_mobile/core/security/session_manager.dart'; import 'package:paperless_mobile/core/service/connectivity_status_service.dart'; @@ -45,6 +47,7 @@ import 'package:paperless_mobile/routes/typed/top_level/logging_out_route.dart'; import 'package:paperless_mobile/routes/typed/top_level/login_route.dart'; import 'package:paperless_mobile/theme.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:pretty_dio_logger/pretty_dio_logger.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -82,7 +85,7 @@ Future performMigrations() async { final requiresMigrationForCurrentVersion = !performedMigrations.contains(currentVersion); if (requiresMigrationForCurrentVersion) { - debugPrint("Applying migration scripts for version $currentVersion"); + logger.t("Applying migration scripts for version $currentVersion"); await migrationProcedure(); await sp.setStringList( 'performed_migrations', @@ -91,7 +94,6 @@ Future performMigrations() async { } } - Future _initHive() async { await Hive.initFlutter(); @@ -125,6 +127,13 @@ void main() async { // ) // .start(); // } + + logger = l.Logger( + output: MirroredFileOutput(), + printer: SpringBootLikePrinter(), + level: l.Level.trace, + ); + packageInfo = await PackageInfo.fromPlatform(); if (Platform.isAndroid) { @@ -160,6 +169,15 @@ void main() async { // Manages security context, required for self signed client certificates final sessionManager = SessionManager([ languageHeaderInterceptor, + PrettyDioLogger( + compact: true, + responseBody: false, + responseHeader: false, + request: false, + requestBody: false, + requestHeader: false, + logPrint: (object) => logger.t, + ), ]); // Initialize Blocs/Cubits @@ -214,9 +232,7 @@ void main() async { ServerMessageException e => e.message, _ => error.toString() }; - debugPrint("An unepxected exception has occured!"); - debugPrint(message); - debugPrintStack(stackTrace: stack); + logger.e(message, stackTrace: stack); }); } @@ -254,7 +270,7 @@ class _GoRouterShellState extends State { final DisplayMode mostOptimalMode = sameResolution.isNotEmpty ? sameResolution.first : active; - debugPrint('Setting refresh rate to ${mostOptimalMode.refreshRate}'); + logger.d('Setting refresh rate to ${mostOptimalMode.refreshRate}'); await FlutterDisplayMode.setPreferredMode(mostOptimalMode); } diff --git a/pubspec.lock b/pubspec.lock index 8585aac..a13759c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -997,6 +997,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.10" + logger: + dependency: "direct main" + description: + name: logger + sha256: "6bbb9d6f7056729537a4309bda2e74e18e5d9f14302489cc1e93f33b3fe32cac" + url: "https://pub.dev" + source: hosted + version: "2.0.2+1" logging: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 561da11..20a9a79 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -98,6 +98,7 @@ dependencies: flutter_animate: ^4.2.0+1 shared_preferences: ^2.2.1 flutter_markdown: ^0.6.18 + logger: ^2.0.2+1 # camerawesome: ^2.0.0-dev.1 dependency_overrides: