feat: add file logs and logging view

This commit is contained in:
Anton Stubenbord
2023-10-11 19:09:26 +02:00
parent a102389cd8
commit f0c3ced804
19 changed files with 681 additions and 231 deletions

View File

@@ -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<void> 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<File>().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<String> 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();
}
}
}

View File

@@ -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<AppLogsPage> createState() => _AppLogsPageState();
}
class _AppLogsPageState extends State<AppLogsPage> {
final _fileContentStream = BehaviorSubject();
final ScrollController _scrollController = ScrollController();
StreamSubscription? _fileChangesSubscription;
late DateTime _date;
File? file;
bool autoScroll = true;
List<DateTime>? _availableLogs;
Future<void> _initFile() async {
final logDir = await FileService.logDirectory;
// logDir.listSync().whereType<File>().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<File>();
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<DateTime>(
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<void> _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<void> _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<String> 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<void> _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'(?<timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\s*(?<level>[A-Z]*)'
r'\s+---\s*(?:\[\s*(?<className>.*)\]\s*-\s*(?<methodName>.*)\s*)?:\s*(?<message>.+)',
);
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,
),
);
}
}

View File

@@ -1,6 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:rxdart/subjects.dart'; import 'package:rxdart/subjects.dart';
@@ -17,12 +16,10 @@ class DocumentChangedNotifier {
Stream<DocumentModel> get $deleted => _deleted.asBroadcastStream(); Stream<DocumentModel> get $deleted => _deleted.asBroadcastStream();
void notifyUpdated(DocumentModel updated) { void notifyUpdated(DocumentModel updated) {
debugPrint("Notifying updated document ${updated.id}");
_updated.add(updated); _updated.add(updated);
} }
void notifyDeleted(DocumentModel deleted) { void notifyDeleted(DocumentModel deleted) {
debugPrint("Notifying deleted document ${deleted.id}");
_deleted.add(deleted); _deleted.add(deleted);
} }

View File

@@ -11,19 +11,14 @@ class LabelRepository extends PersistentRepository<LabelRepositoryState> {
LabelRepository(this._api) : super(const LabelRepositoryState()); LabelRepository(this._api) : super(const LabelRepositoryState());
Future<void> initialize() async { Future<void> initialize() async {
debugPrint("[LabelRepository] initialize() called.");
try {
await Future.wait([ await Future.wait([
findAllCorrespondents(), findAllCorrespondents(),
findAllDocumentTypes(), findAllDocumentTypes(),
findAllStoragePaths(), findAllStoragePaths(),
findAllTags(), findAllTags(),
]); ]);
} catch (error, stackTrace) {
debugPrint(
"[LabelRepository] An error occurred in initialize(): ${error.toString()}");
debugPrintStack(stackTrace: stackTrace);
}
} }
Future<Tag> createTag(Tag object) async { Future<Tag> createTag(Tag object) async {
@@ -95,9 +90,7 @@ class LabelRepository extends PersistentRepository<LabelRepositoryState> {
Future<Iterable<Correspondent>> findAllCorrespondents( Future<Iterable<Correspondent>> findAllCorrespondents(
[Iterable<int>? ids]) async { [Iterable<int>? ids]) async {
debugPrint("Loading correspondents...");
final correspondents = await _api.getCorrespondents(ids); final correspondents = await _api.getCorrespondents(ids);
debugPrint("${correspondents.length} correspondents successfully loaded.");
final updatedState = { final updatedState = {
...state.correspondents, ...state.correspondents,
}..addAll({for (var element in correspondents) element.id!: element}); }..addAll({for (var element in correspondents) element.id!: element});

View File

@@ -39,14 +39,6 @@ class SessionManager extends ValueNotifier<Dio> {
DioUnauthorizedInterceptor(), DioUnauthorizedInterceptor(),
DioHttpErrorInterceptor(), DioHttpErrorInterceptor(),
DioOfflineInterceptor(), DioOfflineInterceptor(),
PrettyDioLogger(
compact: true,
responseBody: false,
responseHeader: false,
request: false,
requestBody: false,
requestHeader: false,
),
RetryOnConnectionChangeInterceptor(dio: dio) RetryOnConnectionChangeInterceptor(dio: dio)
]); ]);
return dio; return dio;

View File

@@ -1,6 +1,8 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart'; 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:path_provider/path_provider.dart';
import 'package:rxdart/rxdart.dart'; import 'package:rxdart/rxdart.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@@ -54,9 +56,24 @@ class FileService {
} }
} }
static Future<Directory> 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<Directory> get downloadsDirectory async { static Future<Directory> get downloadsDirectory async {
if (Platform.isAndroid) { if (Platform.isAndroid) {
Directory directory = Directory('/storage/emulated/0/Download'); var directory = Directory('/storage/emulated/0/Download');
if (!directory.existsSync()) { if (!directory.existsSync()) {
final downloadsDir = await getExternalStorageDirectories( final downloadsDir = await getExternalStorageDirectories(
type: StorageDirectory.downloads, type: StorageDirectory.downloads,
@@ -93,12 +110,30 @@ class FileService {
} }
static Future<void> clearUserData({required String userId}) async { static Future<void> clearUserData({required String userId}) async {
logger.t("FileService#clearUserData(): Clearing data for user $userId...");
final scanDir = await temporaryScansDirectory; final scanDir = await temporaryScansDirectory;
final scanDirSize = formatBytes(await getDirSizeInBytes(scanDir));
final tempDir = await temporaryDirectory; final tempDir = await temporaryDirectory;
final tempDirSize = formatBytes(await getDirSizeInBytes(tempDir));
final consumptionDir = await getConsumptionDirectory(userId: userId); final consumptionDir = await getConsumptionDirectory(userId: userId);
final consumptionDirSize =
formatBytes(await getDirSizeInBytes(consumptionDir));
logger.t("FileService#clearUserData(): Removing scans...");
await scanDir.delete(recursive: true); 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); 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); await consumptionDir.delete(recursive: true);
logger.t("FileService#clearUserData(): Removed $consumptionDirSize...");
} }
static Future<void> clearDirectoryContent(PaperlessDirectoryType type) async { static Future<void> clearDirectoryContent(PaperlessDirectoryType type) async {
@@ -120,6 +155,12 @@ class FileService {
static Future<List<Directory>> getAllSubdirectories(Directory directory) { static Future<List<Directory>> getAllSubdirectories(Directory directory) {
return directory.list().whereType<Directory>().toList(); return directory.list().whereType<Directory>().toList();
} }
static Future<int> getDirSizeInBytes(Directory dir) async {
return dir
.list(recursive: true)
.fold(0, (previous, element) => previous + element.statSync().size);
}
} }
enum PaperlessDirectoryType { enum PaperlessDirectoryType {

View File

@@ -6,6 +6,7 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:paperless_mobile/constants.dart'; import 'package:paperless_mobile/constants.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.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/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/hint_card.dart';
import 'package:paperless_mobile/core/widgets/paperless_logo.dart'; import 'package:paperless_mobile/core/widgets/paperless_logo.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.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); .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( ListTile(
dense: true, dense: true,
leading: const Icon(Icons.settings_outlined), leading: const Icon(Icons.settings_outlined),

View File

@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:open_filex/open_filex.dart'; import 'package:open_filex/open_filex.dart';
import 'package:paperless_api/paperless_api.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/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/core/service/file_service.dart';
@@ -169,7 +170,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
locale: locale, locale: locale,
userId: userId, userId: userId,
); );
debugPrint("Downloaded file to $targetPath"); logger.i("Document '${state.document.title}' saved to $targetPath.");
} }
Future<void> shareDocument({bool shareOriginal = false}) async { Future<void> shareDocument({bool shareOriginal = false}) async {

View File

@@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.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/model/info_message_exception.dart';
import 'package:paperless_mobile/core/service/file_service.dart'; import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart'; import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
@@ -18,13 +19,13 @@ class DocumentScannerCubit extends Cubit<DocumentScannerState> {
: super(const InitialDocumentScannerState()); : super(const InitialDocumentScannerState());
Future<void> initialize() async { Future<void> initialize() async {
debugPrint("Restoring scans..."); logger.t("Restoring scans...");
emit(const RestoringDocumentScannerState()); emit(const RestoringDocumentScannerState());
final tempDir = await FileService.temporaryScansDirectory; final tempDir = await FileService.temporaryScansDirectory;
final allFiles = tempDir.list().whereType<File>(); final allFiles = tempDir.list().whereType<File>();
final scans = final scans =
await allFiles.where((event) => event.path.endsWith(".jpeg")).toList(); await allFiles.where((event) => event.path.endsWith(".jpeg")).toList();
debugPrint("Restored ${scans.length} scans."); logger.t("Restored ${scans.length} scans.");
emit( emit(
scans.isEmpty scans.isEmpty
? const InitialDocumentScannerState() ? const InitialDocumentScannerState()

View File

@@ -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/config/hive/hive_config.dart';
import 'package:paperless_mobile/core/database/tables/global_settings.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/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/repository/label_repository.dart';
import 'package:paperless_mobile/core/widgets/future_or_builder.dart'; import 'package:paperless_mobile/core/widgets/future_or_builder.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart';
@@ -376,10 +377,17 @@ class _DocumentUploadPreparationPageState
showErrorMessage(context, error, stackTrace); showErrorMessage(context, error, stackTrace);
} on PaperlessFormValidationException catch (exception) { } on PaperlessFormValidationException catch (exception) {
setState(() => _errors = exception.validationMessages); setState(() => _errors = exception.validationMessages);
} catch (unknownError, stackTrace) { } catch (error, stackTrace) {
debugPrint(unknownError.toString()); logger.e(
"An unknown error occurred during document upload.",
error: error,
stackTrace: stackTrace,
);
showErrorMessage( showErrorMessage(
context, const PaperlessApiException.unknown(), stackTrace); context,
const PaperlessApiException.unknown(),
stackTrace,
);
} finally { } finally {
setState(() { setState(() {
_isUploadLoading = false; _isUploadLoading = false;

View File

@@ -74,7 +74,6 @@ class DocumentsCubit extends Cubit<DocumentsState>
} }
Future<void> bulkDelete(List<DocumentModel> documents) async { Future<void> bulkDelete(List<DocumentModel> documents) async {
debugPrint("[DocumentsCubit] bulkRemove");
await api.bulkAction( await api.bulkAction(
BulkDeleteAction(documents.map((doc) => doc.id)), BulkDeleteAction(documents.map((doc) => doc.id)),
); );
@@ -85,7 +84,6 @@ class DocumentsCubit extends Cubit<DocumentsState>
} }
void toggleDocumentSelection(DocumentModel model) { void toggleDocumentSelection(DocumentModel model) {
debugPrint("[DocumentsCubit] toggleSelection");
if (state.selectedIds.contains(model.id)) { if (state.selectedIds.contains(model.id)) {
emit( emit(
state.copyWith( state.copyWith(
@@ -100,12 +98,10 @@ class DocumentsCubit extends Cubit<DocumentsState>
} }
void resetSelection() { void resetSelection() {
debugPrint("[DocumentsCubit] resetSelection");
emit(state.copyWith(selection: [])); emit(state.copyWith(selection: []));
} }
void reset() { void reset() {
debugPrint("[DocumentsCubit] reset");
emit(const DocumentsState()); emit(const DocumentsState());
} }

View File

@@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart'; import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.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/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/repository/label_repository.dart'; import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/label_repository_state.dart'; import 'package:paperless_mobile/core/repository/label_repository_state.dart';
@@ -83,11 +84,17 @@ class InboxCubit extends HydratedCubit<InboxState>
} }
Future<void> refreshItemsInInboxCount([bool shouldLoadInbox = true]) async { Future<void> 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(); final stats = await _statsApi.getServerStatistics();
if (stats.documentsInInbox != state.itemsInInboxCount && shouldLoadInbox) { if (stats.documentsInInbox != state.itemsInInboxCount && shouldLoadInbox) {
logger.t(
"InboxCubit#refreshItemsInInboxCount(): New documents found in inbox, reloading inbox.");
await loadInbox(); await loadInbox();
} else {
logger.t(
"InboxCubit#refreshItemsInInboxCount(): No new documents found in inbox.");
} }
emit(state.copyWith(itemsInInboxCount: stats.documentsInInbox)); emit(state.copyWith(itemsInInboxCount: stats.documentsInInbox));
} }
@@ -97,7 +104,6 @@ class InboxCubit extends HydratedCubit<InboxState>
/// ///
Future<void> loadInbox() async { Future<void> loadInbox() async {
if (!isClosed) { if (!isClosed) {
debugPrint("Initializing inbox...");
final inboxTags = await _labelRepository.findAllTags().then( final inboxTags = await _labelRepository.findAllTags().then(
(tags) => tags.where((t) => t.isInboxTag).map((t) => t.id!), (tags) => tags.where((t) => t.isInboxTag).map((t) => t.id!),
); );

View File

@@ -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/global_settings.dart';
import 'package:paperless_mobile/core/database/tables/local_user_account.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/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/core/widgets/material/colored_tab_bar.dart';
import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart'; import 'package:paperless_mobile/features/app_drawer/view/app_drawer.dart';
import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart'; import 'package:paperless_mobile/features/document_search/view/sliver_search_bar.dart';
@@ -212,17 +213,16 @@ class _LabelsPageState extends State<LabelsPage>
][_currentIndex] ][_currentIndex]
.call(); .call();
} catch (error, stackTrace) { } catch (error, stackTrace) {
debugPrint( logger.e(
"[LabelsPage] RefreshIndicator.onRefresh " "An error ocurred while reloading "
"${[ "${[
"correspondents", "correspondents",
"document types", "document types",
"tags", "tags",
"storage paths" "storage paths"
][_currentIndex]}: " ][_currentIndex]}: ${error.toString()}",
"An error occurred (${error.toString()})", stackTrace: stackTrace,
); );
debugPrintStack(stackTrace: stackTrace);
} }
}, },
child: TabBarView( child: TabBarView(

View File

@@ -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:paperless_mobile/routes/typed/top_level/changelog_route.dart';
import 'package:shared_preferences/shared_preferences.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 { class LandingPage extends StatefulWidget {
const LandingPage({super.key}); const LandingPage({super.key});

View File

@@ -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/database/tables/user_credentials.dart';
import 'package:paperless_mobile/core/factory/paperless_api_factory.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/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/model/info_message_exception.dart';
import 'package:paperless_mobile/core/security/session_manager.dart'; import 'package:paperless_mobile/core/security/session_manager.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.dart'; import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
@@ -55,10 +56,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
} }
emit(const AuthenticatingState(AuthenticatingStage.authenticating)); emit(const AuthenticatingState(AuthenticatingStage.authenticating));
final localUserId = "${credentials.username}@$serverUrl"; final localUserId = "${credentials.username}@$serverUrl";
_debugPrintMessage( logger.t("AuthenticationCubit#login(): Trying to log in $localUserId...");
"login",
"Trying to login $localUserId...",
);
try { try {
await _addUser( await _addUser(
localUserId, localUserId,
@@ -97,35 +95,26 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
await globalSettings.save(); await globalSettings.save();
emit(AuthenticatedState(localUserId: localUserId)); emit(AuthenticatedState(localUserId: localUserId));
_debugPrintMessage( logger.t(
"login", 'AuthenticationCubit#login(): User $localUserId successfully logged in.');
"User successfully logged in.",
);
} }
/// Switches to another account if it exists. /// Switches to another account if it exists.
Future<void> switchAccount(String localUserId) async { Future<void> switchAccount(String localUserId) async {
emit(const SwitchingAccountsState()); emit(const SwitchingAccountsState());
_debugPrintMessage( logger.t(
"switchAccount", 'AuthenticationCubit#switchAccount(): Trying to switch to user $localUserId...');
"Trying to switch to user $localUserId...",
);
final globalSettings = final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!; Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
// if (globalSettings.loggedInUserId == localUserId) {
// _debugPrintMessage(
// "switchAccount",
// "User $localUserId is already logged in.",
// );
// emit(AuthenticatedState(localUserId: localUserId));
// return;
// }
final userAccountBox = Hive.localUserAccountBox; final userAccountBox = Hive.localUserAccountBox;
if (!userAccountBox.containsKey(localUserId)) { 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; return;
} }
@@ -135,10 +124,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
final authenticated = await _localAuthService final authenticated = await _localAuthService
.authenticateLocalUser("Authenticate to switch your account."); .authenticateLocalUser("Authenticate to switch your account.");
if (!authenticated) { if (!authenticated) {
_debugPrintMessage( logger.w(
"switchAccount", "AuthenticationCubit#switchAccount(): User could not be authenticated.");
"User could not be authenticated.",
);
emit(VerifyIdentityState(userId: localUserId)); emit(VerifyIdentityState(userId: localUserId));
return; return;
} }
@@ -151,7 +138,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
HiveBoxes.localUserCredentials, (credentialsBox) async { HiveBoxes.localUserCredentials, (credentialsBox) async {
if (!credentialsBox.containsKey(localUserId)) { if (!credentialsBox.containsKey(localUserId)) {
await credentialsBox.close(); await credentialsBox.close();
debugPrint("Invalid authentication for $localUserId"); logger.w(
"AuthenticationCubit#switchAccount(): Invalid authentication for $localUserId.");
return; return;
} }
final credentials = credentialsBox.get(localUserId); final credentials = credentialsBox.get(localUserId);
@@ -188,6 +176,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
}) async { }) async {
assert(credentials.password != null && credentials.username != null); assert(credentials.password != null && credentials.username != null);
final localUserId = "${credentials.username}@$serverUrl"; final localUserId = "${credentials.username}@$serverUrl";
logger
.d("AuthenticationCubit#addAccount(): Adding account $localUserId...");
final sessionManager = SessionManager([ final sessionManager = SessionManager([
LanguageHeaderInterceptor(locale), LanguageHeaderInterceptor(locale),
@@ -204,8 +194,11 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
} }
Future<void> removeAccount(String userId) async { Future<void> removeAccount(String userId) async {
logger
.t("AuthenticationCubit#removeAccount(): Removing account $userId...");
final userAccountBox = Hive.localUserAccountBox; final userAccountBox = Hive.localUserAccountBox;
final userAppStateBox = Hive.localUserAppStateBox; final userAppStateBox = Hive.localUserAppStateBox;
await FileService.clearUserData(userId: userId); await FileService.clearUserData(userId: userId);
await userAccountBox.delete(userId); await userAccountBox.delete(userId);
await userAppStateBox.delete(userId); await userAppStateBox.delete(userId);
@@ -220,19 +213,15 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
/// ///
Future<void> restoreSession([String? userId]) async { Future<void> restoreSession([String? userId]) async {
emit(const RestoringSessionState()); emit(const RestoringSessionState());
_debugPrintMessage( logger.t(
"restoreSessionState", "AuthenticationCubit#restoreSessionState(): Trying to restore previous session...");
"Trying to restore previous session...",
);
final globalSettings = final globalSettings =
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!; Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
final restoreSessionForUser = userId ?? globalSettings.loggedInUserId; final restoreSessionForUser = userId ?? globalSettings.loggedInUserId;
// final localUserId = globalSettings.loggedInUserId; // final localUserId = globalSettings.loggedInUserId;
if (restoreSessionForUser == null) { if (restoreSessionForUser == null) {
_debugPrintMessage( logger.t(
"restoreSessionState", "AuthenticationCubit#restoreSessionState(): There is nothing to restore.");
"There is nothing to restore.",
);
final otherAccountsExist = Hive.localUserAccountBox.isNotEmpty; final otherAccountsExist = Hive.localUserAccountBox.isNotEmpty;
// If there is nothing to restore, we can quit here. // If there is nothing to restore, we can quit here.
emit( emit(
@@ -243,42 +232,25 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
final localUserAccountBox = final localUserAccountBox =
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount); Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
final localUserAccount = localUserAccountBox.get(restoreSessionForUser)!; final localUserAccount = localUserAccountBox.get(restoreSessionForUser)!;
_debugPrintMessage(
"restoreSessionState",
"Checking if biometric authentication is required...",
);
if (localUserAccount.settings.isBiometricAuthenticationEnabled) { if (localUserAccount.settings.isBiometricAuthenticationEnabled) {
_debugPrintMessage( logger.t(
"restoreSessionState", "AuthenticationCubit#restoreSessionState(): Verifying user identity...");
"Biometric authentication required, waiting for user to authenticate...",
);
final authenticationMesage = final authenticationMesage =
(await S.delegate.load(Locale(globalSettings.preferredLocaleSubtag))) (await S.delegate.load(Locale(globalSettings.preferredLocaleSubtag)))
.verifyYourIdentity; .verifyYourIdentity;
final localAuthSuccess = final localAuthSuccess =
await _localAuthService.authenticateLocalUser(authenticationMesage); await _localAuthService.authenticateLocalUser(authenticationMesage);
if (!localAuthSuccess) { if (!localAuthSuccess) {
logger.w(
"AuthenticationCubit#restoreSessionState(): Identity could not be verified.");
emit(VerifyIdentityState(userId: restoreSessionForUser)); emit(VerifyIdentityState(userId: restoreSessionForUser));
_debugPrintMessage(
"restoreSessionState",
"User could not be authenticated.",
);
return; return;
} }
_debugPrintMessage( logger.t(
"restoreSessionState", "AuthenticationCubit#restoreSessionState(): Identity successfully verified.");
"User successfully autheticated.",
);
} else {
_debugPrintMessage(
"restoreSessionState",
"Biometric authentication not configured, skipping.",
);
} }
_debugPrintMessage( logger.t(
"restoreSessionState", "AuthenticationCubit#restoreSessionState(): Reading encrypted credentials...");
"Trying to retrieve authentication credentials...",
);
final authentication = final authentication =
await withEncryptedBox<UserCredentials, UserCredentials>( await withEncryptedBox<UserCredentials, UserCredentials>(
HiveBoxes.localUserCredentials, (box) { HiveBoxes.localUserCredentials, (box) {
@@ -286,80 +258,62 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
}); });
if (authentication == null) { if (authentication == null) {
_debugPrintMessage( logger.e(
"restoreSessionState", "AuthenticationCubit#restoreSessionState(): Credentials could not be read!");
"Could not retrieve existing authentication credentials.",
);
throw Exception( throw Exception(
"User should be authenticated but no authentication information was found.", "User should be authenticated but no authentication information was found.",
); );
} }
logger.t(
"AuthenticationCubit#restoreSessionState(): Credentials successfully retrieved.");
_debugPrintMessage( logger.t(
"restoreSessionState", "AuthenticationCubit#restoreSessionState(): Updating security context...");
"Authentication credentials successfully retrieved.",
);
_debugPrintMessage(
"restoreSessionState",
"Updating current session state...",
);
_sessionManager.updateSettings( _sessionManager.updateSettings(
clientCertificate: authentication.clientCertificate, clientCertificate: authentication.clientCertificate,
authToken: authentication.token, authToken: authentication.token,
baseUrl: localUserAccount.serverUrl, baseUrl: localUserAccount.serverUrl,
); );
_debugPrintMessage( logger.t(
"restoreSessionState", "AuthenticationCubit#restoreSessionState(): Security context successfully updated.");
"Current session state successfully updated.",
);
final isPaperlessServerReachable = final isPaperlessServerReachable =
await _connectivityService.isPaperlessServerReachable( await _connectivityService.isPaperlessServerReachable(
localUserAccount.serverUrl, localUserAccount.serverUrl,
authentication.clientCertificate, authentication.clientCertificate,
) == ) ==
ReachabilityStatus.reachable; ReachabilityStatus.reachable;
logger.t(
"AuthenticationCubit#restoreSessionState(): Trying to update remote paperless user...");
if (isPaperlessServerReachable) { if (isPaperlessServerReachable) {
_debugPrintMessage(
"restoreSessionMState",
"Updating server user...",
);
final apiVersion = await _getApiVersion(_sessionManager.client); final apiVersion = await _getApiVersion(_sessionManager.client);
await _updateRemoteUser( await _updateRemoteUser(
_sessionManager, _sessionManager,
localUserAccount, localUserAccount,
apiVersion, apiVersion,
); );
_debugPrintMessage( logger.t(
"restoreSessionMState", "AuthenticationCubit#restoreSessionState(): Successfully updated remote paperless user.");
"Successfully updated server user.",
);
} else { } else {
_debugPrintMessage( logger.w(
"restoreSessionMState", "AuthenticationCubit#restoreSessionState(): Could not update remote paperless user. Server could not be reached. The app might behave unexpected!");
"Skipping update of server user (server could not be reached).",
);
} }
globalSettings.loggedInUserId = restoreSessionForUser; globalSettings.loggedInUserId = restoreSessionForUser;
await globalSettings.save(); await globalSettings.save();
emit(AuthenticatedState(localUserId: restoreSessionForUser)); emit(AuthenticatedState(localUserId: restoreSessionForUser));
_debugPrintMessage( logger.t(
"restoreSessionState", "AuthenticationCubit#restoreSessionState(): Previous session successfully restored.");
"Session was successfully restored.",
);
} }
Future<void> logout([bool removeAccount = false]) async { Future<void> logout([bool removeAccount = false]) async {
emit(const LoggingOutState()); emit(const LoggingOutState());
_debugPrintMessage(
"logout",
"Trying to log out current user...",
);
await _resetExternalState();
final globalSettings = Hive.globalSettingsBox.getValue()!; final globalSettings = Hive.globalSettingsBox.getValue()!;
final userId = globalSettings.loggedInUserId!; final userId = globalSettings.loggedInUserId!;
logger.t(
"AuthenticationCubit#logout(): Logging out current user ($userId)...");
await _resetExternalState();
await _notificationService.cancelUserNotifications(userId); await _notificationService.cancelUserNotifications(userId);
final otherAccountsExist = Hive.localUserAccountBox.length > 1; final otherAccountsExist = Hive.localUserAccountBox.length > 1;
@@ -370,15 +324,19 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
globalSettings.loggedInUserId = null; globalSettings.loggedInUserId = null;
await globalSettings.save(); await globalSettings.save();
_debugPrintMessage( logger.t("AuthenticationCubit#logout(): User successfully logged out.");
"logout",
"User successfully logged out.",
);
} }
Future<void> _resetExternalState() async { Future<void> _resetExternalState() async {
logger.t(
"AuthenticationCubit#_resetExternalState(): Resetting security context...");
_sessionManager.resetSettings(); _sessionManager.resetSettings();
logger.t(
"AuthenticationCubit#_resetExternalState(): Security context reset.");
logger.t(
"AuthenticationCubit#_resetExternalState(): Clearing local state...");
await HydratedBloc.storage.clear(); await HydratedBloc.storage.clear();
logger.t("AuthenticationCubit#_resetExternalState(): Local state cleard.");
} }
Future<int> _addUser( Future<int> _addUser(
@@ -392,7 +350,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
_FutureVoidCallback? onFetchUserInformation, _FutureVoidCallback? onFetchUserInformation,
}) async { }) async {
assert(credentials.username != null && credentials.password != null); assert(credentials.username != null && credentials.password != null);
_debugPrintMessage("_addUser", "Adding new user $localUserId..."); logger
.t("AuthenticationCubit#_addUser(): Adding new user $localUserId....");
sessionManager.updateSettings( sessionManager.updateSettings(
baseUrl: serverUrl, baseUrl: serverUrl,
@@ -401,10 +360,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
final authApi = _apiFactory.createAuthenticationApi(sessionManager.client); final authApi = _apiFactory.createAuthenticationApi(sessionManager.client);
_debugPrintMessage( logger.t(
"_addUser", "AuthenticationCubit#_addUser(): Fetching bearer token from the server...");
"Trying to login user ${credentials.username} on $serverUrl...",
);
await onPerformLogin?.call(); await onPerformLogin?.call();
@@ -413,10 +370,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
password: credentials.password!, password: credentials.password!,
); );
_debugPrintMessage( logger.t(
"_addUser", "AuthenticationCubit#_addUser(): Bearer token successfully retrieved.");
"Successfully acquired token.",
);
sessionManager.updateSettings( sessionManager.updateSettings(
baseUrl: serverUrl, baseUrl: serverUrl,
@@ -430,18 +385,15 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState); Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState);
if (userAccountBox.containsKey(localUserId)) { if (userAccountBox.containsKey(localUserId)) {
_debugPrintMessage( logger.w(
"_addUser", "AuthenticationCubit#_addUser(): The user $localUserId already exists.");
"An error occurred! The user $localUserId already exists.",
);
throw InfoMessageException(code: ErrorCode.userAlreadyExists); throw InfoMessageException(code: ErrorCode.userAlreadyExists);
} }
await onFetchUserInformation?.call(); await onFetchUserInformation?.call();
final apiVersion = await _getApiVersion(sessionManager.client); final apiVersion = await _getApiVersion(sessionManager.client);
_debugPrintMessage( logger.t(
"_addUser", "AuthenticationCubit#_addUser(): Trying to fetch remote paperless user for $localUserId.");
"Trying to fetch user object for $localUserId...",
);
late UserModel serverUser; late UserModel serverUser;
try { try {
serverUser = await _apiFactory serverUser = await _apiFactory
@@ -451,21 +403,20 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
) )
.findCurrentUser(); .findCurrentUser();
} on DioException catch (error, stackTrace) { } on DioException catch (error, stackTrace) {
_debugPrintMessage( logger.e(
"_addUser", "AuthenticationCubit#_addUser(): An error occurred while fetching the remote paperless user.",
"An error occurred: ${error.message}", error: error,
stackTrace: stackTrace, stackTrace: stackTrace,
); );
rethrow; rethrow;
} }
_debugPrintMessage( logger.t(
"_addUser", "AuthenticationCubit#_addUser(): Remote paperless user successfully fetched.");
"User object successfully fetched.",
); logger.t(
_debugPrintMessage( "AuthenticationCubit#_addUser(): Persisting user account information...");
"_addUser",
"Persisting local user account...",
);
await onPersistLocalUserData?.call(); await onPersistLocalUserData?.call();
// Create user account // Create user account
await userAccountBox.put( await userAccountBox.put(
@@ -478,29 +429,21 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
apiVersion: apiVersion, apiVersion: apiVersion,
), ),
); );
_debugPrintMessage( logger.t(
"_addUser", "AuthenticationCubit#_addUser(): User account information successfully persisted.");
"Local user account successfully persisted.", logger.t("AuthenticationCubit#_addUser(): Persisting user app state...");
);
_debugPrintMessage(
"_addUser",
"Persisting user state...",
);
// Create user state // Create user state
await userStateBox.put( await userStateBox.put(
localUserId, localUserId,
LocalUserAppState(userId: localUserId), LocalUserAppState(userId: localUserId),
); );
_debugPrintMessage( logger.t(
"_addUser", "AuthenticationCubit#_addUser(): User state successfully persisted.");
"User state successfully persisted.",
);
// Save credentials in encrypted box // Save credentials in encrypted box
await withEncryptedBox(HiveBoxes.localUserCredentials, (box) async { await withEncryptedBox(HiveBoxes.localUserCredentials, (box) async {
_debugPrintMessage( logger.t(
"_addUser", "AuthenticationCubit#_addUser(): Saving user credentials inside encrypted storage...");
"Saving user credentials inside encrypted storage...",
);
await box.put( await box.put(
localUserId, localUserId,
UserCredentials( UserCredentials(
@@ -508,10 +451,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
clientCertificate: clientCert, clientCertificate: clientCert,
), ),
); );
_debugPrintMessage( logger.t(
"_addUser", "AuthenticationCubit#_addUser(): User credentials successfully saved.");
"User credentials successfully saved.",
);
}); });
final hostsBox = Hive.box<String>(HiveBoxes.hosts); final hostsBox = Hive.box<String>(HiveBoxes.hosts);
if (!hostsBox.values.contains(serverUrl)) { if (!hostsBox.values.contains(serverUrl)) {
@@ -526,10 +467,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
Duration? timeout, Duration? timeout,
int defaultValue = 2, int defaultValue = 2,
}) async { }) async {
_debugPrintMessage( logger.t(
"_getApiVersion", "AuthenticationCubit#_getApiVersion(): Trying to fetch API version...");
"Trying to fetch API version...",
);
try { try {
final response = await dio.get( final response = await dio.get(
"/api/", "/api/",
@@ -539,12 +478,13 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
); );
final apiVersion = final apiVersion =
int.parse(response.headers.value('x-api-version') ?? "3"); int.parse(response.headers.value('x-api-version') ?? "3");
_debugPrintMessage( logger.t(
"_getApiVersion", "AuthenticationCubit#_getApiVersion(): Successfully retrieved API version ($apiVersion).");
"API version ($apiVersion) successfully retrieved.",
);
return apiVersion; return apiVersion;
} on DioException catch (_) { } on DioException catch (_) {
logger.w(
"AuthenticationCubit#_getApiVersion(): Could not retrieve API version.");
return defaultValue; return defaultValue;
} }
} }
@@ -555,10 +495,8 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
LocalUserAccount localUserAccount, LocalUserAccount localUserAccount,
int apiVersion, int apiVersion,
) async { ) async {
_debugPrintMessage( logger.t(
"_updateRemoteUser", "AuthenticationCubit#_updateRemoteUser(): Trying to update remote user object...");
"Updating paperless user object...",
);
final updatedPaperlessUser = await _apiFactory final updatedPaperlessUser = await _apiFactory
.createUserApi( .createUserApi(
sessionManager.client, sessionManager.client,
@@ -568,24 +506,7 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
localUserAccount.paperlessUser = updatedPaperlessUser; localUserAccount.paperlessUser = updatedPaperlessUser;
await localUserAccount.save(); await localUserAccount.save();
_debugPrintMessage( logger.t(
"_updateRemoteUser", "AuthenticationCubit#_updateRemoteUser(): Successfully updated remote user object.");
"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);
}
} }
} }

View File

@@ -7,7 +7,7 @@ String formatMaxCount(int? count, [int maxCount = 99]) {
return (count ?? 0).toString(); return (count ?? 0).toString();
} }
String formatBytes(int bytes, int decimals) { String formatBytes(int bytes, [int decimals = 2]) {
if (bytes <= 0) return "0 B"; if (bytes <= 0) return "0 B";
const suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; const suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
var i = (log(bytes) / log(1024)).floor(); var i = (log(bytes) / log(1024)).floor();

View File

@@ -16,6 +16,7 @@ import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl_standalone.dart'; import 'package:intl/intl_standalone.dart';
import 'package:local_auth/local_auth.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:package_info_plus/package_info_plus.dart';
import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/constants.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.dart';
import 'package:paperless_mobile/core/factory/paperless_api_factory_impl.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/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/notifier/document_changed_notifier.dart';
import 'package:paperless_mobile/core/security/session_manager.dart'; import 'package:paperless_mobile/core/security/session_manager.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.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/routes/typed/top_level/login_route.dart';
import 'package:paperless_mobile/theme.dart'; import 'package:paperless_mobile/theme.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@@ -82,7 +85,7 @@ Future<void> performMigrations() async {
final requiresMigrationForCurrentVersion = final requiresMigrationForCurrentVersion =
!performedMigrations.contains(currentVersion); !performedMigrations.contains(currentVersion);
if (requiresMigrationForCurrentVersion) { if (requiresMigrationForCurrentVersion) {
debugPrint("Applying migration scripts for version $currentVersion"); logger.t("Applying migration scripts for version $currentVersion");
await migrationProcedure(); await migrationProcedure();
await sp.setStringList( await sp.setStringList(
'performed_migrations', 'performed_migrations',
@@ -91,7 +94,6 @@ Future<void> performMigrations() async {
} }
} }
Future<void> _initHive() async { Future<void> _initHive() async {
await Hive.initFlutter(); await Hive.initFlutter();
@@ -125,6 +127,13 @@ void main() async {
// ) // )
// .start(); // .start();
// } // }
logger = l.Logger(
output: MirroredFileOutput(),
printer: SpringBootLikePrinter(),
level: l.Level.trace,
);
packageInfo = await PackageInfo.fromPlatform(); packageInfo = await PackageInfo.fromPlatform();
if (Platform.isAndroid) { if (Platform.isAndroid) {
@@ -160,6 +169,15 @@ void main() async {
// Manages security context, required for self signed client certificates // Manages security context, required for self signed client certificates
final sessionManager = SessionManager([ final sessionManager = SessionManager([
languageHeaderInterceptor, languageHeaderInterceptor,
PrettyDioLogger(
compact: true,
responseBody: false,
responseHeader: false,
request: false,
requestBody: false,
requestHeader: false,
logPrint: (object) => logger.t,
),
]); ]);
// Initialize Blocs/Cubits // Initialize Blocs/Cubits
@@ -214,9 +232,7 @@ void main() async {
ServerMessageException e => e.message, ServerMessageException e => e.message,
_ => error.toString() _ => error.toString()
}; };
debugPrint("An unepxected exception has occured!"); logger.e(message, stackTrace: stack);
debugPrint(message);
debugPrintStack(stackTrace: stack);
}); });
} }
@@ -254,7 +270,7 @@ class _GoRouterShellState extends State<GoRouterShell> {
final DisplayMode mostOptimalMode = final DisplayMode mostOptimalMode =
sameResolution.isNotEmpty ? sameResolution.first : active; sameResolution.isNotEmpty ? sameResolution.first : active;
debugPrint('Setting refresh rate to ${mostOptimalMode.refreshRate}'); logger.d('Setting refresh rate to ${mostOptimalMode.refreshRate}');
await FlutterDisplayMode.setPreferredMode(mostOptimalMode); await FlutterDisplayMode.setPreferredMode(mostOptimalMode);
} }

View File

@@ -997,6 +997,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.10" 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: logging:
dependency: transitive dependency: transitive
description: description:

View File

@@ -98,6 +98,7 @@ dependencies:
flutter_animate: ^4.2.0+1 flutter_animate: ^4.2.0+1
shared_preferences: ^2.2.1 shared_preferences: ^2.2.1
flutter_markdown: ^0.6.18 flutter_markdown: ^0.6.18
logger: ^2.0.2+1
# camerawesome: ^2.0.0-dev.1 # camerawesome: ^2.0.0-dev.1
dependency_overrides: dependency_overrides: