mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-08 10:07:51 -06:00
feat: finished new logging feature
This commit is contained in:
111
lib/core/logging/cubit/app_logs_cubit.dart
Normal file
111
lib/core/logging/cubit/app_logs_cubit.dart
Normal file
@@ -0,0 +1,111 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:paperless_mobile/core/logging/models/parsed_log_message.dart';
|
||||
import 'package:paperless_mobile/core/service/file_service.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
part 'app_logs_state.dart';
|
||||
|
||||
final _fileNameFormat = DateFormat("yyyy-MM-dd");
|
||||
|
||||
class AppLogsCubit extends Cubit<AppLogsState> {
|
||||
StreamSubscription? _fileChangesSubscription;
|
||||
AppLogsCubit(DateTime date) : super(AppLogsStateInitial(date: date));
|
||||
|
||||
Future<void> loadLogs(DateTime date) async {
|
||||
if (date == state.date) {
|
||||
return;
|
||||
}
|
||||
_fileChangesSubscription?.cancel();
|
||||
emit(AppLogsStateLoading(date: date));
|
||||
final logDir = FileService.instance.logDirectory;
|
||||
final availableLogs = (await logDir
|
||||
.list()
|
||||
.whereType<File>()
|
||||
.where((event) => event.path.endsWith('.log'))
|
||||
.map((e) =>
|
||||
_fileNameFormat.parse(p.basenameWithoutExtension(e.path)))
|
||||
.toList())
|
||||
.sorted();
|
||||
final logFile = _getLogfile(date);
|
||||
if (!await logFile.exists()) {
|
||||
emit(AppLogsStateLoaded(
|
||||
date: date,
|
||||
logs: [],
|
||||
availableLogs: availableLogs,
|
||||
));
|
||||
}
|
||||
try {
|
||||
final logs = await logFile.readAsLines();
|
||||
final parsedLogs =
|
||||
ParsedLogMessage.parse(logs.skip(2000).toList()).reversed.toList();
|
||||
_fileChangesSubscription = logFile.watch().listen((event) async {
|
||||
if (!isClosed) {
|
||||
final logs = await logFile.readAsLines();
|
||||
emit(AppLogsStateLoaded(
|
||||
date: date,
|
||||
logs: parsedLogs,
|
||||
availableLogs: availableLogs,
|
||||
));
|
||||
}
|
||||
});
|
||||
emit(AppLogsStateLoaded(
|
||||
date: date,
|
||||
logs: parsedLogs,
|
||||
availableLogs: availableLogs,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(AppLogsStateError(
|
||||
error: e,
|
||||
date: date,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clearLogs(DateTime date) async {
|
||||
final logFile = _getLogfile(date);
|
||||
await logFile.writeAsString('');
|
||||
await loadLogs(date);
|
||||
}
|
||||
|
||||
Future<void> copyToClipboard(DateTime date) async {
|
||||
final file = _getLogfile(date);
|
||||
if (!await file.exists()) {
|
||||
return;
|
||||
}
|
||||
final content = await file.readAsString();
|
||||
Clipboard.setData(ClipboardData(text: content));
|
||||
}
|
||||
|
||||
Future<void> saveLogs(DateTime date, String locale) async {
|
||||
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
|
||||
? FileService.instance.downloadsDirectory.path
|
||||
: null,
|
||||
);
|
||||
final logFile = _getLogfile(date);
|
||||
if (parentDir != null) {
|
||||
await logFile.copy(p.join(parentDir, filename));
|
||||
}
|
||||
}
|
||||
|
||||
File _getLogfile(DateTime date) {
|
||||
return File(p.join(FileService.instance.logDirectory.path,
|
||||
'${_fileNameFormat.format(date)}.log'));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_fileChangesSubscription?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
33
lib/core/logging/cubit/app_logs_state.dart
Normal file
33
lib/core/logging/cubit/app_logs_state.dart
Normal file
@@ -0,0 +1,33 @@
|
||||
part of 'app_logs_cubit.dart';
|
||||
|
||||
sealed class AppLogsState {
|
||||
final DateTime date;
|
||||
const AppLogsState({required this.date});
|
||||
}
|
||||
|
||||
class AppLogsStateInitial extends AppLogsState {
|
||||
const AppLogsStateInitial({required super.date});
|
||||
}
|
||||
|
||||
class AppLogsStateLoading extends AppLogsState {
|
||||
const AppLogsStateLoading({required super.date});
|
||||
}
|
||||
|
||||
class AppLogsStateLoaded extends AppLogsState {
|
||||
const AppLogsStateLoaded({
|
||||
required super.date,
|
||||
required this.logs,
|
||||
required this.availableLogs,
|
||||
});
|
||||
final List<DateTime> availableLogs;
|
||||
final List<ParsedLogMessage> logs;
|
||||
}
|
||||
|
||||
class AppLogsStateError extends AppLogsState {
|
||||
const AppLogsStateError({
|
||||
required this.error,
|
||||
required super.date,
|
||||
});
|
||||
|
||||
final Object error;
|
||||
}
|
||||
44
lib/core/logging/data/formatted_printer.dart
Normal file
44
lib/core/logging/data/formatted_printer.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:paperless_mobile/core/logging/models/formatted_log_message.dart';
|
||||
|
||||
class FormattedPrinter extends LogPrinter {
|
||||
static final _timestampFormat = DateFormat("yyyy-MM-dd HH:mm:ss.SSS");
|
||||
static const _mulitlineObjectEncoder = JsonEncoder.withIndent(null);
|
||||
|
||||
@override
|
||||
List<String> log(LogEvent event) {
|
||||
final unformattedMessage = event.message;
|
||||
final formattedMessage = switch (unformattedMessage) {
|
||||
FormattedLogMessage m => m.format(),
|
||||
Iterable i => _mulitlineObjectEncoder
|
||||
.convert(i)
|
||||
.padLeft(FormattedLogMessage.maxLength),
|
||||
Map m => _mulitlineObjectEncoder
|
||||
.convert(m)
|
||||
.padLeft(FormattedLogMessage.maxLength),
|
||||
_ => unformattedMessage.toString().padLeft(FormattedLogMessage.maxLength),
|
||||
};
|
||||
final formattedLevel = event.level.name
|
||||
.toUpperCase()
|
||||
.padRight(Level.values.map((e) => e.name.length).max);
|
||||
final formattedTimestamp = _timestampFormat.format(event.time);
|
||||
|
||||
return [
|
||||
'$formattedTimestamp\t$formattedLevel --- $formattedMessage',
|
||||
if (event.error != null) ...[
|
||||
"---BEGIN ERROR---",
|
||||
event.error.toString(),
|
||||
"---END ERROR---",
|
||||
],
|
||||
if (event.stackTrace != null) ...[
|
||||
"---BEGIN STACKTRACE---",
|
||||
event.stackTrace.toString(),
|
||||
"---END STACKTRACE---"
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
116
lib/core/logging/data/logger.dart
Normal file
116
lib/core/logging/data/logger.dart
Normal file
@@ -0,0 +1,116 @@
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:paperless_mobile/core/logging/models/formatted_log_message.dart';
|
||||
|
||||
late Logger logger;
|
||||
|
||||
extension FormattedLoggerExtension on Logger {
|
||||
void ft(
|
||||
dynamic message, {
|
||||
String className = '',
|
||||
String methodName = '',
|
||||
DateTime? time,
|
||||
Object? error,
|
||||
StackTrace? stackTrace,
|
||||
}) {
|
||||
final formattedMessage = FormattedLogMessage(
|
||||
message,
|
||||
className: className,
|
||||
methodName: methodName,
|
||||
);
|
||||
log(
|
||||
Level.trace,
|
||||
formattedMessage,
|
||||
time: time,
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
|
||||
void fw(
|
||||
dynamic message, {
|
||||
String className = '',
|
||||
String methodName = '',
|
||||
DateTime? time,
|
||||
Object? error,
|
||||
StackTrace? stackTrace,
|
||||
}) {
|
||||
final formattedMessage = FormattedLogMessage(
|
||||
message,
|
||||
className: className,
|
||||
methodName: methodName,
|
||||
);
|
||||
log(
|
||||
Level.warning,
|
||||
formattedMessage,
|
||||
time: time,
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
|
||||
void fd(
|
||||
dynamic message, {
|
||||
String className = '',
|
||||
String methodName = '',
|
||||
DateTime? time,
|
||||
Object? error,
|
||||
StackTrace? stackTrace,
|
||||
}) {
|
||||
final formattedMessage = FormattedLogMessage(
|
||||
message,
|
||||
className: className,
|
||||
methodName: methodName,
|
||||
);
|
||||
log(
|
||||
Level.debug,
|
||||
formattedMessage,
|
||||
time: time,
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
|
||||
void fi(
|
||||
dynamic message, {
|
||||
String className = '',
|
||||
String methodName = '',
|
||||
DateTime? time,
|
||||
Object? error,
|
||||
StackTrace? stackTrace,
|
||||
}) {
|
||||
final formattedMessage = FormattedLogMessage(
|
||||
message,
|
||||
className: className,
|
||||
methodName: methodName,
|
||||
);
|
||||
log(
|
||||
Level.info,
|
||||
formattedMessage,
|
||||
time: time,
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
|
||||
void fe(
|
||||
dynamic message, {
|
||||
String className = '',
|
||||
String methodName = '',
|
||||
DateTime? time,
|
||||
Object? error,
|
||||
StackTrace? stackTrace,
|
||||
}) {
|
||||
final formattedMessage = FormattedLogMessage(
|
||||
message,
|
||||
className: className,
|
||||
methodName: methodName,
|
||||
);
|
||||
log(
|
||||
Level.error,
|
||||
formattedMessage,
|
||||
time: time,
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
54
lib/core/logging/data/mirrored_file_output.dart
Normal file
54
lib/core/logging/data/mirrored_file_output.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.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:synchronized/synchronized.dart';
|
||||
|
||||
class MirroredFileOutput extends LogOutput {
|
||||
final Completer _initCompleter = Completer();
|
||||
var lock = Lock();
|
||||
MirroredFileOutput();
|
||||
|
||||
late final File file;
|
||||
|
||||
@override
|
||||
Future<void> init() async {
|
||||
final today = DateFormat("yyyy-MM-dd").format(DateTime.now());
|
||||
final logDir = FileService.instance.logDirectory;
|
||||
file = File(p.join(logDir.path, '$today.log'));
|
||||
debugPrint("Logging files to ${file.path}.");
|
||||
_initCompleter.complete();
|
||||
try {
|
||||
final oldLogs = await FileService.instance.getAllFiles(logDir);
|
||||
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 {
|
||||
await lock.synchronized(() async {
|
||||
for (var line in event.lines) {
|
||||
debugPrint(line);
|
||||
if (_initCompleter.isCompleted) {
|
||||
await file.writeAsString(
|
||||
"$line${Platform.lineTerminator}",
|
||||
mode: FileMode.append,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
19
lib/core/logging/models/formatted_log_message.dart
Normal file
19
lib/core/logging/models/formatted_log_message.dart
Normal file
@@ -0,0 +1,19 @@
|
||||
/// Class passed to the printer to be formatted and printed.
|
||||
class FormattedLogMessage {
|
||||
static const maxLength = 55;
|
||||
final String message;
|
||||
final String methodName;
|
||||
final String className;
|
||||
|
||||
FormattedLogMessage(
|
||||
this.message, {
|
||||
required this.methodName,
|
||||
required this.className,
|
||||
});
|
||||
|
||||
String format() {
|
||||
final formattedClassName = className.padLeft(25);
|
||||
final formattedMethodName = methodName.padRight(25);
|
||||
return '[$formattedClassName] - $formattedMethodName: $message';
|
||||
}
|
||||
}
|
||||
148
lib/core/logging/models/parsed_log_message.dart
Normal file
148
lib/core/logging/models/parsed_log_message.dart
Normal file
@@ -0,0 +1,148 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
final _newLine = Platform.lineTerminator;
|
||||
|
||||
sealed class ParsedLogMessage {
|
||||
static List<ParsedLogMessage> parse(List<String> logs) {
|
||||
List<ParsedLogMessage> messages = [];
|
||||
int offset = 0;
|
||||
while (offset < logs.length) {
|
||||
final currentLine = logs[offset];
|
||||
if (ParsedFormattedLogMessage.canConsumeFirstLine(currentLine)) {
|
||||
final (consumedLines, result) =
|
||||
ParsedFormattedLogMessage.consume(logs.sublist(offset));
|
||||
messages.add(result);
|
||||
offset += consumedLines;
|
||||
} else {
|
||||
messages.add(UnformattedLogMessage(currentLine));
|
||||
offset++;
|
||||
}
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
}
|
||||
|
||||
class ParsedErrorLogMessage {
|
||||
static final RegExp _errorBeginPattern = RegExp(r"---BEGIN ERROR---\s*");
|
||||
static final RegExp _errorEndPattern = RegExp(r"---END ERROR---\s*");
|
||||
static final RegExp _stackTraceBeginPattern =
|
||||
RegExp(r"---BEGIN STACKTRACE---\s*");
|
||||
static final RegExp _stackTraceEndPattern =
|
||||
RegExp(r"---END STACKTRACE---\s*");
|
||||
final String error;
|
||||
final String? stackTrace;
|
||||
ParsedErrorLogMessage({
|
||||
required this.error,
|
||||
this.stackTrace,
|
||||
});
|
||||
static bool canConsumeFirstLine(String line) =>
|
||||
_errorBeginPattern.hasMatch(line);
|
||||
|
||||
static (int consumedLines, ParsedErrorLogMessage result) consume(
|
||||
List<String> log) {
|
||||
assert(log.isNotEmpty && canConsumeFirstLine(log.first));
|
||||
String errorText = "";
|
||||
int currentLine =
|
||||
1; // Skip first because we know that the first line is ---BEGIN ERROR---
|
||||
while (!_errorEndPattern.hasMatch(log[currentLine])) {
|
||||
errorText += log[currentLine] + _newLine;
|
||||
currentLine++;
|
||||
}
|
||||
currentLine++;
|
||||
final hasStackTrace = _stackTraceBeginPattern.hasMatch(log[currentLine]);
|
||||
String? stackTrace;
|
||||
if (hasStackTrace) {
|
||||
currentLine++;
|
||||
String stackTraceText = '';
|
||||
|
||||
while (!_stackTraceEndPattern.hasMatch(log[currentLine])) {
|
||||
stackTraceText += log[currentLine] + _newLine;
|
||||
currentLine++;
|
||||
}
|
||||
stackTrace = stackTraceText;
|
||||
}
|
||||
return (
|
||||
currentLine + 1,
|
||||
ParsedErrorLogMessage(error: errorText, stackTrace: stackTrace)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class UnformattedLogMessage extends ParsedLogMessage {
|
||||
final String message;
|
||||
|
||||
UnformattedLogMessage(this.message);
|
||||
}
|
||||
|
||||
class ParsedFormattedLogMessage extends ParsedLogMessage {
|
||||
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;
|
||||
|
||||
final ParsedErrorLogMessage? error;
|
||||
|
||||
ParsedFormattedLogMessage({
|
||||
required this.level,
|
||||
required this.message,
|
||||
this.className,
|
||||
this.methodName,
|
||||
required this.timestamp,
|
||||
this.error,
|
||||
});
|
||||
|
||||
static bool canConsumeFirstLine(String line) => pattern.hasMatch(line);
|
||||
|
||||
static (int consumedLines, ParsedFormattedLogMessage result) consume(
|
||||
List<String> log) {
|
||||
assert(log.isNotEmpty && canConsumeFirstLine(log.first));
|
||||
|
||||
final match = pattern.firstMatch(log.first)!;
|
||||
final result = ParsedFormattedLogMessage(
|
||||
level: Level.values.byName(match.namedGroup('level')!.toLowerCase()),
|
||||
message: match.namedGroup('message')!,
|
||||
className: match.namedGroup('className'),
|
||||
methodName: match.namedGroup('methodName'),
|
||||
timestamp: DateTime.parse(match.namedGroup('timestamp')!),
|
||||
);
|
||||
final updatedLog = log.sublist(1);
|
||||
if (updatedLog.isEmpty) {
|
||||
return (1, result);
|
||||
}
|
||||
if (ParsedErrorLogMessage.canConsumeFirstLine(updatedLog.first)) {
|
||||
final (consumedLines, parsedError) =
|
||||
ParsedErrorLogMessage.consume(updatedLog);
|
||||
return (
|
||||
consumedLines + 1,
|
||||
result.copyWith(error: parsedError),
|
||||
);
|
||||
}
|
||||
return (1, result);
|
||||
}
|
||||
|
||||
ParsedFormattedLogMessage copyWith({
|
||||
Level? level,
|
||||
String? message,
|
||||
String? className,
|
||||
String? methodName,
|
||||
DateTime? timestamp,
|
||||
ParsedErrorLogMessage? error,
|
||||
}) {
|
||||
return ParsedFormattedLogMessage(
|
||||
level: level ?? this.level,
|
||||
message: message ?? this.message,
|
||||
className: className ?? this.className,
|
||||
methodName: methodName ?? this.methodName,
|
||||
timestamp: timestamp ?? this.timestamp,
|
||||
error: error ?? this.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
22
lib/core/logging/utils/redaction_utils.dart
Normal file
22
lib/core/logging/utils/redaction_utils.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
(String username, String obscuredUrl) splitRedactUserId(String userId) {
|
||||
final parts = userId.split('@');
|
||||
if (parts.length != 2) {
|
||||
return ('unknown', 'unknown');
|
||||
}
|
||||
|
||||
final username = parts.first;
|
||||
final serverUrl = parts.last;
|
||||
final uri = Uri.parse(serverUrl);
|
||||
final hostLen = uri.host.length;
|
||||
final obscuredUrl = uri.scheme +
|
||||
"://" +
|
||||
uri.host.substring(0, 2) +
|
||||
List.filled(hostLen - 4, '*').join() +
|
||||
uri.host.substring(uri.host.length - 2, uri.host.length);
|
||||
return (username, obscuredUrl);
|
||||
}
|
||||
|
||||
String redactUserId(String userId) {
|
||||
final (username, obscuredUrl) = splitRedactUserId(userId);
|
||||
return '$username@$obscuredUrl';
|
||||
}
|
||||
@@ -1,19 +1,13 @@
|
||||
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:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import 'package:paperless_mobile/core/service/file_service.dart';
|
||||
import 'package:paperless_mobile/core/logging/cubit/app_logs_cubit.dart';
|
||||
import 'package:paperless_mobile/core/logging/models/parsed_log_message.dart';
|
||||
import 'package:paperless_mobile/extensions/dart_extensions.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");
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
class AppLogsPage extends StatefulWidget {
|
||||
const AppLogsPage({super.key});
|
||||
@@ -23,304 +17,181 @@ class AppLogsPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
return BlocBuilder<AppLogsCubit, AppLogsState>(
|
||||
builder: (context, state) {
|
||||
final formattedDate = DateFormat.yMMMd(locale).format(state.date);
|
||||
return Scaffold(
|
||||
bottomNavigationBar: BottomAppBar(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: switch (state) {
|
||||
AppLogsStateInitial() => [],
|
||||
AppLogsStateLoading() => [],
|
||||
AppLogsStateLoaded() => [
|
||||
IconButton(
|
||||
tooltip: S.of(context)!.copyToClipboard,
|
||||
onPressed: () {
|
||||
context
|
||||
.read<AppLogsCubit>()
|
||||
.copyToClipboard(state.date);
|
||||
},
|
||||
icon: const Icon(Icons.copy),
|
||||
).padded(),
|
||||
IconButton(
|
||||
tooltip: S.of(context)!.saveLogsToFile,
|
||||
onPressed: () {
|
||||
context
|
||||
.read<AppLogsCubit>()
|
||||
.saveLogs(state.date, locale);
|
||||
},
|
||||
icon: const Icon(Icons.download),
|
||||
).padded(),
|
||||
IconButton(
|
||||
tooltip: S.of(context)!.clearLogs(formattedDate),
|
||||
onPressed: () {
|
||||
context.read<AppLogsCubit>().clearLogs(state.date);
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.delete_sweep,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
).padded(),
|
||||
],
|
||||
_ => [],
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: file != null
|
||||
? [
|
||||
),
|
||||
appBar: AppBar(
|
||||
title: Text(S.of(context)!.appLogs(formattedDate)),
|
||||
actions: [
|
||||
if (state is AppLogsStateLoaded)
|
||||
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),
|
||||
tooltip: MaterialLocalizations.of(context).datePickerHelpText,
|
||||
onPressed: () async {
|
||||
final selectedDate = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: state.date,
|
||||
firstDate: state.availableLogs.first,
|
||||
lastDate: state.availableLogs.last,
|
||||
selectableDayPredicate: (day) => state.availableLogs
|
||||
.any((date) => day.isOnSameDayAs(date)),
|
||||
initialEntryMode: DatePickerEntryMode.calendarOnly,
|
||||
);
|
||||
if (selectedDate != null) {
|
||||
context.read<AppLogsCubit>().loadLogs(selectedDate);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.calendar_today),
|
||||
).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,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
body: switch (state) {
|
||||
AppLogsStateLoaded(
|
||||
logs: var logs,
|
||||
) =>
|
||||
Builder(
|
||||
builder: (context) {
|
||||
if (state.logs.isEmpty) {
|
||||
return Center(
|
||||
child: Text(S.of(context)!.noLogsFoundOn(formattedDate)),
|
||||
);
|
||||
}
|
||||
return ListView.builder(
|
||||
reverse: true,
|
||||
controller: _scrollController,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == 0) {
|
||||
return Center(
|
||||
child: Text(S.of(context)!.logfileBottomReached,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.disabledColor,
|
||||
)),
|
||||
).padded(24);
|
||||
}
|
||||
final messages = state.logs;
|
||||
final logMessage = messages[index - 1];
|
||||
final altColor = CupertinoDynamicColor.withBrightness(
|
||||
color: Colors.grey.shade200,
|
||||
darkColor: Colors.grey.shade800,
|
||||
).resolveFrom(context);
|
||||
return ParsedLogMessageTile(
|
||||
message: logMessage,
|
||||
backgroundColor: (index % 2 == 0)
|
||||
? theme.colorScheme.background
|
||||
: altColor,
|
||||
);
|
||||
},
|
||||
itemCount: logs.length + 1,
|
||||
);
|
||||
},
|
||||
),
|
||||
AppLogsStateError() => Center(
|
||||
child: Text(
|
||||
S.of(context)!.couldNotLoadLogfileFrom(formattedDate),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
_ => _buildLoadingLogs(state.date)
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingLogs(DateTime date) {
|
||||
final formattedDate =
|
||||
DateFormat.yMd(Localizations.localeOf(context).toString()).format(date);
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const CircularProgressIndicator(),
|
||||
Text(S.of(context)!.loadingLogsFrom(formattedDate)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
class ParsedLogMessageTile extends StatelessWidget {
|
||||
final ParsedLogMessage message;
|
||||
final Color backgroundColor;
|
||||
const _LogMessageWidget({
|
||||
|
||||
const ParsedLogMessageTile({
|
||||
super.key,
|
||||
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,
|
||||
return switch (message) {
|
||||
ParsedFormattedLogMessage m => FormattedLogMessageWidget(
|
||||
message: m,
|
||||
backgroundColor: backgroundColor,
|
||||
),
|
||||
UnformattedLogMessage(message: var m) => Text(m),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class FormattedLogMessageWidget extends StatelessWidget {
|
||||
final ParsedFormattedLogMessage message;
|
||||
final Color backgroundColor;
|
||||
const FormattedLogMessageWidget(
|
||||
{super.key, required this.message, required this.backgroundColor});
|
||||
static final _timeFormat = DateFormat("HH:mm:ss.SSS");
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final c = Theme.of(context).colorScheme;
|
||||
|
||||
final icon = switch (message.level) {
|
||||
Level.trace => Icons.troubleshoot,
|
||||
Level.debug => Icons.bug_report,
|
||||
@@ -330,31 +201,83 @@ class _LogMessageWidget extends StatelessWidget {
|
||||
Level.fatal => Icons.error_outline,
|
||||
_ => null,
|
||||
};
|
||||
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,
|
||||
Level.info => Colors.blue,
|
||||
_ => c.onBackground,
|
||||
};
|
||||
|
||||
final logStyle = Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
);
|
||||
final formattedMethodName =
|
||||
message.methodName != null ? '${message.methodName!.trim()}()' : '';
|
||||
final source = switch (message.className) {
|
||||
'' || null => formattedMethodName,
|
||||
String className => '$className.$formattedMethodName',
|
||||
};
|
||||
return Material(
|
||||
child: ListTile(
|
||||
color: backgroundColor,
|
||||
child: ExpansionTile(
|
||||
leading: Text(
|
||||
_timeFormat.format(message.timestamp),
|
||||
style: logStyle?.copyWith(color: color),
|
||||
),
|
||||
title: Text(
|
||||
message.message,
|
||||
style: logStyle?.copyWith(color: color),
|
||||
),
|
||||
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",
|
||||
expandedCrossAxisAlignment: CrossAxisAlignment.start,
|
||||
childrenPadding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
expandedAlignment: Alignment.topLeft,
|
||||
children: source.isNotEmpty
|
||||
? [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.arrow_right),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'In $source',
|
||||
style: logStyle?.copyWith(fontSize: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: null,
|
||||
leading: message.timestamp != null
|
||||
? Text(DateFormat("HH:mm:ss.SSS").format(message.timestamp!))
|
||||
: null,
|
||||
..._buildErrorWidgets(context),
|
||||
]
|
||||
: _buildErrorWidgets(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildErrorWidgets(BuildContext context) {
|
||||
if (message.error != null) {
|
||||
return [
|
||||
Divider(),
|
||||
Text(
|
||||
message.error!.error,
|
||||
style: TextStyle(color: Colors.red),
|
||||
).padded(),
|
||||
if (message.error?.stackTrace != null) ...[
|
||||
Text(
|
||||
message.error!.stackTrace!,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 10,
|
||||
),
|
||||
).paddedOnly(left: 8),
|
||||
],
|
||||
];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,166 +1,260 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:paperless_mobile/core/logging/logger.dart';
|
||||
import 'package:paperless_mobile/core/logging/data/logger.dart';
|
||||
import 'package:paperless_mobile/core/logging/utils/redaction_utils.dart';
|
||||
import 'package:paperless_mobile/helpers/format_helpers.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class FileService {
|
||||
const FileService._();
|
||||
FileService._();
|
||||
|
||||
static Future<File> saveToFile(
|
||||
static FileService? _singleton;
|
||||
|
||||
late final Directory _logDirectory;
|
||||
late final Directory _temporaryDirectory;
|
||||
late final Directory _documentsDirectory;
|
||||
late final Directory _downloadsDirectory;
|
||||
late final Directory _uploadDirectory;
|
||||
late final Directory _temporaryScansDirectory;
|
||||
|
||||
Directory get logDirectory => _logDirectory;
|
||||
Directory get temporaryDirectory => _temporaryDirectory;
|
||||
Directory get documentsDirectory => _documentsDirectory;
|
||||
Directory get downloadsDirectory => _downloadsDirectory;
|
||||
Directory get uploadDirectory => _uploadDirectory;
|
||||
Directory get temporaryScansDirectory => _temporaryScansDirectory;
|
||||
|
||||
Future<void> initialize() async {
|
||||
try {
|
||||
await _initTemporaryDirectory();
|
||||
await _initTemporaryScansDirectory();
|
||||
await _initUploadDirectory();
|
||||
await _initLogDirectory();
|
||||
await _initDownloadsDirectory();
|
||||
await _initializeDocumentsDirectory();
|
||||
} catch (error, stackTrace) {
|
||||
debugPrint("Could not initialize directories.");
|
||||
debugPrint(error.toString());
|
||||
debugPrintStack(stackTrace: stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
/// Make sure to call and await initialize before accessing any of the instance members.
|
||||
static FileService get instance {
|
||||
_singleton ??= FileService._();
|
||||
return _singleton!;
|
||||
}
|
||||
|
||||
Future<File> saveToFile(
|
||||
Uint8List bytes,
|
||||
String filename,
|
||||
) async {
|
||||
final dir = await documentsDirectory;
|
||||
File file = File("${dir.path}/$filename");
|
||||
File file = File(p.join(_logDirectory.path, filename));
|
||||
logger.fd(
|
||||
"Writing bytes to file $filename",
|
||||
methodName: 'saveToFile',
|
||||
className: runtimeType.toString(),
|
||||
);
|
||||
return file..writeAsBytes(bytes);
|
||||
}
|
||||
|
||||
static Future<Directory?> getDirectory(PaperlessDirectoryType type) {
|
||||
Directory getDirectory(PaperlessDirectoryType type) {
|
||||
return switch (type) {
|
||||
PaperlessDirectoryType.documents => documentsDirectory,
|
||||
PaperlessDirectoryType.temporary => temporaryDirectory,
|
||||
PaperlessDirectoryType.scans => temporaryScansDirectory,
|
||||
PaperlessDirectoryType.download => downloadsDirectory,
|
||||
PaperlessDirectoryType.upload => uploadDirectory,
|
||||
PaperlessDirectoryType.documents => _documentsDirectory,
|
||||
PaperlessDirectoryType.temporary => _temporaryDirectory,
|
||||
PaperlessDirectoryType.scans => _temporaryScansDirectory,
|
||||
PaperlessDirectoryType.download => _downloadsDirectory,
|
||||
PaperlessDirectoryType.upload => _uploadDirectory,
|
||||
PaperlessDirectoryType.logs => _logDirectory,
|
||||
};
|
||||
}
|
||||
|
||||
static Future<File> allocateTemporaryFile(
|
||||
///
|
||||
/// Returns a [File] pointing to a temporary file in the directory specified by [type].
|
||||
/// If [create] is true, the file will be created.
|
||||
/// If [fileName] is left blank, a random UUID will be generated.
|
||||
///
|
||||
Future<File> allocateTemporaryFile(
|
||||
PaperlessDirectoryType type, {
|
||||
required String extension,
|
||||
String? fileName,
|
||||
bool create = false,
|
||||
}) async {
|
||||
final dir = await getDirectory(type);
|
||||
final _fileName = (fileName ?? const Uuid().v1()) + '.$extension';
|
||||
return File('${dir?.path}/$_fileName');
|
||||
}
|
||||
|
||||
static Future<Directory> get temporaryDirectory => getTemporaryDirectory();
|
||||
|
||||
static Future<Directory> get documentsDirectory async {
|
||||
if (Platform.isAndroid) {
|
||||
return (await getExternalStorageDirectories(
|
||||
type: StorageDirectory.documents,
|
||||
))!
|
||||
.first;
|
||||
} else if (Platform.isIOS) {
|
||||
final dir = await getApplicationDocumentsDirectory()
|
||||
.then((dir) => Directory('${dir.path}/documents'));
|
||||
return dir.create(recursive: true);
|
||||
} else {
|
||||
throw UnsupportedError("Platform not supported.");
|
||||
final dir = getDirectory(type);
|
||||
final filename = (fileName ?? const Uuid().v1()) + '.$extension';
|
||||
final file = File(p.join(dir.path, filename));
|
||||
if (create) {
|
||||
await file.create(recursive: true);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
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.");
|
||||
Future<Directory> getConsumptionDirectory({required String userId}) async {
|
||||
return Directory(p.join(_uploadDirectory.path, userId))
|
||||
.create(recursive: true);
|
||||
}
|
||||
|
||||
static Future<Directory> get downloadsDirectory async {
|
||||
if (Platform.isAndroid) {
|
||||
var directory = Directory('/storage/emulated/0/Download');
|
||||
if (!directory.existsSync()) {
|
||||
final downloadsDir = await getExternalStorageDirectories(
|
||||
type: StorageDirectory.downloads,
|
||||
);
|
||||
directory = downloadsDir!.first;
|
||||
}
|
||||
return directory;
|
||||
} else if (Platform.isIOS) {
|
||||
final appDir = await getApplicationDocumentsDirectory();
|
||||
final dir = Directory('${appDir.path}/downloads');
|
||||
return dir.create(recursive: true);
|
||||
} else {
|
||||
throw UnsupportedError("Platform not supported.");
|
||||
}
|
||||
}
|
||||
Future<void> clearUserData({required String userId}) async {
|
||||
final redactedId = redactUserId(userId);
|
||||
logger.fd(
|
||||
"Clearing data for user $redactedId...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: "clearUserData",
|
||||
);
|
||||
|
||||
static Future<Directory> get uploadDirectory async {
|
||||
final dir = await getApplicationDocumentsDirectory()
|
||||
.then((dir) => Directory('${dir.path}/upload'));
|
||||
return dir.create(recursive: true);
|
||||
}
|
||||
|
||||
static Future<Directory> getConsumptionDirectory(
|
||||
{required String userId}) async {
|
||||
final uploadDir =
|
||||
await uploadDirectory.then((dir) => Directory('${dir.path}/$userId'));
|
||||
return uploadDir.create(recursive: true);
|
||||
}
|
||||
|
||||
static Future<Directory> get temporaryScansDirectory async {
|
||||
final tempDir = await temporaryDirectory;
|
||||
final scansDir = Directory('${tempDir.path}/scans');
|
||||
return scansDir.create(recursive: true);
|
||||
}
|
||||
|
||||
static Future<void> 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 scanDirSize =
|
||||
formatBytes(await getDirSizeInBytes(_temporaryScansDirectory));
|
||||
final tempDirSize =
|
||||
formatBytes(await getDirSizeInBytes(_temporaryDirectory));
|
||||
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.ft(
|
||||
"Removing scans...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: "clearUserData",
|
||||
);
|
||||
await _temporaryScansDirectory.delete(recursive: true);
|
||||
logger.ft(
|
||||
"Removed $scanDirSize...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: "clearUserData",
|
||||
);
|
||||
logger.ft(
|
||||
"Removing temporary files and cache content...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: "clearUserData",
|
||||
);
|
||||
|
||||
logger.t(
|
||||
"FileService#clearUserData(): Removing temporary files and cache content...");
|
||||
await _temporaryDirectory.delete(recursive: true);
|
||||
logger.ft(
|
||||
"Removed $tempDirSize...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: "clearUserData",
|
||||
);
|
||||
|
||||
await tempDir.delete(recursive: true);
|
||||
logger.t("FileService#clearUserData(): Removed $tempDirSize...");
|
||||
|
||||
logger.t(
|
||||
"FileService#clearUserData(): Removing files waiting for consumption...");
|
||||
logger.ft(
|
||||
"Removing files waiting for consumption...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: "clearUserData",
|
||||
);
|
||||
await consumptionDir.delete(recursive: true);
|
||||
logger.t("FileService#clearUserData(): Removed $consumptionDirSize...");
|
||||
}
|
||||
|
||||
static Future<void> clearDirectoryContent(PaperlessDirectoryType type) async {
|
||||
final dir = await getDirectory(type);
|
||||
|
||||
if (dir == null || !(await dir.exists())) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Future.wait(
|
||||
dir.listSync().map((item) => item.delete(recursive: true)),
|
||||
logger.ft(
|
||||
"Removed $consumptionDirSize...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: "clearUserData",
|
||||
);
|
||||
}
|
||||
|
||||
static Future<List<File>> getAllFiles(Directory directory) {
|
||||
Future<int> clearDirectoryContent(
|
||||
PaperlessDirectoryType type, {
|
||||
bool filesOnly = false,
|
||||
}) async {
|
||||
final dir = getDirectory(type);
|
||||
final dirSize = await getDirSizeInBytes(dir);
|
||||
if (!await dir.exists()) {
|
||||
return 0;
|
||||
}
|
||||
final streamedEntities = filesOnly
|
||||
? dir.list().whereType<File>().cast<FileSystemEntity>()
|
||||
: dir.list();
|
||||
|
||||
final entities = await streamedEntities.toList();
|
||||
await Future.wait([
|
||||
for (var entity in entities) entity.delete(recursive: !filesOnly),
|
||||
]);
|
||||
return dirSize;
|
||||
}
|
||||
|
||||
Future<List<File>> getAllFiles(Directory directory) {
|
||||
return directory.list().whereType<File>().toList();
|
||||
}
|
||||
|
||||
static Future<List<Directory>> getAllSubdirectories(Directory directory) {
|
||||
Future<List<Directory>> getAllSubdirectories(Directory directory) {
|
||||
return directory.list().whereType<Directory>().toList();
|
||||
}
|
||||
|
||||
static Future<int> getDirSizeInBytes(Directory dir) async {
|
||||
Future<int> getDirSizeInBytes(Directory dir) async {
|
||||
return dir
|
||||
.list(recursive: true)
|
||||
.fold(0, (previous, element) => previous + element.statSync().size);
|
||||
}
|
||||
|
||||
Future<void> _initTemporaryDirectory() async {
|
||||
_temporaryDirectory = await getTemporaryDirectory();
|
||||
}
|
||||
|
||||
Future<void> _initializeDocumentsDirectory() async {
|
||||
if (Platform.isAndroid) {
|
||||
final dirs =
|
||||
await getExternalStorageDirectories(type: StorageDirectory.documents);
|
||||
_documentsDirectory = dirs!.first;
|
||||
return;
|
||||
} else if (Platform.isIOS) {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
_documentsDirectory = await Directory(p.join(dir.path, 'documents'))
|
||||
.create(recursive: true);
|
||||
return;
|
||||
} else {
|
||||
throw UnsupportedError("Platform not supported.");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initLogDirectory() async {
|
||||
if (Platform.isAndroid) {
|
||||
_logDirectory =
|
||||
await getExternalStorageDirectories(type: StorageDirectory.documents)
|
||||
.then((directory) async =>
|
||||
directory?.firstOrNull ??
|
||||
await getApplicationDocumentsDirectory())
|
||||
.then((directory) =>
|
||||
Directory('${directory.path}/logs').create(recursive: true));
|
||||
return;
|
||||
} else if (Platform.isIOS) {
|
||||
_logDirectory = await getApplicationDocumentsDirectory().then(
|
||||
(value) => Directory('${value.path}/logs').create(recursive: true));
|
||||
return;
|
||||
}
|
||||
throw UnsupportedError("Platform not supported.");
|
||||
}
|
||||
|
||||
Future<void> _initDownloadsDirectory() async {
|
||||
if (Platform.isAndroid) {
|
||||
var directory = Directory('/storage/emulated/0/Download');
|
||||
if (!await directory.exists()) {
|
||||
final downloadsDir = await getExternalStorageDirectories(
|
||||
type: StorageDirectory.downloads,
|
||||
);
|
||||
directory = await downloadsDir!.first.create(recursive: true);
|
||||
return;
|
||||
}
|
||||
_downloadsDirectory = directory;
|
||||
} else if (Platform.isIOS) {
|
||||
final appDir = await getApplicationDocumentsDirectory();
|
||||
final dir = Directory('${appDir.path}/downloads');
|
||||
_downloadsDirectory = await dir.create(recursive: true);
|
||||
return;
|
||||
} else {
|
||||
throw UnsupportedError("Platform not supported.");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initUploadDirectory() async {
|
||||
final dir = await getApplicationDocumentsDirectory()
|
||||
.then((dir) => Directory('${dir.path}/upload'));
|
||||
_uploadDirectory = await dir.create(recursive: true);
|
||||
}
|
||||
|
||||
Future<void> _initTemporaryScansDirectory() async {
|
||||
_temporaryScansDirectory =
|
||||
await Directory(p.join(_temporaryDirectory.path, 'scans'))
|
||||
.create(recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
enum PaperlessDirectoryType {
|
||||
@@ -168,5 +262,6 @@ enum PaperlessDirectoryType {
|
||||
temporary,
|
||||
scans,
|
||||
download,
|
||||
upload;
|
||||
upload,
|
||||
logs;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,10 @@ extension DateHelpers on DateTime {
|
||||
yesterday.month == month &&
|
||||
yesterday.year == year;
|
||||
}
|
||||
|
||||
bool isOnSameDayAs(DateTime other) {
|
||||
return other.day == day && other.month == month && other.year == year;
|
||||
}
|
||||
}
|
||||
|
||||
extension StringNormalizer on String {
|
||||
|
||||
@@ -2,12 +2,9 @@ import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
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';
|
||||
import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart';
|
||||
@@ -19,6 +16,7 @@ import 'package:paperless_mobile/routes/typed/branches/documents_route.dart';
|
||||
import 'package:paperless_mobile/routes/typed/branches/saved_views_route.dart';
|
||||
import 'package:paperless_mobile/routes/typed/branches/upload_queue_route.dart';
|
||||
import 'package:paperless_mobile/routes/typed/shells/authenticated_route.dart';
|
||||
import 'package:paperless_mobile/routes/typed/top_level/app_logs_route.dart';
|
||||
import 'package:paperless_mobile/routes/typed/top_level/changelog_route.dart';
|
||||
import 'package:paperless_mobile/routes/typed/top_level/settings_route.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -185,12 +183,9 @@ class AppDrawer extends StatelessWidget {
|
||||
ListTile(
|
||||
dense: true,
|
||||
leading: const Icon(Icons.subject),
|
||||
title: const Text('Logs'), //TODO: INTL
|
||||
title: Text(S.of(context)!.appLogs('')),
|
||||
onTap: () {
|
||||
Navigator.of(context)
|
||||
.push(MaterialPageRoute(builder: (context) {
|
||||
return const AppLogsPage();
|
||||
}));
|
||||
AppLogsRoute().push(context);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
|
||||
@@ -6,7 +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/logging/data/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';
|
||||
@@ -85,7 +85,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
}
|
||||
|
||||
Future<ResultType> openDocumentInSystemViewer() async {
|
||||
final cacheDir = await FileService.temporaryDirectory;
|
||||
final cacheDir = FileService.instance.temporaryDirectory;
|
||||
if (state.metaData == null) {
|
||||
await loadMetaData();
|
||||
}
|
||||
@@ -121,7 +121,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
}
|
||||
String targetPath = _buildDownloadFilePath(
|
||||
downloadOriginal,
|
||||
await FileService.downloadsDirectory,
|
||||
FileService.instance.downloadsDirectory,
|
||||
);
|
||||
|
||||
if (!await File(targetPath).exists()) {
|
||||
@@ -170,7 +170,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
locale: locale,
|
||||
userId: userId,
|
||||
);
|
||||
logger.i("Document '${state.document.title}' saved to $targetPath.");
|
||||
logger.fi("Document '${state.document.title}' saved to $targetPath.");
|
||||
}
|
||||
|
||||
Future<void> shareDocument({bool shareOriginal = false}) async {
|
||||
@@ -179,7 +179,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
}
|
||||
String filePath = _buildDownloadFilePath(
|
||||
shareOriginal,
|
||||
await FileService.temporaryDirectory,
|
||||
FileService.instance.temporaryDirectory,
|
||||
);
|
||||
await _api.downloadToFile(
|
||||
state.document,
|
||||
@@ -204,7 +204,7 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
|
||||
await loadMetaData();
|
||||
}
|
||||
final filePath =
|
||||
_buildDownloadFilePath(false, await FileService.temporaryDirectory);
|
||||
_buildDownloadFilePath(false, FileService.instance.temporaryDirectory);
|
||||
await _api.downloadToFile(
|
||||
state.document,
|
||||
filePath,
|
||||
|
||||
@@ -4,7 +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/logging/data/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';
|
||||
@@ -19,13 +19,21 @@ class DocumentScannerCubit extends Cubit<DocumentScannerState> {
|
||||
: super(const InitialDocumentScannerState());
|
||||
|
||||
Future<void> initialize() async {
|
||||
logger.t("Restoring scans...");
|
||||
logger.fd(
|
||||
"Restoring scans...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: "initialize",
|
||||
);
|
||||
emit(const RestoringDocumentScannerState());
|
||||
final tempDir = await FileService.temporaryScansDirectory;
|
||||
final tempDir = FileService.instance.temporaryScansDirectory;
|
||||
final allFiles = tempDir.list().whereType<File>();
|
||||
final scans =
|
||||
await allFiles.where((event) => event.path.endsWith(".jpeg")).toList();
|
||||
logger.t("Restored ${scans.length} scans.");
|
||||
logger.fd(
|
||||
"Restored ${scans.length} scans.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: "initialize",
|
||||
);
|
||||
emit(
|
||||
scans.isEmpty
|
||||
? const InitialDocumentScannerState()
|
||||
@@ -75,7 +83,7 @@ class DocumentScannerCubit extends Cubit<DocumentScannerState> {
|
||||
String fileName,
|
||||
String locale,
|
||||
) async {
|
||||
var file = await FileService.saveToFile(bytes, fileName);
|
||||
var file = await FileService.instance.saveToFile(bytes, fileName);
|
||||
_notificationService.notifyFileSaved(
|
||||
filename: fileName,
|
||||
filePath: file.path,
|
||||
|
||||
@@ -227,9 +227,10 @@ class _ScannerPageState extends State<ScannerPage>
|
||||
if (!isGranted) {
|
||||
return;
|
||||
}
|
||||
final file = await FileService.allocateTemporaryFile(
|
||||
final file = await FileService.instance.allocateTemporaryFile(
|
||||
PaperlessDirectoryType.scans,
|
||||
extension: 'jpeg',
|
||||
create: true,
|
||||
);
|
||||
if (kDebugMode) {
|
||||
dev.log('[ScannerPage] Created temporary file: ${file.path}');
|
||||
|
||||
@@ -12,7 +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/logging/data/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';
|
||||
@@ -378,8 +378,10 @@ class _DocumentUploadPreparationPageState
|
||||
} on PaperlessFormValidationException catch (exception) {
|
||||
setState(() => _errors = exception.validationMessages);
|
||||
} catch (error, stackTrace) {
|
||||
logger.e(
|
||||
logger.fe(
|
||||
"An unknown error occurred during document upload.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: "_onSubmit",
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import 'dart:async';
|
||||
|
||||
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/logging/data/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';
|
||||
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/cubit/document_paging_bloc_mixin.dart';
|
||||
import 'package:paperless_mobile/features/paged_document_view/cubit/paged_documents_state.dart';
|
||||
|
||||
part 'inbox_cubit.g.dart';
|
||||
part 'inbox_state.dart';
|
||||
@@ -50,18 +49,12 @@ class InboxCubit extends HydratedCubit<InboxState>
|
||||
final wasInInboxBeforeUpdate =
|
||||
state.documents.map((e) => e.id).contains(document.id);
|
||||
if (!hasInboxTag && wasInInboxBeforeUpdate) {
|
||||
print(
|
||||
"INBOX: Removing document: has: $hasInboxTag, had: $wasInInboxBeforeUpdate");
|
||||
remove(document);
|
||||
emit(state.copyWith(itemsInInboxCount: state.itemsInInboxCount - 1));
|
||||
} else if (hasInboxTag) {
|
||||
if (wasInInboxBeforeUpdate) {
|
||||
print(
|
||||
"INBOX: Replacing document: has: $hasInboxTag, had: $wasInInboxBeforeUpdate");
|
||||
replace(document);
|
||||
} else {
|
||||
print(
|
||||
"INBOX: Adding document: has: $hasInboxTag, had: $wasInInboxBeforeUpdate");
|
||||
_addDocument(document);
|
||||
emit(
|
||||
state.copyWith(itemsInInboxCount: state.itemsInInboxCount + 1));
|
||||
@@ -84,17 +77,26 @@ class InboxCubit extends HydratedCubit<InboxState>
|
||||
}
|
||||
|
||||
Future<void> refreshItemsInInboxCount([bool shouldLoadInbox = true]) async {
|
||||
logger.t(
|
||||
"InboxCubit#refreshItemsInInboxCount(): Checking for new documents in inbox...");
|
||||
logger.fi(
|
||||
"Checking for new documents in inbox...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: "refreshItemsInInboxCount",
|
||||
);
|
||||
final stats = await _statsApi.getServerStatistics();
|
||||
|
||||
if (stats.documentsInInbox != state.itemsInInboxCount && shouldLoadInbox) {
|
||||
logger.t(
|
||||
"InboxCubit#refreshItemsInInboxCount(): New documents found in inbox, reloading inbox.");
|
||||
logger.fi(
|
||||
"New documents found in inbox, reloading.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: "refreshItemsInInboxCount",
|
||||
);
|
||||
await loadInbox();
|
||||
} else {
|
||||
logger.t(
|
||||
"InboxCubit#refreshItemsInInboxCount(): No new documents found in inbox.");
|
||||
logger.fi(
|
||||
"No new documents found in inbox.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: "refreshItemsInInboxCount",
|
||||
);
|
||||
}
|
||||
emit(state.copyWith(itemsInInboxCount: stats.documentsInInbox));
|
||||
}
|
||||
|
||||
@@ -7,7 +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/logging/data/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';
|
||||
@@ -213,16 +213,18 @@ class _LabelsPageState extends State<LabelsPage>
|
||||
][_currentIndex]
|
||||
.call();
|
||||
} catch (error, stackTrace) {
|
||||
logger.e(
|
||||
"An error ocurred while reloading "
|
||||
"${[
|
||||
"correspondents",
|
||||
"document types",
|
||||
"tags",
|
||||
"storage paths"
|
||||
][_currentIndex]}: ${error.toString()}",
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
logger.fe(
|
||||
"An error ocurred while reloading "
|
||||
"${[
|
||||
"correspondents",
|
||||
"document types",
|
||||
"tags",
|
||||
"storage paths"
|
||||
][_currentIndex]}.",
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'onRefresh');
|
||||
}
|
||||
},
|
||||
child: TabBarView(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:hive_flutter/adapters.dart';
|
||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
@@ -13,7 +14,8 @@ 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/logging/data/logger.dart';
|
||||
import 'package:paperless_mobile/core/logging/utils/redaction_utils.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';
|
||||
@@ -56,7 +58,13 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
}
|
||||
emit(const AuthenticatingState(AuthenticatingStage.authenticating));
|
||||
final localUserId = "${credentials.username}@$serverUrl";
|
||||
logger.t("AuthenticationCubit#login(): Trying to log in $localUserId...");
|
||||
final redactedId = redactUserId(localUserId);
|
||||
|
||||
logger.fd(
|
||||
"Trying to log in $redactedId...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'login',
|
||||
);
|
||||
try {
|
||||
await _addUser(
|
||||
localUserId,
|
||||
@@ -95,15 +103,22 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
await globalSettings.save();
|
||||
|
||||
emit(AuthenticatedState(localUserId: localUserId));
|
||||
logger.t(
|
||||
'AuthenticationCubit#login(): User $localUserId successfully logged in.');
|
||||
logger.fd(
|
||||
'User $redactedId successfully logged in.',
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'login',
|
||||
);
|
||||
}
|
||||
|
||||
/// Switches to another account if it exists.
|
||||
Future<void> switchAccount(String localUserId) async {
|
||||
emit(const SwitchingAccountsState());
|
||||
logger.t(
|
||||
'AuthenticationCubit#switchAccount(): Trying to switch to user $localUserId...');
|
||||
final redactedId = redactUserId(localUserId);
|
||||
logger.fd(
|
||||
'Trying to switch to user $redactedId...',
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'switchAccount',
|
||||
);
|
||||
|
||||
final globalSettings =
|
||||
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
|
||||
@@ -111,9 +126,11 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
final userAccountBox = Hive.localUserAccountBox;
|
||||
|
||||
if (!userAccountBox.containsKey(localUserId)) {
|
||||
logger.w(
|
||||
'AuthenticationCubit#switchAccount(): User $localUserId not yet registered. '
|
||||
logger.fw(
|
||||
'User $redactedId not yet registered. '
|
||||
'This should never be the case!',
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'switchAccount',
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -124,8 +141,11 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
final authenticated = await _localAuthService
|
||||
.authenticateLocalUser("Authenticate to switch your account.");
|
||||
if (!authenticated) {
|
||||
logger.w(
|
||||
"AuthenticationCubit#switchAccount(): User could not be authenticated.");
|
||||
logger.fw(
|
||||
"User could not be authenticated.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'switchAccount',
|
||||
);
|
||||
emit(VerifyIdentityState(userId: localUserId));
|
||||
return;
|
||||
}
|
||||
@@ -138,8 +158,11 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
HiveBoxes.localUserCredentials, (credentialsBox) async {
|
||||
if (!credentialsBox.containsKey(localUserId)) {
|
||||
await credentialsBox.close();
|
||||
logger.w(
|
||||
"AuthenticationCubit#switchAccount(): Invalid authentication for $localUserId.");
|
||||
logger.fw(
|
||||
"Invalid authentication for $redactedId.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'switchAccount',
|
||||
);
|
||||
return;
|
||||
}
|
||||
final credentials = credentialsBox.get(localUserId);
|
||||
@@ -176,8 +199,12 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
}) async {
|
||||
assert(credentials.password != null && credentials.username != null);
|
||||
final localUserId = "${credentials.username}@$serverUrl";
|
||||
logger
|
||||
.d("AuthenticationCubit#addAccount(): Adding account $localUserId...");
|
||||
final redactedId = redactUserId(localUserId);
|
||||
logger.fd(
|
||||
"Adding account $redactedId...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'switchAccount',
|
||||
);
|
||||
|
||||
final sessionManager = SessionManager([
|
||||
LanguageHeaderInterceptor(locale),
|
||||
@@ -194,12 +221,16 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
}
|
||||
|
||||
Future<void> removeAccount(String userId) async {
|
||||
logger
|
||||
.t("AuthenticationCubit#removeAccount(): Removing account $userId...");
|
||||
final redactedId = redactUserId(userId);
|
||||
logger.fd(
|
||||
"Trying to remove account $redactedId...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'removeAccount',
|
||||
);
|
||||
final userAccountBox = Hive.localUserAccountBox;
|
||||
final userAppStateBox = Hive.localUserAppStateBox;
|
||||
|
||||
await FileService.clearUserData(userId: userId);
|
||||
await FileService.instance.clearUserData(userId: userId);
|
||||
await userAccountBox.delete(userId);
|
||||
await userAppStateBox.delete(userId);
|
||||
await withEncryptedBox<UserCredentials, void>(
|
||||
@@ -213,15 +244,21 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
///
|
||||
Future<void> restoreSession([String? userId]) async {
|
||||
emit(const RestoringSessionState());
|
||||
logger.t(
|
||||
"AuthenticationCubit#restoreSessionState(): Trying to restore previous session...");
|
||||
logger.fd(
|
||||
"Trying to restore previous session...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'restoreSession',
|
||||
);
|
||||
final globalSettings =
|
||||
Hive.box<GlobalSettings>(HiveBoxes.globalSettings).getValue()!;
|
||||
final restoreSessionForUser = userId ?? globalSettings.loggedInUserId;
|
||||
// final localUserId = globalSettings.loggedInUserId;
|
||||
if (restoreSessionForUser == null) {
|
||||
logger.t(
|
||||
"AuthenticationCubit#restoreSessionState(): There is nothing to restore.");
|
||||
logger.fd(
|
||||
"There is nothing to restore.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'restoreSession',
|
||||
);
|
||||
final otherAccountsExist = Hive.localUserAccountBox.isNotEmpty;
|
||||
// If there is nothing to restore, we can quit here.
|
||||
emit(
|
||||
@@ -233,24 +270,36 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
Hive.box<LocalUserAccount>(HiveBoxes.localUserAccount);
|
||||
final localUserAccount = localUserAccountBox.get(restoreSessionForUser)!;
|
||||
if (localUserAccount.settings.isBiometricAuthenticationEnabled) {
|
||||
logger.t(
|
||||
"AuthenticationCubit#restoreSessionState(): Verifying user identity...");
|
||||
logger.fd(
|
||||
"Verifying user identity...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'restoreSession',
|
||||
);
|
||||
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.");
|
||||
logger.fw(
|
||||
"Identity could not be verified.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'restoreSession',
|
||||
);
|
||||
emit(VerifyIdentityState(userId: restoreSessionForUser));
|
||||
return;
|
||||
}
|
||||
logger.t(
|
||||
"AuthenticationCubit#restoreSessionState(): Identity successfully verified.");
|
||||
logger.fd(
|
||||
"Identity successfully verified.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'restoreSession',
|
||||
);
|
||||
}
|
||||
logger.t(
|
||||
"AuthenticationCubit#restoreSessionState(): Reading encrypted credentials...");
|
||||
logger.fd(
|
||||
"Reading encrypted credentials...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'restoreSession',
|
||||
);
|
||||
final authentication =
|
||||
await withEncryptedBox<UserCredentials, UserCredentials>(
|
||||
HiveBoxes.localUserCredentials, (box) {
|
||||
@@ -258,33 +307,48 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
});
|
||||
|
||||
if (authentication == null) {
|
||||
logger.e(
|
||||
"AuthenticationCubit#restoreSessionState(): Credentials could not be read!");
|
||||
logger.fe(
|
||||
"Credentials could not be read!",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'restoreSession',
|
||||
);
|
||||
throw Exception(
|
||||
"User should be authenticated but no authentication information was found.",
|
||||
);
|
||||
}
|
||||
logger.t(
|
||||
"AuthenticationCubit#restoreSessionState(): Credentials successfully retrieved.");
|
||||
logger.fd(
|
||||
"Credentials successfully retrieved.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'restoreSession',
|
||||
);
|
||||
|
||||
logger.t(
|
||||
"AuthenticationCubit#restoreSessionState(): Updating security context...");
|
||||
logger.fd(
|
||||
"Updating security context...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'restoreSession',
|
||||
);
|
||||
|
||||
_sessionManager.updateSettings(
|
||||
clientCertificate: authentication.clientCertificate,
|
||||
authToken: authentication.token,
|
||||
baseUrl: localUserAccount.serverUrl,
|
||||
);
|
||||
logger.t(
|
||||
"AuthenticationCubit#restoreSessionState(): Security context successfully updated.");
|
||||
logger.fd(
|
||||
"Security context successfully updated.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'restoreSession',
|
||||
);
|
||||
final isPaperlessServerReachable =
|
||||
await _connectivityService.isPaperlessServerReachable(
|
||||
localUserAccount.serverUrl,
|
||||
authentication.clientCertificate,
|
||||
) ==
|
||||
ReachabilityStatus.reachable;
|
||||
logger.t(
|
||||
"AuthenticationCubit#restoreSessionState(): Trying to update remote paperless user...");
|
||||
logger.fd(
|
||||
"Trying to update remote paperless user...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'restoreSession',
|
||||
);
|
||||
if (isPaperlessServerReachable) {
|
||||
final apiVersion = await _getApiVersion(_sessionManager.client);
|
||||
await _updateRemoteUser(
|
||||
@@ -292,51 +356,83 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
localUserAccount,
|
||||
apiVersion,
|
||||
);
|
||||
logger.t(
|
||||
"AuthenticationCubit#restoreSessionState(): Successfully updated remote paperless user.");
|
||||
logger.fd(
|
||||
"Successfully updated remote paperless user.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'restoreSession',
|
||||
);
|
||||
} else {
|
||||
logger.w(
|
||||
"AuthenticationCubit#restoreSessionState(): Could not update remote paperless user. Server could not be reached. The app might behave unexpected!");
|
||||
logger.fw(
|
||||
"Could not update remote paperless user - "
|
||||
"Server could not be reached. The app might behave unexpected!",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'restoreSession',
|
||||
);
|
||||
}
|
||||
globalSettings.loggedInUserId = restoreSessionForUser;
|
||||
await globalSettings.save();
|
||||
emit(AuthenticatedState(localUserId: restoreSessionForUser));
|
||||
|
||||
logger.t(
|
||||
"AuthenticationCubit#restoreSessionState(): Previous session successfully restored.");
|
||||
logger.fd(
|
||||
"Previous session successfully restored.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'restoreSession',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> logout([bool removeAccount = false]) async {
|
||||
Future<void> logout([bool shouldRemoveAccount = false]) async {
|
||||
emit(const LoggingOutState());
|
||||
final globalSettings = Hive.globalSettingsBox.getValue()!;
|
||||
final userId = globalSettings.loggedInUserId!;
|
||||
logger.t(
|
||||
"AuthenticationCubit#logout(): Logging out current user ($userId)...");
|
||||
final redactedId = redactUserId(userId);
|
||||
|
||||
logger.fd(
|
||||
"Logging out $redactedId...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'logout',
|
||||
);
|
||||
|
||||
await _resetExternalState();
|
||||
await _notificationService.cancelUserNotifications(userId);
|
||||
|
||||
final otherAccountsExist = Hive.localUserAccountBox.length > 1;
|
||||
emit(UnauthenticatedState(redirectToAccountSelection: otherAccountsExist));
|
||||
if (removeAccount) {
|
||||
await this.removeAccount(userId);
|
||||
if (shouldRemoveAccount) {
|
||||
await removeAccount(userId);
|
||||
}
|
||||
globalSettings.loggedInUserId = null;
|
||||
await globalSettings.save();
|
||||
|
||||
logger.t("AuthenticationCubit#logout(): User successfully logged out.");
|
||||
logger.fd(
|
||||
"User successfully logged out.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: 'logout',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _resetExternalState() async {
|
||||
logger.t(
|
||||
"AuthenticationCubit#_resetExternalState(): Resetting security context...");
|
||||
logger.fd(
|
||||
"Resetting security context...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_resetExternalState',
|
||||
);
|
||||
_sessionManager.resetSettings();
|
||||
logger.t(
|
||||
"AuthenticationCubit#_resetExternalState(): Security context reset.");
|
||||
logger.t(
|
||||
"AuthenticationCubit#_resetExternalState(): Clearing local state...");
|
||||
logger.fd(
|
||||
"Security context reset.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_resetExternalState',
|
||||
);
|
||||
logger.fd(
|
||||
"Clearing local state...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_resetExternalState',
|
||||
);
|
||||
await HydratedBloc.storage.clear();
|
||||
logger.t("AuthenticationCubit#_resetExternalState(): Local state cleard.");
|
||||
logger.fd(
|
||||
"Local state cleard.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_resetExternalState',
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> _addUser(
|
||||
@@ -350,8 +446,13 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
_FutureVoidCallback? onFetchUserInformation,
|
||||
}) async {
|
||||
assert(credentials.username != null && credentials.password != null);
|
||||
logger
|
||||
.t("AuthenticationCubit#_addUser(): Adding new user $localUserId....");
|
||||
final redactedId = redactUserId(localUserId);
|
||||
|
||||
logger.fd(
|
||||
"Adding new user $redactedId..",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_addUser',
|
||||
);
|
||||
|
||||
sessionManager.updateSettings(
|
||||
baseUrl: serverUrl,
|
||||
@@ -360,8 +461,11 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
|
||||
final authApi = _apiFactory.createAuthenticationApi(sessionManager.client);
|
||||
|
||||
logger.t(
|
||||
"AuthenticationCubit#_addUser(): Fetching bearer token from the server...");
|
||||
logger.fd(
|
||||
"Fetching bearer token from the server...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_addUser',
|
||||
);
|
||||
|
||||
await onPerformLogin?.call();
|
||||
|
||||
@@ -370,8 +474,11 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
password: credentials.password!,
|
||||
);
|
||||
|
||||
logger.t(
|
||||
"AuthenticationCubit#_addUser(): Bearer token successfully retrieved.");
|
||||
logger.fd(
|
||||
"Bearer token successfully retrieved.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_addUser',
|
||||
);
|
||||
|
||||
sessionManager.updateSettings(
|
||||
baseUrl: serverUrl,
|
||||
@@ -385,14 +492,20 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
Hive.box<LocalUserAppState>(HiveBoxes.localUserAppState);
|
||||
|
||||
if (userAccountBox.containsKey(localUserId)) {
|
||||
logger.w(
|
||||
"AuthenticationCubit#_addUser(): The user $localUserId already exists.");
|
||||
logger.fw(
|
||||
"The user $redactedId already exists.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_addUser',
|
||||
);
|
||||
throw InfoMessageException(code: ErrorCode.userAlreadyExists);
|
||||
}
|
||||
await onFetchUserInformation?.call();
|
||||
final apiVersion = await _getApiVersion(sessionManager.client);
|
||||
logger.t(
|
||||
"AuthenticationCubit#_addUser(): Trying to fetch remote paperless user for $localUserId.");
|
||||
logger.fd(
|
||||
"Trying to fetch remote paperless user for $redactedId.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_addUser',
|
||||
);
|
||||
|
||||
late UserModel serverUser;
|
||||
try {
|
||||
@@ -403,19 +516,27 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
)
|
||||
.findCurrentUser();
|
||||
} on DioException catch (error, stackTrace) {
|
||||
logger.e(
|
||||
"AuthenticationCubit#_addUser(): An error occurred while fetching the remote paperless user.",
|
||||
logger.fe(
|
||||
"An error occurred while fetching the remote paperless user.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_addUser',
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
|
||||
rethrow;
|
||||
}
|
||||
logger.t(
|
||||
"AuthenticationCubit#_addUser(): Remote paperless user successfully fetched.");
|
||||
logger.fd(
|
||||
"Remote paperless user successfully fetched.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_addUser',
|
||||
);
|
||||
|
||||
logger.t(
|
||||
"AuthenticationCubit#_addUser(): Persisting user account information...");
|
||||
logger.fd(
|
||||
"Persisting user account information...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_addUser',
|
||||
);
|
||||
|
||||
await onPersistLocalUserData?.call();
|
||||
// Create user account
|
||||
@@ -429,20 +550,33 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
apiVersion: apiVersion,
|
||||
),
|
||||
);
|
||||
logger.t(
|
||||
"AuthenticationCubit#_addUser(): User account information successfully persisted.");
|
||||
logger.t("AuthenticationCubit#_addUser(): Persisting user app state...");
|
||||
logger.fd(
|
||||
"User account information successfully persisted.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_addUser',
|
||||
);
|
||||
logger.fd(
|
||||
"Persisting user app state...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_addUser',
|
||||
);
|
||||
// Create user state
|
||||
await userStateBox.put(
|
||||
localUserId,
|
||||
LocalUserAppState(userId: localUserId),
|
||||
);
|
||||
logger.t(
|
||||
"AuthenticationCubit#_addUser(): User state successfully persisted.");
|
||||
logger.fd(
|
||||
"User state successfully persisted.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_addUser',
|
||||
);
|
||||
// Save credentials in encrypted box
|
||||
await withEncryptedBox(HiveBoxes.localUserCredentials, (box) async {
|
||||
logger.t(
|
||||
"AuthenticationCubit#_addUser(): Saving user credentials inside encrypted storage...");
|
||||
logger.fd(
|
||||
"Saving user credentials inside encrypted storage...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_addUser',
|
||||
);
|
||||
|
||||
await box.put(
|
||||
localUserId,
|
||||
@@ -451,12 +585,20 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
clientCertificate: clientCert,
|
||||
),
|
||||
);
|
||||
logger.t(
|
||||
"AuthenticationCubit#_addUser(): User credentials successfully saved.");
|
||||
logger.fd(
|
||||
"User credentials successfully saved.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_addUser',
|
||||
);
|
||||
});
|
||||
final hostsBox = Hive.box<String>(HiveBoxes.hosts);
|
||||
if (!hostsBox.values.contains(serverUrl)) {
|
||||
await hostsBox.add(serverUrl);
|
||||
logger.fd(
|
||||
"Added new url to list of hosts.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_addUser',
|
||||
);
|
||||
}
|
||||
|
||||
return serverUser.id;
|
||||
@@ -467,8 +609,11 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
Duration? timeout,
|
||||
int defaultValue = 2,
|
||||
}) async {
|
||||
logger.t(
|
||||
"AuthenticationCubit#_getApiVersion(): Trying to fetch API version...");
|
||||
logger.fd(
|
||||
"Trying to fetch API version...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_getApiVersion',
|
||||
);
|
||||
try {
|
||||
final response = await dio.get(
|
||||
"/api/",
|
||||
@@ -478,13 +623,19 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
);
|
||||
final apiVersion =
|
||||
int.parse(response.headers.value('x-api-version') ?? "3");
|
||||
logger.t(
|
||||
"AuthenticationCubit#_getApiVersion(): Successfully retrieved API version ($apiVersion).");
|
||||
logger.fd(
|
||||
"Successfully retrieved API version ($apiVersion).",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_getApiVersion',
|
||||
);
|
||||
|
||||
return apiVersion;
|
||||
} on DioException catch (_) {
|
||||
logger.w(
|
||||
"AuthenticationCubit#_getApiVersion(): Could not retrieve API version.");
|
||||
logger.fw(
|
||||
"Could not retrieve API version, using default ($defaultValue).",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_getApiVersion',
|
||||
);
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
@@ -495,18 +646,21 @@ class AuthenticationCubit extends Cubit<AuthenticationState> {
|
||||
LocalUserAccount localUserAccount,
|
||||
int apiVersion,
|
||||
) async {
|
||||
logger.t(
|
||||
"AuthenticationCubit#_updateRemoteUser(): Trying to update remote user object...");
|
||||
logger.fd(
|
||||
"Trying to update remote user object...",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_updateRemoteUser',
|
||||
);
|
||||
final updatedPaperlessUser = await _apiFactory
|
||||
.createUserApi(
|
||||
sessionManager.client,
|
||||
apiVersion: apiVersion,
|
||||
)
|
||||
.createUserApi(sessionManager.client, apiVersion: apiVersion)
|
||||
.findCurrentUser();
|
||||
|
||||
localUserAccount.paperlessUser = updatedPaperlessUser;
|
||||
await localUserAccount.save();
|
||||
logger.t(
|
||||
"AuthenticationCubit#_updateRemoteUser(): Successfully updated remote user object.");
|
||||
logger.fd(
|
||||
"Successfully updated remote user object.",
|
||||
className: runtimeType.toString(),
|
||||
methodName: '_updateRemoteUser',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,43 +18,25 @@ class _ClearCacheSettingState extends State<ClearCacheSetting> {
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(S.of(context)!.clearCache),
|
||||
subtitle: FutureBuilder<String>(
|
||||
future: FileService.temporaryDirectory.then(_dirSize),
|
||||
subtitle: FutureBuilder<int>(
|
||||
future: FileService.instance
|
||||
.getDirSizeInBytes(FileService.instance.temporaryDirectory),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return Text(S.of(context)!.calculatingDots);
|
||||
}
|
||||
return Text(S.of(context)!.freeBytes(snapshot.data!));
|
||||
final dirSize = formatBytes(snapshot.data!);
|
||||
return Text(S.of(context)!.freeBytes(dirSize));
|
||||
},
|
||||
),
|
||||
onTap: () async {
|
||||
final dir = await FileService.temporaryDirectory;
|
||||
final deletedSize = await _dirSize(dir);
|
||||
await dir.delete(recursive: true);
|
||||
final freedBytes = await FileService.instance
|
||||
.clearDirectoryContent(PaperlessDirectoryType.temporary);
|
||||
showSnackBar(
|
||||
context,
|
||||
S.of(context)!.freedDiskSpace(deletedSize),
|
||||
S.of(context)!.freedDiskSpace(formatBytes(freedBytes)),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _dirSize(Directory dir) async {
|
||||
int totalSize = 0;
|
||||
try {
|
||||
if (await dir.exists()) {
|
||||
dir
|
||||
.listSync(recursive: true, followLinks: false)
|
||||
.forEach((FileSystemEntity entity) async {
|
||||
if (entity is File) {
|
||||
totalSize += (await entity.length());
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
debugPrint(error.toString());
|
||||
}
|
||||
|
||||
return formatBytes(totalSize, 0);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ class ConsumptionChangeNotifier extends ChangeNotifier {
|
||||
return [];
|
||||
}
|
||||
final consumptionDirectory =
|
||||
await FileService.getConsumptionDirectory(userId: userId);
|
||||
await FileService.instance.getConsumptionDirectory(userId: userId);
|
||||
final List<File> localFiles = [];
|
||||
for (final file in files) {
|
||||
if (!file.path.startsWith(consumptionDirectory.path)) {
|
||||
@@ -53,7 +53,7 @@ class ConsumptionChangeNotifier extends ChangeNotifier {
|
||||
required String userId,
|
||||
}) async {
|
||||
final consumptionDirectory =
|
||||
await FileService.getConsumptionDirectory(userId: userId);
|
||||
await FileService.instance.getConsumptionDirectory(userId: userId);
|
||||
if (file.path.startsWith(consumptionDirectory.path)) {
|
||||
await file.delete();
|
||||
}
|
||||
@@ -70,8 +70,8 @@ class ConsumptionChangeNotifier extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<List<File>> _getCurrentFiles(String userId) async {
|
||||
final directory = await FileService.getConsumptionDirectory(userId: userId);
|
||||
final files = await FileService.getAllFiles(directory);
|
||||
return files;
|
||||
final directory =
|
||||
await FileService.instance.getConsumptionDirectory(userId: userId);
|
||||
return await FileService.instance.getAllFiles(directory);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,6 @@ class _EventListenerShellState extends State<EventListenerShell>
|
||||
if (!currentUser.paperlessUser.canViewInbox || _inboxTimer != null) {
|
||||
return;
|
||||
}
|
||||
cubit.refreshItemsInInboxCount(false);
|
||||
_inboxTimer = Timer.periodic(30.seconds, (_) {
|
||||
cubit.refreshItemsInInboxCount(false);
|
||||
});
|
||||
|
||||
@@ -1001,5 +1001,13 @@
|
||||
"@discardChangesWarning": {
|
||||
"description": "Warning message shown when the user tries to close a route without saving the changes."
|
||||
},
|
||||
"changelog": "Changelog"
|
||||
"changelog": "Changelog",
|
||||
"noLogsFoundOn": "No logs found on {date}.",
|
||||
"logfileBottomReached": "You have reached the bottom of this logfile.",
|
||||
"appLogs": "App logs {date}",
|
||||
"saveLogsToFile": "Save logs to file",
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"couldNotLoadLogfileFrom": "Could not load logfile from {date}.",
|
||||
"loadingLogsFrom": "Loading logs from {date}...",
|
||||
"clearLogs": "Clear logs from {date}"
|
||||
}
|
||||
@@ -1001,5 +1001,13 @@
|
||||
"@discardChangesWarning": {
|
||||
"description": "Warning message shown when the user tries to close a route without saving the changes."
|
||||
},
|
||||
"changelog": "Changelog"
|
||||
"changelog": "Changelog",
|
||||
"noLogsFoundOn": "No logs found on {date}.",
|
||||
"logfileBottomReached": "You have reached the bottom of this logfile.",
|
||||
"appLogs": "App logs {date}",
|
||||
"saveLogsToFile": "Save logs to file",
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"couldNotLoadLogfileFrom": "Could not load logfile from {date}.",
|
||||
"loadingLogsFrom": "Loading logs from {date}...",
|
||||
"clearLogs": "Clear logs from {date}"
|
||||
}
|
||||
@@ -1001,5 +1001,13 @@
|
||||
"@discardChangesWarning": {
|
||||
"description": "Warning message shown when the user tries to close a route without saving the changes."
|
||||
},
|
||||
"changelog": "Changelog"
|
||||
"changelog": "Changelog",
|
||||
"noLogsFoundOn": "Keine Logs am {date} gefunden.",
|
||||
"logfileBottomReached": "Du hast das Ende dieser Logdatei erreicht.",
|
||||
"appLogs": "App Logs {date}",
|
||||
"saveLogsToFile": "Logs in Datei speichern",
|
||||
"copyToClipboard": "In Zwischenablage kopieren",
|
||||
"couldNotLoadLogfileFrom": "Logs vom {date} konnten nicht geladen werden.",
|
||||
"loadingLogsFrom": "Lade Logs vom {date}...",
|
||||
"clearLogs": "Logs vom {date} leeren"
|
||||
}
|
||||
@@ -1001,5 +1001,13 @@
|
||||
"@discardChangesWarning": {
|
||||
"description": "Warning message shown when the user tries to close a route without saving the changes."
|
||||
},
|
||||
"changelog": "Changelog"
|
||||
"changelog": "Changelog",
|
||||
"noLogsFoundOn": "No logs found on {date}.",
|
||||
"logfileBottomReached": "You have reached the bottom of this logfile.",
|
||||
"appLogs": "App logs {date}",
|
||||
"saveLogsToFile": "Save logs to file",
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"couldNotLoadLogfileFrom": "Could not load logfile from {date}.",
|
||||
"loadingLogsFrom": "Loading logs from {date}...",
|
||||
"clearLogs": "Clear logs from {date}"
|
||||
}
|
||||
@@ -1001,5 +1001,13 @@
|
||||
"@discardChangesWarning": {
|
||||
"description": "Warning message shown when the user tries to close a route without saving the changes."
|
||||
},
|
||||
"changelog": "Changelog"
|
||||
"changelog": "Changelog",
|
||||
"noLogsFoundOn": "No logs found on {date}.",
|
||||
"logfileBottomReached": "You have reached the bottom of this logfile.",
|
||||
"appLogs": "App logs {date}",
|
||||
"saveLogsToFile": "Save logs to file",
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"couldNotLoadLogfileFrom": "Could not load logfile from {date}.",
|
||||
"loadingLogsFrom": "Loading logs from {date}...",
|
||||
"clearLogs": "Clear logs from {date}"
|
||||
}
|
||||
@@ -1001,5 +1001,13 @@
|
||||
"@discardChangesWarning": {
|
||||
"description": "Warning message shown when the user tries to close a route without saving the changes."
|
||||
},
|
||||
"changelog": "Changelog"
|
||||
"changelog": "Changelog",
|
||||
"noLogsFoundOn": "No logs found on {date}.",
|
||||
"logfileBottomReached": "You have reached the bottom of this logfile.",
|
||||
"appLogs": "App logs {date}",
|
||||
"saveLogsToFile": "Save logs to file",
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"couldNotLoadLogfileFrom": "Could not load logfile from {date}.",
|
||||
"loadingLogsFrom": "Loading logs from {date}...",
|
||||
"clearLogs": "Clear logs from {date}"
|
||||
}
|
||||
@@ -1001,5 +1001,13 @@
|
||||
"@discardChangesWarning": {
|
||||
"description": "Warning message shown when the user tries to close a route without saving the changes."
|
||||
},
|
||||
"changelog": "Changelog"
|
||||
"changelog": "Changelog",
|
||||
"noLogsFoundOn": "No logs found on {date}.",
|
||||
"logfileBottomReached": "You have reached the bottom of this logfile.",
|
||||
"appLogs": "App logs {date}",
|
||||
"saveLogsToFile": "Save logs to file",
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"couldNotLoadLogfileFrom": "Could not load logfile from {date}.",
|
||||
"loadingLogsFrom": "Loading logs from {date}...",
|
||||
"clearLogs": "Clear logs from {date}"
|
||||
}
|
||||
@@ -1001,5 +1001,13 @@
|
||||
"@discardChangesWarning": {
|
||||
"description": "Warning message shown when the user tries to close a route without saving the changes."
|
||||
},
|
||||
"changelog": "Changelog"
|
||||
"changelog": "Changelog",
|
||||
"noLogsFoundOn": "No logs found on {date}.",
|
||||
"logfileBottomReached": "You have reached the bottom of this logfile.",
|
||||
"appLogs": "App logs {date}",
|
||||
"saveLogsToFile": "Save logs to file",
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"couldNotLoadLogfileFrom": "Could not load logfile from {date}.",
|
||||
"loadingLogsFrom": "Loading logs from {date}...",
|
||||
"clearLogs": "Clear logs from {date}"
|
||||
}
|
||||
@@ -1001,5 +1001,13 @@
|
||||
"@discardChangesWarning": {
|
||||
"description": "Warning message shown when the user tries to close a route without saving the changes."
|
||||
},
|
||||
"changelog": "Changelog"
|
||||
"changelog": "Changelog",
|
||||
"noLogsFoundOn": "No logs found on {date}.",
|
||||
"logfileBottomReached": "You have reached the bottom of this logfile.",
|
||||
"appLogs": "App logs {date}",
|
||||
"saveLogsToFile": "Save logs to file",
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"couldNotLoadLogfileFrom": "Could not load logfile from {date}.",
|
||||
"loadingLogsFrom": "Loading logs from {date}...",
|
||||
"clearLogs": "Clear logs from {date}"
|
||||
}
|
||||
@@ -29,10 +29,13 @@ 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/logging/data/formatted_printer.dart';
|
||||
import 'package:paperless_mobile/core/logging/data/logger.dart';
|
||||
import 'package:paperless_mobile/core/logging/data/mirrored_file_output.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';
|
||||
import 'package:paperless_mobile/core/service/file_service.dart';
|
||||
import 'package:paperless_mobile/features/login/cubit/authentication_cubit.dart';
|
||||
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
|
||||
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
|
||||
@@ -42,6 +45,7 @@ import 'package:paperless_mobile/routes/navigation_keys.dart';
|
||||
import 'package:paperless_mobile/routes/typed/branches/landing_route.dart';
|
||||
import 'package:paperless_mobile/routes/typed/shells/authenticated_route.dart';
|
||||
import 'package:paperless_mobile/routes/typed/top_level/add_account_route.dart';
|
||||
import 'package:paperless_mobile/routes/typed/top_level/app_logs_route.dart';
|
||||
import 'package:paperless_mobile/routes/typed/top_level/changelog_route.dart';
|
||||
import 'package:paperless_mobile/routes/typed/top_level/logging_out_route.dart';
|
||||
import 'package:paperless_mobile/routes/typed/top_level/login_route.dart';
|
||||
@@ -85,7 +89,11 @@ Future<void> performMigrations() async {
|
||||
final requiresMigrationForCurrentVersion =
|
||||
!performedMigrations.contains(currentVersion);
|
||||
if (requiresMigrationForCurrentVersion) {
|
||||
logger.t("Applying migration scripts for version $currentVersion");
|
||||
logger.fd(
|
||||
"Applying migration scripts for version $currentVersion",
|
||||
className: "",
|
||||
methodName: "performMigrations",
|
||||
);
|
||||
await migrationProcedure();
|
||||
await sp.setStringList(
|
||||
'performed_migrations',
|
||||
@@ -115,7 +123,15 @@ Future<void> _initHive() async {
|
||||
void main() async {
|
||||
runZonedGuarded(() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await FileService.instance.initialize();
|
||||
|
||||
logger = l.Logger(
|
||||
output: MirroredFileOutput(),
|
||||
printer: FormattedPrinter(),
|
||||
level: l.Level.trace,
|
||||
);
|
||||
Paint.enableDithering = true;
|
||||
|
||||
// if (kDebugMode) {
|
||||
// // URL: http://localhost:3131
|
||||
// // Login: admin:test
|
||||
@@ -128,12 +144,6 @@ void main() async {
|
||||
// .start();
|
||||
// }
|
||||
|
||||
logger = l.Logger(
|
||||
output: MirroredFileOutput(),
|
||||
printer: SpringBootLikePrinter(),
|
||||
level: l.Level.trace,
|
||||
);
|
||||
|
||||
packageInfo = await PackageInfo.fromPlatform();
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
@@ -168,7 +178,6 @@ void main() async {
|
||||
);
|
||||
// Manages security context, required for self signed client certificates
|
||||
final sessionManager = SessionManager([
|
||||
languageHeaderInterceptor,
|
||||
PrettyDioLogger(
|
||||
compact: true,
|
||||
responseBody: false,
|
||||
@@ -178,6 +187,7 @@ void main() async {
|
||||
requestHeader: false,
|
||||
logPrint: (object) => logger.t,
|
||||
),
|
||||
languageHeaderInterceptor,
|
||||
]);
|
||||
|
||||
// Initialize Blocs/Cubits
|
||||
@@ -225,14 +235,19 @@ void main() async {
|
||||
),
|
||||
),
|
||||
);
|
||||
}, (error, stack) {
|
||||
}, (error, stackTrace) {
|
||||
// Catches all unexpected/uncaught errors and prints them to the console.
|
||||
String message = switch (error) {
|
||||
final message = switch (error) {
|
||||
PaperlessApiException e => e.details ?? error.toString(),
|
||||
ServerMessageException e => e.message,
|
||||
_ => error.toString()
|
||||
_ => null
|
||||
};
|
||||
logger.e(message, stackTrace: stack);
|
||||
logger.fe(
|
||||
"An unexpected error occurred${message != null ? "- $message" : ""}",
|
||||
error: message == null ? error : null,
|
||||
methodName: "main",
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -270,7 +285,7 @@ class _GoRouterShellState extends State<GoRouterShell> {
|
||||
|
||||
final DisplayMode mostOptimalMode =
|
||||
sameResolution.isNotEmpty ? sameResolution.first : active;
|
||||
logger.d('Setting refresh rate to ${mostOptimalMode.refreshRate}');
|
||||
logger.fi('Setting refresh rate to ${mostOptimalMode.refreshRate}');
|
||||
|
||||
await FlutterDisplayMode.setPreferredMode(mostOptimalMode);
|
||||
}
|
||||
@@ -336,6 +351,7 @@ class _GoRouterShellState extends State<GoRouterShell> {
|
||||
$loggingOutRoute,
|
||||
$addAccountRoute,
|
||||
$changelogRoute,
|
||||
$appLogsRoute,
|
||||
$authenticatedRoute,
|
||||
],
|
||||
),
|
||||
|
||||
26
lib/routes/typed/top_level/app_logs_route.dart
Normal file
26
lib/routes/typed/top_level/app_logs_route.dart
Normal file
@@ -0,0 +1,26 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:paperless_mobile/core/logging/cubit/app_logs_cubit.dart';
|
||||
import 'package:paperless_mobile/core/logging/view/app_logs_page.dart';
|
||||
import 'package:paperless_mobile/routes/navigation_keys.dart';
|
||||
import 'package:paperless_mobile/theme.dart';
|
||||
|
||||
part 'app_logs_route.g.dart';
|
||||
|
||||
@TypedGoRoute<AppLogsRoute>(path: '/app-logs')
|
||||
class AppLogsRoute extends GoRouteData {
|
||||
static final $parentNavigatorKey = rootNavigatorKey;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return AnnotatedRegion(
|
||||
value: buildOverlayStyle(Theme.of(context)),
|
||||
child: BlocProvider(
|
||||
create: (context) =>
|
||||
AppLogsCubit(DateTime.now())..loadLogs(DateTime.now()),
|
||||
child: AppLogsPage(key: state.pageKey),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import 'package:paperless_mobile/routes/utils/dialog_page.dart';
|
||||
|
||||
part 'changelog_route.g.dart';
|
||||
|
||||
@TypedGoRoute<ChangelogRoute>(path: '/changelogs)')
|
||||
@TypedGoRoute<ChangelogRoute>(path: '/changelogs')
|
||||
class ChangelogRoute extends GoRouteData {
|
||||
static final $parentNavigatorKey = rootNavigatorKey;
|
||||
@override
|
||||
|
||||
@@ -1657,7 +1657,7 @@ packages:
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: synchronized
|
||||
sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60"
|
||||
|
||||
@@ -99,6 +99,7 @@ dependencies:
|
||||
shared_preferences: ^2.2.1
|
||||
flutter_markdown: ^0.6.18
|
||||
logger: ^2.0.2+1
|
||||
synchronized: ^3.1.0
|
||||
# camerawesome: ^2.0.0-dev.1
|
||||
|
||||
dependency_overrides:
|
||||
|
||||
Reference in New Issue
Block a user